Configuring Crossplane with Argo CD

Argo CD and Crossplane are a great combination. Argo CD provides GitOps while Crossplane turns any Kubernetes cluster into a Universal Control Plane for all your resources. Configuration details are required in order for the two to work together properly. This doc will help you understand these requirements. It is recommended to use Argo CD version 2.4.8 or later with Crossplane.

Argo CD synchronizes Kubernetes resource manifests stored in a Git repository with those running in a Kubernetes cluster (GitOps). Argo CD has different ways to configure how it tracks resources. With Crossplane, you need to configure Argo CD to use Annotation based resource tracking. See the Argo CD docs for additional detail.

Configuring Argo CD with Crossplane

Set resource tracking method

In order for Argo CD to track Application resources that contain Crossplane related objects, configure it to use the annotation mechanism.

To configure it, edit the argocd-cm ConfigMap in the argocd Namespace as such:

1apiVersion: v1
2kind: ConfigMap
3data:
4  application.resourceTrackingMethod: annotation

Set health status

Argo CD has a built-in health assessment for Kubernetes resources. The community directly supports some checks in Argo’s repository. For example the Provider from pkg.crossplane.io already exists which means there no further configuration needed.

Argo CD also enable customising these checks per instance, and that’s the mechanism used to provide support of Provider’s CRDs.

To configure it, edit the argocd-cm ConfigMap in the argocd Namespace.

Note
ProviderConfig may have no status or a status.users field.

  1apiVersion: v1
  2kind: ConfigMap
  3data:
  4  application.resourceTrackingMethod: annotation
  5  resource.customizations: |
  6    "*.upbound.io/*":
  7      health.lua: |
  8        health_status = {
  9          status = "Progressing",
 10          message = "Provisioning ..."
 11        }
 12
 13        local function contains (table, val)
 14          for i, v in ipairs(table) do
 15            if v == val then
 16              return true
 17            end
 18          end
 19          return false
 20        end
 21
 22        local has_no_status = {
 23          "ClusterProviderConfig",
 24          "ProviderConfig",
 25          "ProviderConfigUsage"
 26        }
 27
 28        if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
 29          health_status.status = "Healthy"
 30          health_status.message = "Resource is up-to-date."
 31          return health_status
 32        end
 33
 34        if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
 35          if (obj.kind == "ProviderConfig" or obj.kind == "ClusterProviderConfig") and obj.status.users ~= nil then
 36            health_status.status = "Healthy"
 37            health_status.message = "Resource is in use."
 38            return health_status
 39          end
 40          return health_status
 41        end
 42
 43        for i, condition in ipairs(obj.status.conditions) do
 44          if condition.type == "LastAsyncOperation" then
 45            if condition.status == "False" then
 46              health_status.status = "Degraded"
 47              health_status.message = condition.message
 48              return health_status
 49            end
 50          end
 51
 52          if condition.type == "Synced" then
 53            if condition.status == "False" then
 54              health_status.status = "Degraded"
 55              health_status.message = condition.message
 56              return health_status
 57            end
 58          end
 59
 60          if condition.type == "Ready" then
 61            if condition.status == "True" then
 62              health_status.status = "Healthy"
 63              health_status.message = "Resource is up-to-date."
 64            end
 65          end
 66        end
 67
 68        return health_status
 69
 70    "*.crossplane.io/*":
 71      health.lua: |
 72        health_status = {
 73          status = "Progressing",
 74          message = "Provisioning ..."
 75        }
 76
 77        local function contains (table, val)
 78          for i, v in ipairs(table) do
 79            if v == val then
 80              return true
 81            end
 82          end
 83          return false
 84        end
 85
 86        local has_no_status = {
 87          "Composition",
 88          "CompositionRevision",
 89          "DeploymentRuntimeConfig",
 90          "ClusterProviderConfig",
 91          "ProviderConfig",
 92          "ProviderConfigUsage"
 93        }
 94        if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
 95            health_status.status = "Healthy"
 96            health_status.message = "Resource is up-to-date."
 97          return health_status
 98        end
 99
100        if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
101          if (obj.kind == "ProviderConfig" or obj.kind == "ClusterProviderConfig") and obj.status.users ~= nil then
102            health_status.status = "Healthy"
103            health_status.message = "Resource is in use."
104            return health_status
105          end
106          return health_status
107        end
108
109        for i, condition in ipairs(obj.status.conditions) do
110          if condition.type == "LastAsyncOperation" then
111            if condition.status == "False" then
112              health_status.status = "Degraded"
113              health_status.message = condition.message
114              return health_status
115            end
116          end
117
118          if condition.type == "Synced" then
119            if condition.status == "False" then
120              health_status.status = "Degraded"
121              health_status.message = condition.message
122              return health_status
123            end
124          end
125
126          if contains({"Ready", "Healthy", "Offered", "Established", "ValidPipeline", "RevisionHealthy"}, condition.type) then
127            if condition.status == "True" then
128              health_status.status = "Healthy"
129              health_status.message = "Resource is up-to-date."
130            end
131          end
132        end
133
134        return health_status

Set resource exclusion

Crossplane providers generate a ProviderConfigUsage for each managed resource (MR) they handle. This resource enables representing the relationship between MR and a ProviderConfig so that the controller can use it as a finalizer when you delete a ProviderConfig. End users of Crossplane don’t need to interact with this resource.

A growing number of resources and types can impact Argo CD UI reactivity. To help keep this number low, Crossplane recommend hiding all ProviderConfigUsage resources from Argo CD UI.

To configure resource exclusion edit the argocd-cm ConfigMap in the argocd Namespace as such:

1apiVersion: v1
2kind: ConfigMap
3data:
4    resource.exclusions: |
5      - apiGroups:
6        - "*"
7        kinds:
8        - ProviderConfigUsage

The use of "*" as apiGroups enables the mechanism for all Crossplane Providers.

Increase Kubernetes client QPS

As the number of CRDs grow on a control plane it increases the amount of queries Argo CD Application Controller needs to send to the Kubernetes API. If this is the case you can increase the rate limits of the Argo CD Kubernetes client.

Set the environment variable ARGOCD_K8S_CLIENT_QPS to 300 for improved compatibility with multiple CRDs.

The default value of ARGOCD_K8S_CLIENT_QPS is 50, modifying the value also updates ARGOCD_K8S_CLIENT_BURST as it is default to ARGOCD_K8S_CLIENT_QPS x 2.

Cross-namespace resource hierarchy

Argo CD versions before v3.3.0 have a limitation displaying namespaced resources owned by cluster-scoped resources in the application tree. This affects Crossplane deployments where cluster-scoped resources like ProviderRevision create namespaced children like Deployment and Service resources.

The issue

When viewing a Crossplane application in Argo CD versions before v3.3.0, cluster-scoped resources and their cluster-scoped children appear correctly, but namespaced children don’t appear in the resource tree.

For example:

  • ProviderRevision (cluster-scoped parent) appears
  • ClusterRole (cluster-scoped child) appears
  • Deployment (namespaced child) is missing from the tree

This occurs because the gitops-engine’s hierarchy traversal only processes resources within the same namespace, preventing cross-namespace parent-child relationships from being discovered.

Important
The missing resources are still deployed and managed by Argo CD. They just don’t appear in the UI tree visualization.
Example
 1# This cluster-scoped parent appears in Argo CD
 2apiVersion: pkg.crossplane.io/v1
 3kind: ProviderRevision
 4metadata:
 5  name: provider-aws-s3-96df8f51090d
 6
 7---
 8# This namespaced child is missing from the Argo CD tree
 9apiVersion: apps/v1
10kind: Deployment
11metadata:
12  name: provider-aws-s3-96df8f51090d
13  namespace: crossplane-system
14  ownerReferences:
15  - apiVersion: pkg.crossplane.io/v1
16    kind: ProviderRevision
17    name: provider-aws-s3-96df8f51090d
18    controller: true
Resolution

This issue is fixed in Argo CD v3.3.0 and later. Upgrade to Argo CD v3.3.0 or later for full Crossplane resource visibility in the application tree.

After upgrading, verify the fix by expanding a Provider or ProviderRevision resource in the Argo CD UI and confirming that namespaced children like Deployment and Service resources now appear.

Workaround for older versions

If you can’t upgrade to v3.3.0 immediately, use kubectl to verify namespaced resources:

1# List all resources owned by a ProviderRevision
2kubectl get all -n crossplane-system -l pkg.crossplane.io/revision=provider-aws-s3-96df8f51090d
3
4# Check Deployments created by Providers
5kubectl get deployments -n crossplane-system

GitOps synchronization, health status reporting, and automatic reconciliation continue to work correctly. Only the visual representation in the Argo CD UI is affected.

For more details, see Argo CD issue #24379 and PR #24847.