Ever wondered how to keep track of every Custom Resource Definition (CRD) in your Kubernetes cluster without writing endless code for each one? Monitoring all CRDs efficiently is crucial as teams adopt more custom resources for flexibility and scaling.
This article explains how to build a custom controller using Go’s dynamic informer, so you can monitor all CRDs seamlessly. You’ll get step-by-step guidance, practical tips, and key insights to streamline your Kubernetes workflows. Let’s dive in!
Custom Controllers: Monitoring All CRDs with Dynamic Informers in Golang
Building robust Kubernetes operators and controllers often raises the question: how can you watch all resources of any CustomResourceDefinition (CRD) dynamically, especially when you don’t know in advance which CRDs may exist? Fortunately, with the flexibility of Kubernetes client libraries in Golang, particularly the dynamic informer, you can monitor all CRD resources effectively.
Let’s explore how you can design a custom controller in Go to monitor all CRDs using dynamic informers, breaking down the process, benefits, challenges, and best practices for real-world success.
Understanding the Problem
When working with Kubernetes, controllers frequently watch resources—like Pods or Deployments—by interacting with the Kubernetes API. For built-in types, typed informers (e.g., PodInformer) are available. CRDs, however, are custom types, and you may not know their definitions ahead of time. This unknown aspect is where static informers fall short.
Dynamic informers, on the other hand, allow you to:
- Handle resources defined at runtime without pre-generated code.
- Monitor any CRD as soon as it’s added to the cluster, even if it didn’t exist at start-up.
How to Monitor All CRDs with a Dynamic Informer in Go
At a high level, your custom controller will:
1. Discover all active CRDs.
2. For each CRD, start a dynamic informer to watch resources defined by that CRD.
3. Handle (add, update, delete) events from each CRD’s custom resources.
Let’s break down these steps in detail.
1. Set Up Kubernetes Client Configuration
Before you interact with the Kubernetes API, you need a REST configuration and the appropriate clients:
import (
"k8s.io/client-go/rest"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/clientcmd"
)
// Example: Load config from kubeconfig or use in-cluster config
config, err := rest.InClusterConfig()
// Use clientcmd.BuildConfigFromFlags() if running locally
2. Create a Dynamic Client
With your configuration, initialize the dynamic client:
dynClient, err := dynamic.NewForConfig(config)
if err != nil {
// handle error
}
This client can watch and manipulate any resource without pre-generated Golang code.
3. Discover All CRDs
Use the API to list all custom resource definitions:
import (
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
)
crdClient, err := clientset.NewForConfig(config)
crdList, err := crdClient.ApiextensionsV1().CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{})
for _, crd := range crdList.Items {
// crd.Spec.Group, crd.Spec.Names.Plural, crd.Spec.Version, etc.
}
Each CRD defines how its resources appear in the API.
4. Set Up a Dynamic SharedInformerFactory
A SharedInformerFactory
manages shared informers for resources. With dynamic CRDs, use the dynamicinformer
package:
import (
"k8s.io/client-go/dynamic/dynamicinformer"
"time"
)
factory := dynamicinformer.NewDynamicSharedInformerFactory(dynClient, time.Minute)
5. Create Informers for Each CRD
For each CRD, construct the GroupVersionResource (GVR), and start the informer:
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
informer := factory.ForResource(gvr).Informer()
6. Register Event Handlers
Set up handlers for add, update, and delete events:
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { /* handle create */ },
UpdateFunc: func(oldObj, newObj interface{}) { /* handle update */ },
DeleteFunc: func(obj interface{}) { /* handle delete */ },
})
7. Start the Informers
Finally, launch the factory and its informers in your main controller loop:
stopCh := make(chan struct{})
defer close(stopCh)
factory.Start(stopCh)
factory.WaitForCacheSync(stopCh)
// Now all handlers will receive events as they happen
Key Benefits of Dynamic Informers for CRDs
- Zero Boilerplate: You don’t need static Go structs for each CRD. Dynamism means your controller works no matter how many CRDs are present or created in the future.
- Automatic Discovery: Easily adapt to new CRDs introduced after your controller starts.
- Single Controller Pattern: Streamline your codebase—support arbitrary CRDs with less duplication.
Practical Tips & Best Practices
Building on the steps above, consider these crucial points for a production-ready controller.
Watch for New CRDs at Runtime
- Periodically re-discover and create new informers when new CRDs are registered.
- Start a watcher on the CRD objects themselves, so you can auto-register informers as CRDs are added or removed.
Start Informers Only Once per GVR
- Keep a map or set of CRDs you are already watching to prevent duplicate informers.
Namespace Considerations
- Choose to watch resources cluster-wide or within specific namespaces, depending on requirements.
Error Handling & Resiliency
- Handle informer failures gracefully with retries or back-off strategies.
- Log failed events but continue processing others.
Resource Cleanup
- Watch for CRD deletion. When a CRD itself is deleted, ensure your controller stops its associated informer to free resources.
RBAC Configuration
- Make sure your controller ServiceAccount has list/watch permissions for all CRDs and for CustomResourceDefinitions themselves.
Common Challenges
While the dynamic informer approach is powerful, watch out for:
- Scaling Issues: If your cluster has hundreds of CRDs, running an informer for each can use significant resources.
- Complex Event Data: CRD objects are returned as unstructured JSON; parsing them is less type-safe than using generated Go structs.
- Event Filtering: Not all CRDs may interest your controller—decide which CRDs to filter or ignore.
Example Use Case: Multi-CRD Operator
Imagine you want to build an operator that reacts whenever any custom resource, from any CRD, is created in the cluster. By using the dynamic informer pattern:
- You discover CRDs at startup and as they’re added later.
- For each, register an informer and handle events regardless of resource type.
- Your controller remains generic, extensible, and future-proof.
Cost Considerations
If you are running your controller as part of a SaaS or managed cloud service, keep in mind:
- More Informers ⟹ More Memory & CPU: Monitoring every CRD increases resource usage. Scale your controller’s pod accordingly.
- API Rate Limits: Polling and watching can hit API limits in large clusters.
- Container Images: Avoid unnecessary dependencies to keep your controller’s image small and quick to ship & deploy.
Conclusion
Using a dynamic informer in Golang empowers you to build custom controllers that can watch and manage any CRD—present or future—without writing repetitive code for each resource. This dynamic, scalable approach is perfect for creating universal operators, auditing tools, or admin utilities that work across the ever-expanding Kubernetes ecosystem.
While implementation is straightforward, thoughtful resource management, correct RBAC, and good event handling practices are key to building a reliable solution.
Frequently Asked Questions (FAQs)
What is a dynamic informer, and how does it differ from a typed informer?
A dynamic informer is a Kubernetes client component that watches for changes to any resource at runtime, using generic “unstructured” data. Typed informers, by contrast, are generated for each resource type and offer type safety. Dynamic informers are ideal when you don’t know the resource’s types at compile time, such as for arbitrary CRDs.
How can I automatically watch new CRDs as they are created?
Monitor the CustomResourceDefinition objects themselves to detect new CRDs. When you see a new CRD added, extract its Group, Version, and Resource details, and start a new dynamic informer for its resources. Also, periodically reconcile your set of watched CRDs to capture changes.
Is it safe to watch all CRDs in a large cluster?
Yes, but with caution. Monitoring hundreds of CRDs can consume significant memory and processing power. Optimize by filtering for only the CRDs you care about, and ensure your controller pod is allocated sufficient resources. Always handle errors gracefully.
How do I process CRD events received in an unstructured form?
When using dynamic informers, event objects are returned as “unstructured.Unstructured” types (essentially, JSON objects). You can extract fields using generic map access methods (e.g., .Object["spec"]
) or convert them to Go structs if you know their schema. Use helper functions for frequent operations.
What RBAC permissions are needed for a dynamic informer-based controller?
The controller’s ServiceAccount needs the ability to list
and watch
all CRD resources, and also to access the CustomResourceDefinition
resources themselves. This may require granting cluster-wide permissions on some or all groups and resources, so ensure only trusted workloads use these permissions.