Kubernetes, Crossplane, and Atlas—Better Together

Helder de Jesus Santana

More and more companies are moving away from the original approach to DevOps, where application developers were given access to—and often responsibility for—the tooling used to spin up infrastructure and deploy workloads. While this definitely ticked the self-service box, the overhead in cognitive load was very high for developers; they now had to learn and support what was previously centrally managed (even if that meant a slow ticket-ops approach to provisioning and deploying).

At MongoDB, we're seeing more and more customers moving towards the concept of an Internal Developer Platform. Tooling and governance are centrally managed and developed, often by central teams with titles like "platform engineering". Application developers retain the self-service that was the great value proposition of DevOps, but thanks to the central ownership, they don't suffer the overhead of maintaining (or even needing to fully understand) the tooling they leverage.

The tooling often abstracts the developers from many of the optional or typically unchanged options and settings. This can offer developers a minimal number of decisions to make to meet their needs

It also offers many benefits to the company: not only are developers empowered to move and deliver faster, but governance and compliance become easier thanks to centrally enforced settings (e.g., security best practices like TLS). Such tooling also typically makes it far easier for the company to roll out changes to the centrally owned templates used by application developers, whether that be new defaults or new enforced security settings.

Properly enabling application developers makes their lives easier, and delivering customer value becomes faster than with either a fully centralized approach (with ticket-ops) or a totally decentralized 'classic' DevOps approach. The company retains security, governance, and monitoring oversight, helping meet business requirements. Everyone wins.

Atlas Kubernetes Operator

MongoDB Atlas is a fully managed cloud database service that simplifies deploying, managing, and scaling MongoDB clusters. With features like automated backups, advanced security controls, global clusters, and real-time performance metrics, Atlas is designed to help teams move faster while ensuring the reliability and security of their data infrastructure.

MongoDB Atlas offers a wide range of programmatic management options. Many MongoDB customers are leveraging Kubernetes-native workflows, either where applications are deployed to Kubernetes, or where a centrally managed Internal Developer Platform is run through Kubernetes (regardless of where the applications run). To support this, MongoDB provides the Atlas Kubernetes Operator—an open-source operator that lets you manage Atlas resources declaratively through Custom Resource Definitions (CRDs). This means you can define projects, clusters, database users, IP access lists, and more, directly in YAML files, and the operator will reconcile these specs with the actual state in MongoDB Atlas - in other words, apply your declarative configuration to Atlas.

The value for customers and application development teams is that you can manage Atlas through the same workflow (often GitOps and leveraging tooling like ArgoCD) that you already use to configure your applications running in Kubernetes, or through a Kubernetes-based Internal Developer Platform.

By bridging the gap between GitOps and database management, the Atlas Kubernetes Operator empowers platform engineers to treat Atlas resources as first-class citizens in their Kubernetes ecosystem—just like pods, deployments, or services.

Crossplane

Various tools and solutions are competing to provide a base for a Kubernetes-native Internal Developer Platform.

Crossplane is a tool at the forefront of this, and we're gradually hearing more interest in it among MongoDB customers.

It is a powerful open-source framework that extends Kubernetes into a universal control plane for managing infrastructure and services across environments using standard Kubernetes APIs - whether those services run in Kubernetes or outside. Rather than relying on separate tools or external scripts, Crossplane lets users define and manage infrastructure and workloads in a consistent, declarative, and version-controlled way.

At its core, Crossplane works by installing custom controllers and CRDs into a Kubernetes cluster. These controllers can manage internal or external systems, including cloud services, databases, and more, via APIs, using native Kubernetes resources. This makes it possible to describe infrastructure as code, enforce organizational policies, and integrate infrastructure provisioning into existing GitOps workflows.

The standardization even simplifies any further abstraction a company might implement for its application developers, for example, a GUI for simplified infrastructure provisioning.

Crossplane compositions

Crossplane is very well aligned with the value proposition of an Internal Developer Platform thanks to its ability to abstract infrastructure provisioning behind Compositions and Composite Resources (XRs). This allows platform engineers (with input from other teams, such as security) to define reusable blueprints (sometimes called "golden paths" in the context of Internal Developer Platforms) for common services. The standardization and simplification possible with these templates make it far easier for application developers - whether they use the simplified declarative configuration directly or it is abstracted behind a customer's user interface—all without exposing the underlying complexity or provider-specific details.

Crossplane’s flexibility lies in its ability to:

  • Connect to multiple external systems via providers with custom templates that:
    • Enforce standards through mandatory settings aligned with the organization’s policies (e.g., TLS)
    • Reduce cognitive load for application developers through abstraction that cuts down on what they have to think about when trying to provision something like a MongoDB Atlas cluster
    • Enable centrally implemented changes through central management of the templates, addressing a common problem with other configuration tooling

Key concepts

Concept Description
Managed Resource
This is the end result and the resulting configuration in Kubernetes that defines the thing actually being managed—this might be a server, a VM, or a much lower-level object like an IP access list. So, for Atlas, a managed resource could be a custom resource defining an Atlas project. This is the output of Crossplane and the configuration ultimately applied to do something, like create or update an Atlas Project.
Composite Resource (XR)
This is the input to Crossplane. It's the template, created by a platform team and used (either directly or indirectly) by the application developers. A composite resource might define a single resource in Kubernetes or a cloud service or represent something more akin to a whole stack. For example, a composite resource might describe a full MongoDB Atlas setup, including a project, a cluster, and a user.
Composite Resource Definition (XRD)
This defines a new custom resource type for Crossplane—essentially the schema of the template. This enables customers to request infrastructure via a Composite Resource (XR). The central platform team creates this type, but application developers do not directly use it.
Composition
This governs the translation of a Composite Resource (XR) into one or more Managed Resources. An example would be translating a Composite Resource defining an Atlas Project, Cluster, and user into distinct custom resources, which the Application Developer doesn't need to see but which are then applied to Atlas via the Atlas Kubernetes Operator.
Claim (optional)
A developer-friendly alias for an XR. Think of it as a simplified interface to create a Composite Resource without exposing platform-specific naming. An optional—but powerful and recommended—further abstraction and simplification.
Crossplane Provider
Crossplane Providers are secondary to the inbuilt capabilities in the previous concepts. Providers are extensions to Crossplane that typically enable management of a specific service or workload. They're akin to Terraform Providers, and many Crossplane Providers are even built from an existing Terraform Provider. A Crossplane Provider is one option for actually applying Managed resources - for example, applying the configuration to an external service via APIs.
Kubernetes Operators and Crossplane Kubernetes Providers
Kubernetes Operators are a well-established concept in the Kubernetes ecosystem. They can run services in Kubernetes (e.g., the MongoDB Controllers for Kubernetes Operator, which supports running MongoDB Community or Enterprise Advanced in Kubernetes) or manage external services via APIs (e.g., the MongoDB Atlas Kubernetes Operator, which supports managing Atlas).

When using Crossplane, any Kubernetes Operator can be used thanks to the Crossplane Kubernetes Provider, which enables Crossplane to provide a consistent, simplified, and centrally managed interface in front of any number of existing Kubernetes Operators.

How it works

Below is a visual representation of how Crossplane Compositions work—from claim to managed infrastructure:

Figure 1. Crossplane compositions at work
The top of this diagram starts with a box labeled Application. This box then connects to a box labeled claims, with the line labeled claims composite resource. On the left is a box labeled composite resource definition, which has lines running to the claim box and a box labeled composite resource. The claim box also runs down and connects to the composite resource box. On the right, a box labeled composition connects to the composite resource box. And the composite resource box has a line running down to boxes labeled managed resource.

Workflow:

  1. Application Developer applies a Claim to Kubernetes - likely via a GitOps flow. (This may also be generated through some further abstraction for application developers—e.g., a GUI—and automatically applied to Kubernetes)
  2. Crossplane creates a Composite Resource (XR) from the claim.
  3. Crossplane selects the matching Composition.
  4. The Composition generates one or more managed resources, which are actioned/applied using Crossplane Providers—for example, the Crossplane Kubernetes Provider, which enables the use of the Atlas Kubernetes Operator to apply configuration to Atlas.

In essence, Crossplane shifts the responsibility of infrastructure design and lifecycle management to the central platform team, while giving application development teams a clean, consistent, simplified interface to request the services they need.

MongoDB's support of Crossplane

As described above, Crossplane provides a centrally managed self-service interface for a wide array of services thanks to its flexibility and native support.

Though not directly supported by MongoDB, Crossplane can be used in conjunction with tools like the MongoDB Atlas Kubernetes Operator, thanks to the already mentioned Crossplane Kubernetes Provider, which enables Crossplane to work with any operator.

The following is an example of how this can be done. Bear in mind that this blog is not official guidance on Crossplane (consult the official Crossplane documentation) and is not guaranteed to be kept up to date. It's meant as an illustration of using Crossplane with the Crossplane Kubernetes Provider and the Atlas Kubernetes Operator.

As a result, MongoDB is not able to offer anything beyond best efforts guidance on using the Atlas Kubernetes Operator with Crossplane, though MongoDB officially supports its use by Atlas customers.

Example use case: Self-service data platform for microservices teams

We are going to define a Composite Resource Definition called ProjectEnvironment (which allows users to define specific ProjectEnvironments) and a Composition that governs how a ProjectEnvironment is broken down into the underlying Managed Resources, which in this case define Atlas resources, including Project, Deployment, IP Access List, and Database Users.

So, with one ProjectEnvironment Composite Resource (perhaps applied to Kubernetes via GitOps), our Crossplane Composition will generate several custom resources in Kubernetes. The Atlas Kubernetes Operator will then read these and apply them via the Atlas Admin API to create the various resources in Atlas.

Before diving into the examples, let’s make sure you have the necessary setup in place.

Pre-requisites

To follow the examples below, ensure the following:

  • A Kubernetes cluster is available and running (can be local or managed).
  • Crossplane is installed in the cluster. You can follow the official installation guide.
  • The Atlas Kubernetes Operator is installed. Refer to the official quick start guide.
  • An Atlas Organization has been created and API keys set up and put in place for the Operator to use (all covered in the quickstart guide, but stop after step 4 as we'll be using the Operator via Crossplane!)
    • Ensure your API keys include permission to manage:
    • Projects
    • Deployments (clusters)
    • Database Custom Roles
    • Database Users

Note: In order to allow the crossplane provider-kubernetes to manage Atlas Kubernetes Operator objects, RBAC permissions must be given:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: crossplane:provider:provider-kubernetes:mongodb-atlas
rules:
  - apiGroups:
      - ""
    resources:
      - serviceaccounts
    verbs:
      - '*'
  - apiGroups:
      - atlas.mongodb.com
    resources:
      - "*"
    verbs:
      - "*"
  - apiGroups:
      - rbac.authorization.k8s.io
    resources:
      - "*"
    verbs:
      - "*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: crossplane:provider:provider-kubernetes:mongodb-atlas
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: crossplane:provider:provider-kubernetes:mongodb-atlas
subjects:
  - kind: ServiceAccount
    name: upbound-provider-kubernetes-beb1eef47cde
    namespace: crossplane-system

Step 1: Define the Composite Resource Definition (XRD)

A Composite Resource Definition (XRD) defines a custom API for your platform, offering a self-service interface for users to provision resources. It acts as a template for the Composite Resources (XRs) that your users will create. While it looks like a Kubernetes Custom Resource Definition (CRD), an XRD provides a higher-level abstraction designed for Crossplane.

The following XRD defines a ProjectEnvironment resource. This custom API allows development teams to provision a complete MongoDB Atlas environment, including a project, a database deployment, access lists, and users, by creating a single, simple Kubernetes object.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: projectenvironments.platform.example.org
spec:
  group: platform.example.org
  names:
    kind: ProjectEnvironment
    plural: projectenvironments
  claimNames:
    kind: ProjectEnvironmentClaim
    plural: projectenvironmentsclaims
  defaultCompositeDeletePolicy: Foreground
  • spec.group: Defines the API group for your composite resource, used for organizing related resources.
  • spec.names: Sets the name for the Composite Resource (XR) that platform administrators will see. In this case, it's ProjectEnvironment.
  • spec.claimNames: Defines the developer-facing resource, known as a Composite Resource Claim. Developers create a ProjectEnvironmentClaim, and the platform automatically provisions a corresponding ProjectEnvironment resource based on this definition.
  • spec.defaultCompositeDeletePolicy: Specifies what happens when a ProjectEnvironment is deleted.
    • Background: Deletes the Composite Resource first and cleans up the underlying cloud resources (like the Atlas project and database) in the background. This is faster but can leave resources orphaned if cleanup fails.
    • Foreground: Ensures all underlying cloud resources are successfully deleted before removing the Composite Resource from Kubernetes. This is safer and prevents orphaned resources.

The schema block defines the structure of your new API, including the fields that users can configure (spec) and the information that will be reported back (status).

 versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required:
                - project
                - users
              properties:
                project:
                  type: string
                environment:
                  type: string
                  enum: ["dev", "qa", "prod"]
                  default: dev
                version:
                  type: string
                  enum: ["6.0", "7.0", "8.0"]
                  default: "8.0"
                users:
                  type: array
                  items:
                    type: object
                    required: ["username", "secret"]
                    properties:
                      username:
                        type: string
                      secret:
                        type: string
            status:
              type: object
              properties:
                id:
                  type: string
                connectionStrings:
                  type: object
                  properties:
                    standard:
                      type: string
                    standardSrv:
                      type: string
                    private:
                      type: string
                    privateSrv:
                      type: string
                mongoDBVersion:
                  type: string
      additionalPrinterColumns:
        - name: Project
          type: string
          jsonPath: ".spec.project"
        - name: MongoDB Version
          type: string
          jsonPath: ".status.mongoDBVersion"
  • versions: Contains one or more versions of your API schema.
  • served: When true, this API version is enabled and can be used on the cluster.
  • referenceable: Allows other resources to reference this one, which is useful for building complex compositions where one resource depends on another.
  • openAPIV3Schema: Defines the data structure for your API.
    • spec: The fields that users configure when creating a ProjectEnvironmentClaim. This example includes required fields like project and users, along with optional fields like environment and MongoDB version that have default values.
    • status: The fields that Crossplane will populate with information from the provisioned cloud resources, such as the Atlas project id and database connectionStrings.

For a comprehensive list of all attributes of a CompositeResourceDefinition see its API Reference.

Step 2: Define the composition

This Composition defines the platform logic that translates a ProjectEnvironment into Managed Resources representing all the elements of a full MongoDB Atlas environment. The Managed Resources (representing project, deployment, access list, and users) are then consumed and applied to MongoDB Atlas by the MongoDB Atlas Kubernetes Operator.

This Composition provides the logic that turns a high-level ProjectEnvironment resource into a complete, ready-to-use MongoDB Atlas environment. It acts as the "brain" of the platform, orchestrating the creation of several underlying resources in a specific order.

It uses Pipeline mode, which allows for a sequence of steps, each powered by a Composition Function. This mode is ideal for complex scenarios that require conditional logic, looping, and custom processing beyond simple field mapping.

Step 2.1: Composition definition

This initial block defines that this Composition is responsible for fulfilling requests for ProjectEnvironment resources and that it will use the Pipeline mode for its logic.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: projectenvironments.platform.example.org
spec:
  compositeTypeRef:
    apiVersion: platform.example.org/v1alpha1
    kind: ProjectEnvironment
  mode: Pipeline
  • compositeTypeRef Specifies the Composite Resource type this Composition applies to. In our case, it refers to ProjectEnvironment Composite Resources.
  • mode defines how the composition is executed.
    • Pipeline indicates that a Composition specifies a pipeline of Composition Functions, each of which is responsible for producing composed resources that Crossplane should create or update.
    • Resource indicates that a Composition uses “Patch & Transform” (P&T) composition. This uses an array of resources, each a template for a composed resource.

The pipeline defines the sequence of operations required to build the full environment. Each step can use a different function and is executed in order.

Step 2.2: Atlas project and IP access list

These steps use the patch-and-transform function to create the foundational AtlasProject and a corresponding AtlasIPAccessList.

 pipeline:
    - step: atlas-project-with-ip-access-list
      functionRef:
        name: function-patch-and-transform
      input:
        apiVersion: pt.fn.crossplane.io/v1beta1
        kind: Resources
        patchSets:
          - name: project-ref
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.name"
                toFieldPath: "spec.forProvider.manifest.spec.projectRef.name"
                transforms:
                  - type: string
                    string:
                      type: Format
                      fmt: "%s-project"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.namespace"
                toFieldPath: "spec.forProvider.manifest.spec.projectRef.namespace"
        resources:
          - name: atlas-project
            base:
              apiVersion: kubernetes.crossplane.io/v1alpha2
              kind: Object
              spec:
                readiness:
                  policy: AllTrue
                deletionPolicy: Delete
                forProvider:
                  manifest:
                    apiVersion: atlas.mongodb.com/v1
                    kind: AtlasProject
                providerConfigRef:
                  name: kubernetes-provider
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.name"
                toFieldPath: "metadata.name"
                transforms:
                  - type: string
                    string:
                      type: Format
                      fmt: "%s-project-object"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.name"
                toFieldPath: "spec.forProvider.manifest.metadata.name"
                transforms:
                  - type: string
                    string:
                      type: Format
                      fmt: "%s-project"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.namespace"
                toFieldPath: "spec.forProvider.manifest.metadata.namespace"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.project"
                toFieldPath: "spec.forProvider.manifest.spec.name"
              - type: ToCompositeFieldPath
                fromFieldPath: "status.atProvider.manifest.status.id"
                toFieldPath: "status.id"

          - name: atlas-ip-access-list
            base:
              apiVersion: kubernetes.crossplane.io/v1alpha2
              kind: Object
              spec:
                readiness:
                  policy: AllTrue
                deletionPolicy: Delete
                references:
                  - dependsOn: {}
                forProvider:
                  manifest:
                    apiVersion: atlas.mongodb.com/v1
                    kind: AtlasIPAccessList
                    spec:
                      entries:
                        - cidrBlock: "10.0.16.0/20"
                          comment: "Company Office Network"
                providerConfigRef:
                  name: kubernetes-provider
            patches:
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.name"
                toFieldPath: "metadata.name"
                transforms:
                  - type: string
                    string:
                      type: Format
                      fmt: "%s-ial-object"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.name"
                toFieldPath: "spec.references[0].dependsOn.name"
                transforms:
                  - type: string
                    string:
                      type: Format
                      fmt: "%s-project-object"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.name"
                toFieldPath: "spec.forProvider.manifest.metadata.name"
                transforms:
                  - type: string
                    string:
                      type: Format
                      fmt: "%s-ial"
              - type: FromCompositeFieldPath
                fromFieldPath: "spec.claimRef.namespace"
                toFieldPath: "spec.forProvider.manifest.metadata.namespace"
              - type: PatchSet
                patchSetName: project-ref

This step performs direct field mappings from the incoming ProjectEnvironmentClaim to the new AtlasProject and AtlasIPAccessList resources. It uses a PatchSet to define a reusable way to reference the project's name and namespace, ensuring consistency. For simplicity, the IP access list is hardcoded to a specific CIDR block, but could be parameterized by consuming an input from our composite or by applying custom logic using a custom function.

Step 2.3: Atlas deployment

   - step: atlas-deployment
      functionRef:
        name: function-template-go
      input:
        apiVersion: gotemplating.fn.crossplane.io/v1beta1
        kind: GoTemplate
        source: Inline
        inline:
          template: |
            apiVersion: kubernetes.crossplane.io/v1alpha2
            kind: Object
            metadata:
              name: {{ printf "%s-deployment-object" .observed.composite.resource.spec.claimRef.name }}
              annotations:
                {{ setResourceNameAnnotation (printf "%s-deployment" .observed.composite.resource.spec.claimRef.name) }}
            spec:
              readiness:
                policy: AllTrue
              deletionPolicy: Delete
              references:
                - dependsOn:
                    name: {{ printf "%s-project-object" .observed.composite.resource.spec.claimRef.name }}
              providerConfigRef:
                name: kubernetes-provider
              forProvider:
                manifest:
                  apiVersion: atlas.mongodb.com/v1
                  kind: AtlasDeployment
                  metadata:
                    name: {{ printf "%s-deployment" .observed.composite.resource.spec.claimRef.name }}
                    namespace: {{ .observed.composite.resource.spec.claimRef.namespace }}
                    labels:
                        environment: {{ .observed.composite.resource.spec.environment }}
                  spec:
                    projectRef:
                        name: {{ printf "%s-project" .observed.composite.resource.spec.claimRef.name }}
                    {{- $env := .observed.composite.resource.spec.environment }}
                    {{- if eq $env "dev" }}
                    flexSpec:
                      name: {{ printf "%s-deployment" .observed.composite.resource.spec.project }}
                      terminationProtectionEnabled: false
                      providerSettings:
                        backingProviderName: AWS
                        regionName: US_EAST_1
                    {{- else }}
                    deploymentSpec:
                      tags:
                        - key: environment
                          value: {{ $env }}
                      name: {{ printf "%s-deployment" .observed.composite.resource.spec.project }}
                      clusterType: REPLICASET
                      mongoDBMajorVersion: "{{ .observed.composite.resource.spec.version }}"
                      backupEnabled: false
                      replicationSpecs:
                        - regionConfigs:
                          - providerName: AWS
                            regionName: US_EAST_1
                            priority: 7
                            electableSpecs:
                              instanceSize: M10
                              nodeCount: 3
                            {{- if eq $env "prod" }}
                            readonlySpecs:
                              instanceSize: M40
                              nodeCount: 2
                            autoscaling:
                              compute:
                                enabled: true
                                maxInstanceSize: M50
                              diskGB:
                                enabled: true
                            {{- end }}
                    {{- end }}

A Go template generates the deployment's specification dynamically. It inspects the environment field from the user's claim and creates a cost-effective flexSpec deployment for dev environments, or a more robust deploymentSpec for qa and prod. It also uses references to ensure this deployment is only created after the project from Step 1 is ready.

Step 2.4: Atlas database users

   - step: atlas-database-users
      functionRef:
        name: function-template-go
      input:
        apiVersion: gotemplating.fn.crossplane.io/v1beta1
        kind: GoTemplate
        source: Inline
        inline:
          template: |
            {{- range $index, $user := .observed.composite.resource.spec.users }}
            ---
            apiVersion: kubernetes.crossplane.io/v1alpha2
            kind: Object
            metadata:
              name: {{ printf "%s-user-%s" $.observed.composite.resource.spec.claimRef.name $user.username }}
              annotations:
                {{ setResourceNameAnnotation (printf "%s-user-%s" $.observed.composite.resource.spec.claimRef.name $user.username) }}
            spec:
              readiness:
                policy: AllTrue
              deletionPolicy: Delete
              references:
                - dependsOn:
                    name: {{ printf "%s-project-object" $.observed.composite.resource.spec.claimRef.name }}
              providerConfigRef:
                name: kubernetes-provider
              forProvider:
                manifest:
                  apiVersion: atlas.mongodb.com/v1
                  kind: AtlasDatabaseUser
                  metadata:
                    name: {{ printf "%s-user-%s" $.observed.composite.resource.spec.claimRef.name $user.username }}
                    namespace: {{ $.observed.composite.resource.spec.claimRef.namespace }}
                  spec:
                    projectRef:
                      name: {{ printf "%s-project" $.observed.composite.resource.spec.claimRef.name }}
                      namespace: {{ $.observed.composite.resource.spec.claimRef.namespace }}
                    username: {{ $user.username }}
                    passwordSecretRef:
                      name: {{ $user.secret }}
                      namespace: {{ $.observed.composite.resource.spec.claimRef.namespace }}
                    databaseName: admin
                    roles:
                      - roleName: readWriteAnyDatabase
                        databaseName: admin
                    scopes:
                      - name: {{ printf "%s-deployment" $.observed.composite.resource.spec.project }}
                        type: CLUSTER
            {{- end }}

This step also uses the template-go function, but this time to loop through the user's request and create multiple database users. The template uses a range block to iterate over the users array specified in the ProjectEnvironmentClaim. For each entry in the array, it generates a complete AtlasDatabaseUser resource, linking it to the correct project and password secret. This allows a single claim to stamp out multiple, similar resources.

Step 2.5: Status update

   - step: custom-status-update
      functionRef:
        name: function-template-go
      input:
        apiVersion: gotemplating.fn.crossplane.io/v1beta1
        kind: GoTemplate
        source: Inline
        inline:
          template: |
            {{ if .observed.resources }}
            {{ $project := index .observed.resources "atlas-project" }}
            {{ $deployment := index .observed.resources (printf "%s-deployment" .observed.composite.resource.spec.claimRef.name) }}
            apiVersion: platform.example.org/v1alpha1
            kind: ProjectEnvironment
            status:
              {{ if $project.resource.status.atProvider.manifest }}
              id: {{ $project.resource.status.atProvider.manifest.status.id }}
              {{ end }}
              {{ if $deployment.resource.status.atProvider.manifest }}
              mongoDBVersion: {{ $deployment.resource.status.atProvider.manifest.status.mongoDBVersion }}
              connectionStrings: {{ $deployment.resource.status.atProvider.manifest.status.connectionStrings | toJson }}
              {{ end }}
            {{ end }}

This crucial step uses a Go template not to create a cloud resource, but to feed information back to the user. It inspects the resources created in the previous steps, extracts key information like the project ID, MongoDB version, and connection strings, and patches them into the status field of the ProjectEnvironment resource. This makes vital information available to the developer directly on their claim object.

Step 2.6: Readiness

   - step: readiness-check
      functionRef:
        name: function-auto-ready

The final step uses the function-auto-ready function to determine when the entire composition is complete and healthy. This function automatically inspects all the resources managed by the pipeline and updates the ProjectEnvironment's status conditions accordingly. It signals that the environment is fully provisioned and ready for use only when every component (project, deployment, users) reports a ready state.

For a comprehensive list of all attributes of a Composition resource, see its API reference.

Step 3: Create a composite resource

This is the final step - all of the previous elements are the cogs in the machine, and this is a specific instance of a Composite Resource that is our input to Crossplane. This is what an application development team would use (directly or with further abstraction) to request a MongoDB environment in a self-service manner, using a single claim and secret.

apiVersion: platform.example.org/v1alpha1
kind: ProjectEnvironmentClaim
metadata:
  name: dev-env-claim
spec:
  compositionRef:
    name: projectenvironments.platform.example.org
  project: payments
  environment: dev
  users:
    - username: user1
      secret: user-secret
    - username: user2
      secret: user-secret
    - username: user3
      secret: user-secret

This is a standard Kubernetes Secret that securely stores the password for the database users.

apiVersion: v1
kind: Secret
metadata:
  name: user-secret
  labels:
    atlas.mongodb.com/type: credentials
type: Opaque
stringData:
  password: myH4rdP@ssw0rd

Once this ProjectEnvironmentClaim is applied to the Kubernetes cluster where Crossplane is running:

  • The Composition is selected by Crossplane via compositionRef.name in our ProjectEnvironmentClaim.
  • The Composition instructs Crossplane on the creation of custom resources in Kubernetes for:
    • A MongoDB project
    • A dev environment deployment
    • A CIDR-bound IP access list
    • 3 database users, sharing the same password by using the same secret (for simplicity)
  • Once created in Kubernetes, those custom resources are read by the MongoDB Atlas Kubernetes Operator and applied to Atlas via the Atlas Admin API.

The status of the claim reflects the readiness of the composed resources. This means that the Application Developers have visibility into the success/failure/status of what's ultimately being created in MongoDB Atlas. How developers see the claim status depends on how they've applied the claim into Kubernetes—e.g., it might be surfaced in a GUI or through a tool like ArgoCD.

Conclusion

Crossplane is a powerful tool for providing central management and governance while enabling developer self-service with minimal cognitive load.

Though MongoDB does not directly support Crossplane, it's entirely possible to leverage the MongoDB Atlas Kubernetes Operator through Crossplane and the Crossplane Kubernetes Provider.

The self-service data platform design using Crossplane Compositions, CompositeResourceDefinitions, and Kubernetes Provider Object resources empowers microservices teams with declarative infrastructure provisioning for MongoDB Atlas. This approach offers several key benefits:

  • Application developer self-service
    • Write a simplified claim without needing to understand the underlying mechanics or infrastructure.
  • Central enablement and governance
    • Platform Engineers define and version the Composition, while developers only need to interact with a simplified custom API tailored to their use case.
    • Platform Engineers can enforce mandatory settings (enabling central governance) and recommend sensible defaults that simplify application developer use.
  • Flexible environment profiles
    • Environment-specific configurations (dev, qa, prod) can be baked into the templates, enabling consistent infrastructure provisioning with variable capacity and redundancy.
  • Extensibility
    • The use of template-go and patch-and-transform functions allows for rich customization logic, templating, and conditional behavior, going beyond what vanilla Crossplane patching can offer.
  • Namespace isolation & multi-tenancy
    • Claims can be namespace-scoped and operate safely in multi-tenant environments using namespaced secrets and resources.

Despite its many strengths, there are known limitations to this approach, mainly due to the current state of the Crossplane and external operators' interoperability:

  • Deletion Ordering & Finalizer Issues
    • While Crossplane Kubernetes Provider coordinates deletion in a dependency-aware manner, a bug in the Crossplane Kubernetes Provider fails to properly set a finalizer on dependent resources. This means that a parent resource (e.g., AtlasProject) may be deleted before children resources (e.g., AtlasDatabaseUser), and these resources will become orphaned. This is especially problematic because:
    • Some Atlas Kubernetes Operator resources do not support independent references - the orphaned child resources may sit there in an error state due to their dependence on the deleted parent resource.
    • Manual cleanup may be required for such orphaned resources.
  • Readiness may not be fully tracked
    • When using go-template to generate the resource, readiness is not automatically checked.
    • This might mean that the status of resources that a claim is meant to generate or manage might not be up to date by default.
    • This must be mitigated by explicitly generating ClaimConditions to update the status, or using the `auto-ready` function.
    • With Resource mode, or using Resources in the Pipeline mode, this is not an issue, as readiness is automatically checked.

Visit our docs page to learn more about the MongoDB Atlas Kubernetes Operator.