Configuring Crossplane with Argo CD

This document is for an older version of Crossplane.

This document applies to Crossplane v1.20 and not to the latest release v2.4.

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 of 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 correctly track Application resources that contain Crossplane related objects it needs to be configured 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. Some checks are supported by the community directly in Argo’s repository. For example the Provider from pkg.crossplane.io has already been declared 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.

Argo CD v3.1 and later also include built-in health checks for the *.crossplane.io and *.upbound.io API groups. An entry in resource.customizations, including a wildcard entry like the ones in the following example, takes precedence over the built-in health checks for the resources it matches.

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

Set Resource Exclusion

Crossplane providers generates a ProviderConfigUsage for each of the managed resource (MR) it handles. This resource enable representing the relationship between MR and a ProviderConfig so that the controller can use it as finalizer when a ProviderConfig is deleted. End users of Crossplane aren’t expected to interact with this resource.

Argo CD UI reactivity can be impacted as the number of resource and types grow. To help keep this number low we 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 will enable the mechanism for all Crossplane Providers.

Increase Kubernetes Client QPS

As the number of CRDs grow on a control plane it will increase 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 a large number of CRDs.

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