How to Watch for Changes in Custom Resources
In the rapidly evolving landscape of cloud-native computing, Kubernetes has emerged as the de facto orchestrator for containerized applications. Its extensibility, driven by the ability to define and manage Custom Resources (CRs), is a cornerstone of its power. Custom Resources allow users to extend the Kubernetes API, introducing new object types that behave just like native Kubernetes objects, enabling organizations to tailor the platform precisely to their domain-specific needs. However, merely defining these custom resources is only half the story; the true power lies in building reactive systems that can actively watch for changes in these resources and respond dynamically. This article delves into the intricate mechanisms, best practices, and strategic considerations for effectively monitoring and reacting to mutations in Custom Resources, ensuring your cloud-native applications remain robust, intelligent, and self-managing.
The journey of building sophisticated, self-healing, and automated systems within Kubernetes often begins with a fundamental understanding of how to observe the desired state. When an administrator or an automated process declares a new Custom Resource, or modifies an existing one, it represents a change in intent. For the system to fulfill this intent, specialized components, often referred to as controllers or operators, must be constantly vigilant, detecting these changes and initiating the necessary actions to reconcile the actual state with the declared desired state. This continuous loop of observation and action is what breathes life into the declarative nature of Kubernetes, transforming static definitions into dynamic, adaptive infrastructure. Without an effective strategy for watching these changes, Custom Resources would remain mere dormant declarations, incapable of driving real-world operations or automating complex workflows.
The Genesis of Custom Resources: Extending Kubernetes' Universe
Before we embark on the specifics of watching for changes, it's crucial to solidify our understanding of what Custom Resources truly represent within the Kubernetes ecosystem. Kubernetes, at its core, is a declarative system where users describe the desired state of their infrastructure and applications using API objects. These objects, such as Pods, Deployments, Services, and Namespaces, are predefined by Kubernetes itself. They encapsulate common patterns and abstractions for running applications.
However, the real world often presents requirements that go beyond these standard abstractions. Enterprises have unique business logic, proprietary services, or highly specialized infrastructure components that don't fit neatly into existing Kubernetes object types. This is where Custom Resources come into play. A Custom Resource (CR) allows you to introduce your own object kinds into the Kubernetes API, expanding its vocabulary to include domain-specific concepts. For instance, an organization managing a fleet of IoT devices might define a Device Custom Resource, specifying attributes like device ID, firmware version, and desired operational state. A company deploying sophisticated AI models might create an AIModelDeployment CR, detailing model name, version, resource requirements, and serving endpoints. These CRs are backed by CustomResourceDefinitions (CRDs), which are Kubernetes objects themselves that define the schema, scope, and validation rules for your new custom resource types. Once a CRD is created, the Kubernetes API server automatically starts serving your new custom resource, allowing you to create, read, update, and delete instances of it using standard Kubernetes API calls, just like any built-in resource.
The introduction of Custom Resources represents a paradigm shift from a generic platform to a highly specialized application platform. It enables the "Operator Pattern," where human operational knowledge about a specific application (how to deploy it, scale it, back it up, upgrade it) is encoded into software, residing within a Kubernetes controller. This controller continuously watches instances of its associated Custom Resource and takes actions to bring the cluster's state closer to the desired state specified in the CR. This extensibility is not merely an academic feature; it's a critical enabler for building powerful platform-as-a-service (PaaS) solutions atop Kubernetes, managing complex stateful applications, and integrating diverse third-party services seamlessly. The ability to define api endpoints for these custom types, exposing them through the Kubernetes api gateway, transforms Kubernetes from a mere container orchestrator into a truly programmable infrastructure platform.
Why Watching for Changes in Custom Resources is Paramount
The declarative nature of Kubernetes dictates that users describe what they want, not how to achieve it. It is the responsibility of various controllers within the Kubernetes control plane to observe these declarations and enact the necessary changes. For Custom Resources, this responsibility falls upon custom controllers or Operators. The act of "watching for changes" is not a peripheral activity; it is the very heartbeat of these controllers, enabling them to:
- Maintain Desired State: The primary goal of any Kubernetes controller is to ensure that the actual state of the system matches the desired state declared in the resources. If a Custom Resource representing a "DatabaseCluster" is updated to specify a new replica count, the controller must detect this change and provision or de-provision database instances accordingly. Without actively watching, such changes would go unnoticed, leading to a divergence between desired and actual states.
- Enable Automation and Self-Healing: By continuously monitoring CRs, controllers can automate complex operational tasks. For instance, if a
BackupPolicyCR specifies a daily backup schedule, the controller can trigger backup jobs at the appropriate times. Should a component managed by a CR fail (e.g., aLoadBalancerCR defining an external load balancer whose underlying instance becomes unhealthy), the controller can detect this by observing associated status updates (which might also be part of the CR) and initiate recovery actions, such as replacing the failed instance. This self-healing capability significantly reduces manual intervention and improves system resilience. - Facilitate Reactive Scaling and Resource Management: Custom Resources often define the resource requirements or scaling policies for specific applications or services. A
HorizontalPodAutoscalerCR, for example, watches CPU utilization and adjusts pod replicas. Similarly, a custom CR defining a specialized GPU pool could be watched by a controller that allocates GPU resources based on demand, scaling up or down underlying GPU instances as needed. Detecting changes in these CRs allows for dynamic and efficient resource allocation. - Integrate with External Systems: Many Custom Resources act as bridges to external systems. A
KafkaTopicCR might declare a desired Kafka topic configuration, and a controller watches this CR to interact with an external Kafka cluster'sapito create or update the topic. Similarly, aDNSZoneCR could be used to manage DNS records in an external DNS provider. The ability to watch these CRs is fundamental to enabling seamless integration and automation across heterogeneous environments. - Ensure Configuration Consistency and Compliance: In regulated industries or large organizations, maintaining consistent configurations and compliance with policies is paramount. Custom Resources can encapsulate these policies (e.g.,
NetworkPolicyEnforcementCRs orSecurityProfileCRs). Controllers watching these CRs can enforce rules, report deviations, or even automatically remediate non-compliant configurations, ensuring a high degree of operational consistency and adherence to governance requirements.
The significance of watching changes extends beyond mere technical functionality; it underpins the entire operational philosophy of Kubernetes operators. It's the mechanism through which declared intent transforms into tangible reality, allowing for the construction of intelligent, adaptive, and highly automated cloud-native systems. Without this continuous vigilance, Custom Resources would remain static documents, and the promise of a self-managing infrastructure would largely go unfulfilled.
The Mechanisms of Vigilance: How Kubernetes Exposes Changes
At the heart of Kubernetes' reactive capabilities lies its robust API server, which acts as the central communication hub and the single source of truth for the cluster's state. All interactions with Kubernetes, whether by kubectl, other controllers, or external tools, occur via this api server. For watching changes in Custom Resources, Kubernetes provides several sophisticated mechanisms, built upon fundamental api interactions.
The Kubernetes API Server: The Ultimate Source of Truth
Every resource in Kubernetes, including Custom Resources, is exposed via a RESTful api endpoint served by the Kubernetes api server. When you interact with kubectl, you're essentially sending HTTP requests to this api server. For Custom Resources, the typical endpoint structure follows /apis/<group>/<version>/<plural_resource_name>. For example, a DatabaseCluster Custom Resource might be accessible at /apis/example.com/v1/databaseclusters.
Changes to these resources—creations, updates, or deletions—are persisted in etcd, Kubernetes' distributed key-value store. The API server then serves these changes to clients. The crucial insight here is that clients don't poll the API server repeatedly; instead, Kubernetes offers a more efficient "watch" mechanism.
The Kubernetes Watch API: Long-Polling for Events
At its lowest level, Kubernetes provides a "watch" api endpoint. Instead of performing a standard GET request that returns the current state and then closing the connection, a watch request keeps the HTTP connection open. The api server then streams a series of "events" to the client as changes occur for the requested resource type. This is typically implemented using HTTP long-polling or WebSockets, depending on the client library.
A watch request typically looks like GET /apis/<group>/<version>/<plural_resource_name>?watch=true&resourceVersion=<version>. Key components of a watch request include:
watch=true: This query parameter instructs theapiserver to initiate a watch stream instead of a one-time GET.resourceVersion: This is a crucial parameter. When a client initiates a watch, it typically first performs a list operation (GET /apis/...) to get the current state of all resources and notes theresourceVersionof the last resource received. It then starts its watch from thatresourceVersion. This ensures that the client doesn't miss any events that occurred between its list operation and the start of its watch. TheresourceVersionis an opaque identifier that represents the state of the object in etcd. Events streamed from theapiserver will always include the newresourceVersionof the changed object.timeoutSeconds: A client can specify a timeout for the watch connection. If no events occur within this period, the server will close the connection, and the client is expected to re-establish the watch. This is a robust pattern to handle network glitches or server-side connection management.allowWatchBookmarks=true(since Kubernetes 1.16): This feature allows theapiserver to send "bookmark" events, which are watch events that only contain aresourceVersionand no object. These bookmarks indicate that theapiserver has progressed to a certainresourceVersion, even if no relevant objects have changed. This can help clients maintain their watch positions more efficiently, especially during periods of low activity.sendInitialEvents=true(since Kubernetes 1.27): This allows a watch to start with an initial set of objects, eliminating the need for a separate list call. This simplifies client logic and reduces round trips.
When an object changes, the api server sends a watch event object. This event contains:
type: Indicates the type of change (ADDED,MODIFIED,DELETED).object: The full JSON representation of the object after the change. ForDELETEDevents, this is typically the object before deletion.
While the raw Watch API provides fundamental capabilities, directly interacting with it can be complex. Clients must handle connection drops, re-establish watches from the correct resourceVersion, manage large numbers of objects, and filter events. This complexity is why higher-level abstractions are typically used.
Client-go Informers: The Standard Abstraction for Event-Driven Controllers
For building robust Kubernetes controllers, especially in Go (the language Kubernetes itself is written in), the client-go library provides a powerful abstraction called an "Informer." Informers significantly simplify the process of watching and reacting to resource changes. They build upon the raw Watch API to provide:
- Event-Driven Processing: Informers abstract away the details of the watch
apicalls, connection management, andresourceVersionhandling. Instead, they provide callback functions (AddFunc,UpdateFunc,DeleteFunc) that are triggered when changes are detected. This transforms a pull-based observation model into an efficient push-based, event-driven architecture. - Local Caching (Lister and Store): This is perhaps the most critical feature of Informers. Instead of making an
apicall every time a controller needs to retrieve a resource (which would be inefficient and put a heavy load on theapiserver), Informers maintain a local, in-memory cache of the resources they are watching. This cache is kept synchronized with theapiserver through the watch mechanism.- DeltaFIFO: Informers typically use a
DeltaFIFOqueue to store events (add, update, delete) received from the watch stream. This FIFO ensures that events are processed in order and that updates to the same object are coalesced or handled sequentially. - Store: The
Store(often aCacheorIndexer) is populated and updated by theDeltaFIFO. It provides efficient lookup methods (Get,List) for cached objects. This means controllers can query the desired state locally without hitting theapiserver, dramatically improving performance and reducingapiserver load. - Lister: A
Listeris typically built on top of theStoreto provide convenient type-safe access to cached objects, often with indexing capabilities (e.g., listing all pods belonging to a specific node).
- DeltaFIFO: Informers typically use a
- Resynchronization: To guard against potential inconsistencies between the informer's cache and the
apiserver (e.g., due to a temporary network partition orapiserver restart), informers periodically perform a full list operation. This "resync" ensures that any objects missed by the watch stream are eventually caught up and that the cache remains consistent. The resync period is configurable. - Shared Informers: In a cluster, multiple controllers might need to watch the same type of Custom Resource. To avoid each controller opening its own watch connection and maintaining its own cache (which would be wasteful),
client-goprovidesSharedInformerFactory. A shared informer factory creates and manages a single informer instance for each resource type, which is then shared among all interested controllers. This significantly reduces resource consumption (network connections, memory) andapiserver load.
The workflow for an informer-based controller typically involves:
- Setting up a
SharedInformerFactoryfor the specific Kubernetes cluster. - Retrieving an
Informerfor the Custom Resource type (and any other related native resources like Pods, Services, etc., that the controller might need to manage). - Registering event handlers (
AddFunc,UpdateFunc,DeleteFunc) with the informer. These functions typically add the changed object's key (namespace/name) to a work queue. - Starting the informers and waiting for their caches to synchronize.
- Running worker goroutines that continuously pull items from the work queue, retrieve the latest version of the object from the informer's cache, and execute the controller's reconciliation logic.
This architecture forms the backbone of almost all Kubernetes controllers and Operators, providing a robust, scalable, and efficient way to watch for changes in Custom Resources and react to them deterministically.
Table: Comparison of Watch Mechanisms
| Feature | Raw Kubernetes Watch API | Client-go Informers |
|---|---|---|
| Complexity | High (manual connection management, resourceVersion tracking, retry logic) |
Low (abstracts away low-level details) |
| Caching | None (client must implement) | Built-in local cache (Lister/Store) for efficient lookups |
| API Server Load | Potentially high if not implemented carefully (e.g., frequent reconnects, missed resourceVersion) |
Low (single watch connection, local cache reduces GET requests) |
| Event Handling | Raw stream of JSON events | Callback functions (AddFunc, UpdateFunc, DeleteFunc) |
| Resynchronization | Manual implementation required | Built-in periodic resync |
| Shared Access | No inherent sharing mechanism | SharedInformerFactory enables efficient sharing across controllers |
| Use Case | Low-level debugging, custom api clients, very specific scenarios |
Standard for building robust, high-performance Kubernetes controllers and Operators |
This table clearly illustrates why client-go informers are the preferred and standard method for watching Custom Resources in any serious Kubernetes controller development. They provide a foundational gateway to building resilient, scalable, and efficient reactive systems within the Kubernetes ecosystem.
APIPark is a high-performance AI gateway that allows you to securely access the most comprehensive LLM APIs globally on the APIPark platform, including OpenAI, Anthropic, Mistral, Llama2, Google Gemini, and more.Try APIPark now! 👇👇👇
Advanced Concepts and Best Practices for Reliable Observation
While informers provide a powerful abstraction, building truly robust and reliable controllers that effectively watch Custom Resources requires an understanding of several advanced concepts and adherence to best practices. These considerations ensure that your controller can handle edge cases, operate efficiently at scale, and recover gracefully from failures.
Idempotency and Exactly-Once Processing (or Close Enough)
The Kubernetes watch mechanism guarantees at-least-once delivery of events. This means that an event might be delivered multiple times (e.g., due to a controller restart, network partition, or a resync). Therefore, the reconciliation logic within your controller's event handlers must be idempotent. An idempotent operation is one that can be applied multiple times without changing the result beyond the initial application.
For example, if your controller creates a Deployment based on a MyApplication CR: * AddFunc: Create the Deployment. If the Deployment already exists, this operation should ideally do nothing or simply ensure its state matches the CR. * UpdateFunc: Update the Deployment. If the update is identical to the current state, no action is needed. If the Deployment doesn't exist (e.g., due to an earlier deletion that was missed), recreate it. * DeleteFunc: Delete the Deployment. If the Deployment has already been deleted, this operation should not cause an error.
The key to idempotency is focusing on reconciling the desired state (from the CR) with the actual state (from the cluster) rather than reacting purely to the event type. The reconciliation loop should always strive to bring the actual state into alignment with the desired state, regardless of how many times it's invoked for the same desired state. This is often achieved by comparing the current state of dependent resources (e.g., Pods, Deployments, Services) with the state implied by the Custom Resource and performing only the necessary delta operations.
Work Queues and Rate Limiting
Event handlers for informers typically don't perform the reconciliation logic directly. Instead, they push the "key" (e.g., namespace/name) of the changed Custom Resource into a work queue. Worker goroutines then pull keys from this queue, fetch the latest version of the object from the informer's local cache, and execute the reconciliation.
A work queue offers several benefits:
- Concurrency Control: You can limit the number of worker goroutines processing items from the queue, controlling the concurrency of your reconciliation logic.
- Decoupling: Event handling is fast and lightweight, preventing bottlenecks in the informer's event processing.
- Rate Limiting/Backoff: Crucially, work queues can be rate-limited. If a reconciliation attempt fails (e.g., due to a temporary
apiserver error, a conflicting update, or an external dependency being unavailable), the item can be re-added to the queue with an exponential backoff. This prevents "busy-looping" on transient errors and reduces load on theapiserver and other external systems.client-goprovidesRateLimitingQueueimplementations that handle this automatically.
Leader Election for High Availability
For critical Custom Resources, you'll often want multiple instances of your controller running for high availability. However, only one instance should be actively performing reconciliation at any given time to prevent conflicting updates and race conditions. Kubernetes provides a standard mechanism for leader election using ConfigMaps or Leases.
Multiple controller instances can contend for a leader election lock. Only one successfully acquires the lock and becomes the leader, actively performing reconciliation. The other instances run as replicas, constantly trying to acquire the lock. If the leader fails, one of the replicas will acquire the lock and take over, ensuring continuous operation. client-go provides utilities for implementing leader election, typically using the LeaderElector or LeaderElectorConfig mechanisms. This ensures that even if a controller instance crashes, another quickly picks up the watch and reconciliation, maintaining the desired state of your Custom Resources.
Health Checks and Metrics
A robust controller must not only perform its reconciliation task but also expose its operational health and performance.
- Liveness and Readiness Probes: Standard Kubernetes probes (HTTP, TCP, exec) should be configured for your controller Pods.
- Liveness Probe: Ensures the controller process is still running and responsive. If it fails, Kubernetes will restart the Pod.
- Readiness Probe: Indicates whether the controller is ready to process requests. For an informer-based controller, this typically means waiting until all its informers' caches have synchronized with the
apiserver. A controller is not "ready" to reconcile until its local view of the cluster state is consistent.
- Metrics: Instrumenting your controller with metrics is invaluable for observability.
- Work Queue Depth: How many items are pending in the work queue? A consistently high depth indicates a bottleneck.
- Reconciliation Duration: How long does it take for a typical reconciliation cycle to complete?
- API Calls: Number of
apicalls made to the Kubernetesapiserver or external services. - Error Rates: How often does reconciliation fail?
- Custom Resource Status: Your controller should update the
.statusfield of the Custom Resource to reflect its current state, conditions, and any observed errors. This provides immediate feedback to users. Prometheus is the de facto standard for metrics collection in Kubernetes, andclient-goincludes Prometheus instrumentation for its components.
Security Considerations: RBAC
When your controller watches Custom Resources, it needs appropriate permissions. These permissions are granted via Kubernetes Role-Based Access Control (RBAC).
Your controller's ServiceAccount must be bound to Roles that grant: * watch, get, list permissions on its target Custom Resource (kind: mycustomresource). * watch, get, list, create, update, delete permissions on any standard Kubernetes resources (e.g., Pods, Deployments, Services) that it manages or modifies. * watch, get, list permissions on any other Custom Resources or standard resources it needs to read for its logic (e.g., Secret for credentials, ConfigMap for configuration).
Insufficient permissions will lead to api call errors and prevent your controller from functioning correctly. Overly permissive roles are a security risk. Always adhere to the principle of least privilege.
Performance at Scale: Optimizing Watchers
At very large scales (clusters with thousands of nodes, tens of thousands of resources, or highly active Custom Resources), performance of watch mechanisms becomes critical.
- Field Selectors and Label Selectors: When setting up an informer, you can often specify
FieldSelectororLabelSelectorto narrow down the scope of resources it watches. For example, watching only Custom Resources with a specific label or in a particular namespace can significantly reduce the volume of events the informer processes, improving efficiency. - Namespace-Scoped Informers: If your controller only operates within a specific namespace, configure your informers to be namespace-scoped (
informerFactory.ForResource(gvr).Namespace(namespace)). This reduces memory usage andapiserver load by only caching resources from that namespace. - Efficient Reconciliation Logic: The reconciliation function itself should be optimized. Avoid complex computations inside the reconciliation loop if they can be pre-computed. Minimize
apicalls to external services. Use efficient data structures. - Garbage Collection: Ensure your controller properly garbage collects resources it creates (e.g., adding
OwnerReferencesto managed resources so they are automatically deleted when the parent CR is deleted). Orphaned resources can lead to performance degradation and resource leaks.
By meticulously applying these advanced concepts and best practices, developers can build controllers that not only reliably watch for changes in Custom Resources but also operate efficiently, securely, and resiliently in demanding cloud-native environments, providing a dependable gateway for automation and self-management.
Connecting Custom Resources to API Management and Gateways
The true power of Custom Resources often extends beyond internal cluster automation, bridging the gap between internal infrastructure state and external service exposure. This is where the concepts of api, api gateway, and gateway become directly relevant. Custom Resources can serve as the declarative foundation for defining, configuring, and managing the lifecycle of APIs that expose services, including those managed by custom controllers, to external consumers.
Custom Resources as Declarative API Definitions
Imagine a scenario where an organization deploys numerous microservices, some of which are complex AI models or specialized data processing pipelines. Instead of manually configuring an api gateway for each service, Custom Resources can be used to declare the desired API configuration.
For example, you could define a PublicAPI Custom Resource:
apiVersion: api.example.com/v1
kind: PublicAPI
metadata:
name: sentiment-analysis-api
spec:
serviceRef:
name: sentiment-analyzer
namespace: my-ai-services
path: /v1/sentiment
methods: ["POST"]
authentication:
type: JWT
issuer: https://auth.example.com
rateLimit:
requestsPerSecond: 100
tls:
enabled: true
secretName: api-tls-secret
In this example, the PublicAPI CR declares that an internal sentiment-analyzer service should be exposed at /v1/sentiment with specific authentication and rate-limiting policies.
A dedicated controller would watch for changes to PublicAPI Custom Resources. Upon detecting an ADDED or MODIFIED event for sentiment-analysis-api, this controller would then: 1. Extract Configuration: Parse the spec of the PublicAPI CR. 2. Interact with an API Gateway: Use the api of an external or in-cluster api gateway (e.g., Kong, Envoy, Nginx, or a specialized AI gateway) to configure the routing, authentication, and policies as defined in the CR. This might involve generating configuration files, making api calls to the gateway's control plane, or updating specific gateway-related resources. 3. Update Status: Update the status field of the PublicAPI CR to indicate whether the API was successfully provisioned on the gateway and to provide its external endpoint.
This pattern makes the api gateway's configuration entirely declarative and version-controlled within Kubernetes, leveraging the same GitOps principles applied to other cluster resources. Changes to the API definition become simple kubectl apply operations on the Custom Resource, and the controller ensures the gateway reflects the desired state.
API Gateways Consuming Custom Resources
Beyond defining specific PublicAPI resources, an api gateway itself can be configured directly through Custom Resources. The Gateway API, a collaborative open-source project, is a prime example. It defines a set of Custom Resources (GatewayClass, Gateway, HTTPRoute, TCPRoute, TLSRoute, UDPRoute) that allow users to configure various aspects of ingress and API routing in a portable and expressive manner. Different gateway implementations (like Envoy-based gateways, Nginx, etc.) can then implement these CRDs, watching them to configure their underlying infrastructure.
In this scenario: * A user creates an HTTPRoute CR to define how traffic for a specific host and path should be routed to an internal Kubernetes service. * A gateway controller (e.g., a specific implementation of the Gateway API) watches these HTTPRoute CRs. * Upon detecting a change, the gateway controller translates the HTTPRoute definition into the native configuration of its underlying proxy technology (e.g., Envoy's xDS configuration, Nginx config files). * The gateway then applies this configuration, dynamically updating its routing rules without requiring a restart or manual intervention.
This approach provides a powerful and standardized way to manage the entry point to your cluster and its services, making the api gateway an integral part of the Kubernetes control plane, driven by Custom Resources.
APIPark: Enhancing Custom Resource-Driven API Exposure
Imagine you've successfully defined complex internal services, perhaps even AI inference endpoints, using Custom Resources within your Kubernetes cluster. Your custom controllers meticulously manage the lifecycle of these services, ensuring they are always running and correctly configured. Now, how do you expose these services reliably, manage their lifecycle effectively, apply advanced policies (like prompt engineering for AI services), track usage, and ensure secure access for external consumers? This is precisely where an intelligent api gateway and API management platform like APIPark becomes invaluable.
APIPark complements a Custom Resource-driven architecture by providing a sophisticated layer for API management, especially tailored for AI and REST services. While your CRs might define the intent for internal service deployment and configuration, APIPark focuses on the exposition and governance of these services as external APIs.
Here's how APIPark naturally fits into this ecosystem:
- Unified API Format for AI Invocation: If your Custom Resources define various AI models (e.g.,
ImageRecognitionModel,NaturalLanguageModel), APIPark can provide a unifiedapiformat for invoking them. This means your application developers don't need to know the specific underlyingapiof each model; they interact with a standardizedapiexposed by APIPark, abstracting away the complexity introduced by diverse AI model infrastructures potentially configured via CRs. - Prompt Encapsulation into REST API: Your Custom Resources might define the core AI model. APIPark allows you to quickly combine these AI models with custom prompts to create new, specialized REST APIs. For instance, a CR might define a general-purpose large language model (LLM), and APIPark enables you to expose "Sentiment Analysis API" or "Text Summarization API" using that LLM with specific prompts, turning internal capabilities into consumable external APIs managed through a user-friendly
api gateway. - End-to-End API Lifecycle Management: Once your internal services, defined and managed by CRs and their controllers, are ready for external consumption, APIPark offers comprehensive lifecycle management. It helps regulate API design, publication, invocation, and decommission, managing traffic forwarding, load balancing, and versioning. This extends the declarative management of CRs to the external API surface, ensuring consistency and control over how your services are consumed.
- API Service Sharing and Access Permissions: APIPark provides a centralized developer portal where all API services, regardless of how they are internally managed (e.g., by CRs), can be displayed and shared within teams or across tenants. It enables independent API and access permissions for each tenant, adding a crucial layer of security and governance that your internal CR-driven system might not natively provide for external consumption.
- Performance and Observability: With its high-performance
gatewayrivaling Nginx, APIPark ensures that your CR-defined services, when exposed as APIs, can handle large-scale traffic. Furthermore, its detailed API call logging and powerful data analysis features provide invaluable insights into the consumption patterns and performance of your APIs, offering a comprehensive view that complements the internal observability of your CR-driven controllers.
In essence, while Custom Resources and their controllers are expertly managing the "what" and "how" of your internal services within Kubernetes, APIPark steps in to manage the "who," "where," and "how responsibly" of exposing these services as enterprise-grade APIs. It acts as an intelligent api gateway that harmonizes with your cloud-native internal architecture, transforming internal capabilities into discoverable, manageable, and secure external offerings. By leveraging APIPark, organizations can effectively turn their Custom Resource-driven microservices into a robust and valuable API product ecosystem, extending their reach and unlocking new business opportunities.
Practical Implementation Examples and Considerations
Bringing these concepts to life requires diving into practical implementation. While a full, production-ready controller is complex, we can illustrate the core principles with a simplified Go example using client-go. We'll then discuss tools and common pitfalls.
Simplified Go Example: Watching a Custom Resource
Let's assume you have a Custom Resource Definition (CRD) for MyResource in the test.io group, version v1.
// myresource.go (Simplified Go struct for MyResource)
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// MyResource is the Schema for the myresources API
type MyResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyResourceSpec `json:"spec,omitempty"`
Status MyResourceStatus `json:"status,omitempty"`
}
// MyResourceSpec defines the desired state of MyResource
type MyResourceSpec struct {
Message string `json:"message"`
Replicas int32 `json:"replicas"`
}
// MyResourceStatus defines the observed state of MyResource
type MyResourceStatus struct {
AvailableReplicas int32 `json:"availableReplicas"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// MyResourceList contains a list of MyResource
type MyResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MyResource `json:"items"`
}
// DeepCopyObject is part of the runtime.Object interface.
func (in *MyResource) DeepCopyObject() runtime.Object {
out := MyResource{}
in.DeepCopyInto(&out)
return &out
}
// DeepCopyObject is part of the runtime.Object interface.
func (in *MyResourceList) DeepCopyObject() runtime.Object {
out := MyResourceList{}
in.DeepCopyInto(&out)
return &out
}
// GroupVersionKind returns the GVK for this type
func (in *MyResource) GroupVersionKind() schema.GroupVersionKind {
return SchemeGroupVersion.WithKind("MyResource")
}
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: "test.io", Version: "v1"}
And a basic informer-based controller:
// main.go
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
// Import our generated clientset and informers
testclientset "github.com/your-org/my-controller/pkg/client/clientset/versioned"
testinformers "github.com/your-org/my-controller/pkg/client/informers/externalversions"
testlisters "github.com/your-org/my-controller/pkg/client/listers/test/v1"
testv1 "github.com/your-org/my-controller/pkg/apis/test/v1"
)
const controllerAgentName = "my-resource-controller"
// Controller is the controller for MyResource objects
type Controller struct {
kubeclientset kubernetes.Interface
testclientset testclientset.Interface
myresourcesLister testlisters.MyResourceLister
myresourcesSynced cache.InformerSynced
workqueue workqueue.RateLimitingInterface
}
// NewController returns a new MyResource controller
func NewController(
kubeclientset kubernetes.Interface,
testclientset testclientset.Interface,
testInformerFactory testinformers.SharedInformerFactory) *Controller {
klog.V(4).Info("Creating event handlers")
// Get a reference to a MyResource informer for the 'test.io/v1' group.
myresourceInformer := testInformerFactory.Test().V1().MyResources()
controller := &Controller{
kubeclientset: kubeclientset,
testclientset: testclientset,
myresourcesLister: myresourceInformer.Lister(),
myresourcesSynced: myresourceInformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "MyResources"),
}
klog.Info("Setting up event handlers for MyResource")
myresourceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.handleAddMyResource,
UpdateFunc: func(old, new interface{}) {
controller.handleUpdateMyResource(old, new)
},
DeleteFunc: controller.handleDeleteMyResource,
})
return controller
}
// handleAddMyResource adds the MyResource to the workqueue
func (c *Controller) handleAddMyResource(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
klog.Errorf("couldn't get key for object %+v: %v", obj, err)
return
}
klog.Infof("MyResource added: %s", key)
c.workqueue.Add(key)
}
// handleUpdateMyResource adds the MyResource to the workqueue
func (c *Controller) handleUpdateMyResource(old, new interface{}) {
oldObj := old.(*testv1.MyResource)
newObj := new.(*testv1.MyResource)
if oldObj.ResourceVersion == newObj.ResourceVersion {
// Periodic resync will send update events for all known objects.
// Two objects are identical if their ResourceVersion is identical.
return
}
key, err := cache.MetaNamespaceKeyFunc(new)
if err != nil {
klog.Errorf("couldn't get key for object %+v: %v", new, err)
return
}
klog.Infof("MyResource updated: %s", key)
c.workqueue.Add(key)
}
// handleDeleteMyResource adds the MyResource to the workqueue for deletion handling
func (c *Controller) handleDeleteMyResource(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
klog.Errorf("couldn't get key for object %+v: %v", obj, err)
return
}
klog.Infof("MyResource deleted: %s", key)
c.workqueue.Add(key)
}
// Run will set up the event handlers for types we are interested in, as well
// as syncing informer caches and starting workers. It will block until stopCh
// is closed, at which point it will shutdown the workqueue and wait for
// workers to finish processing their current work items.
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
defer c.workqueue.ShutDown()
klog.Info("Starting MyResource controller")
klog.Info("Waiting for informer caches to sync")
if ok := cache.WaitForCacheSync(stopCh, c.myresourcesSynced); !ok {
return fmt.Errorf("failed to wait for caches to sync")
}
klog.Info("Starting workers")
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
klog.Info("Started workers")
<-stopCh
klog.Info("Shutting down workers")
return nil
}
// runWorker is a long-running function that will continually call the
// processNextWorkItem function in order to read and process a message on the workqueue.
func (c *Controller) runWorker() {
for c.processNextWorkItem() {
}
}
// processNextWorkItem will read a single work item off the workqueue and
// attempt to process it, by calling the reconcile function.
func (c *Controller) processNextWorkItem() bool {
obj, shutdown := c.workqueue.Get()
if shutdown {
return false
}
// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj interface{}) error {
defer c.workqueue.Done(obj)
var key string
var ok bool
if key, ok = obj.(string); !ok {
c.workqueue.Forget(obj)
klog.Errorf("expected string in workqueue but got %#v", obj)
return nil
}
// Run the syncHandler, passing it the namespace/name string of the MyResource
if err := c.reconcile(key); err != nil {
// Put the item back on the workqueue to handle any transient errors.
c.workqueue.AddRateLimited(key)
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
}
// If no error occurs we Forget this item so it doesn't get queued again until
// another change happens.
c.workqueue.Forget(obj)
klog.Infof("Successfully synced '%s'", key)
return nil
}(obj)
if err != nil {
klog.Error(err)
return true
}
return true
}
// reconcile compares the actual state with the desired state, and attempts to
// converge the two.
func (c *Controller) reconcile(key string) error {
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
klog.Errorf("invalid resource key: %s", key)
return nil
}
// Get the MyResource object from the informer's cache
myresource, err := c.myresourcesLister.MyResources(namespace).Get(name)
if err != nil {
// The MyResource object may no longer exist, in which case we stop processing.
if errors.IsNotFound(err) {
klog.Infof("MyResource '%s' in namespace '%s' no longer exists", name, namespace)
// Here, you would typically clean up any resources previously created by this MyResource.
// E.g., delete a Deployment, Service, etc.
return nil
}
return err
}
klog.Infof("Reconciling MyResource '%s/%s' with message '%s' and replicas %d",
myresource.Namespace, myresource.Name, myresource.Spec.Message, myresource.Spec.Replicas)
// --- Your core reconciliation logic goes here ---
// This is where you would create/update/delete actual Kubernetes resources
// (e.g., Deployments, Services) based on myresource.Spec.
// You might also update myresource.Status here.
// Example: Log the desired message and replicas. In a real controller,
// you'd interact with Kubernetes API to create/update actual pods.
klog.Infof("Desired state for MyResource %s/%s: Message='%s', Replicas=%d",
myresource.Namespace, myresource.Name, myresource.Spec.Message, myresource.Spec.Replicas)
// Update status (example)
// You would fetch the latest MyResource, modify its status, and then call client.UpdateStatus
latestMyResource := myresource.DeepCopy()
latestMyResource.Status.AvailableReplicas = myresource.Spec.Replicas // For simplicity, assume all are available
latestMyResource.Status.Conditions = []metav1.Condition{
{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "ReconciliationSuccessful",
Message: "MyResource successfully reconciled",
},
}
_, err = c.testclientset.TestV1().MyResources(namespace).UpdateStatus(context.TODO(), latestMyResource, metav1.UpdateOptions{})
if err != nil {
klog.Errorf("Failed to update status for MyResource %s/%s: %v", namespace, name, err)
return err
}
// --- End of core reconciliation logic ---
return nil
}
func main() {
klog.InitFlags(nil)
flag.Parse()
// Set up signals so we can handle the first shutdown signal gracefully.
stopCh := setupSignalHandler()
// Load Kubernetes configuration
var cfg *rest.Config
var err error
if kubeconfigPath := os.Getenv("KUBECONFIG"); kubeconfigPath != "" {
cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
klog.Fatalf("Error building kubeconfig from path %s: %s", kubeconfigPath, err.Error())
}
} else {
cfg, err = rest.InClusterConfig()
if err != nil {
klog.Fatalf("Error building in-cluster config: %s", err.Error())
}
}
kubeClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
}
testClient, err := testclientset.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building custom clientset: %s", err.Error())
}
testInformerFactory := testinformers.NewSharedInformerFactory(testClient, time.Second*30) // Resync every 30 seconds
controller := NewController(kubeClient, testClient, testInformerFactory)
// Notice that Start method is non-blocking and runs all registered informers in a dedicated goroutine.
testInformerFactory.Start(stopCh)
if err = controller.Run(1, stopCh); err != nil { // Run with 1 worker thread
klog.Fatalf("Error running controller: %s", err.Error())
}
}
// setupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned
// which is closed on one of these signals. If a second signal is caught, the program
// exits with code 1.
func setupSignalHandler() (stopCh <-chan struct{}) {
s := make(chan os.Signal, 1)
signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
c := make(chan struct{})
go func() {
<-s
close(c)
<-s
os.Exit(1) // Second signal, hard exit
}()
return c
}
This example demonstrates the core components: 1. Clientsets and InformerFactory: Generated from your CRD using k8s.io/code-generator. 2. Controller Structure: Holds clients, listers, and the work queue. 3. Event Handlers: Add/Update/Delete functions push the object's key to the work queue. 4. Work Queue: RateLimitingInterface handles retries with backoff. 5. Reconcile Function: The heart of the controller, where desired state from the CR is compared to actual state, and necessary actions (e.g., creating/updating Deployments) are taken. This function must be idempotent. 6. Run Loop: Starts informers, waits for cache sync, and then starts worker goroutines to process the work queue.
Operator SDK / Kubebuilder: Accelerating Controller Development
Writing client-go controllers from scratch, including clientset generation, informer factories, and error handling, is verbose and prone to boilerplate. To simplify this, the Kubernetes community provides powerful frameworks:
- Operator SDK: A toolkit for building Kubernetes Operators. It provides scaffolding, code generation, and helpers for common Operator patterns like leader election, metrics, and reconciliation loops. It supports Go, Ansible, and Helm-based Operators.
- Kubebuilder: A framework for building Kubernetes APIs using CRDs, similar to Operator SDK (in fact, Operator SDK now leverages Kubebuilder's core libraries). It focuses on generating API types, controllers, and webhooks with minimal boilerplate, emphasizing idiomatic Go and
client-gopatterns.
Both tools significantly reduce the effort required to build a production-grade controller that watches Custom Resources, by generating most of the client-go related code and providing a clear structure for your reconciliation logic. They are highly recommended for any serious controller development.
Debugging Strategies
Debugging reactive systems like Kubernetes controllers can be challenging due to their asynchronous, event-driven nature.
- Logging: Comprehensive, structured logging is paramount. Use
klog(orzapwith controller-runtime) with appropriate verbosity levels (-v). Log the state of CRs, actions taken, and any errors. - Events: Emit Kubernetes events (
c.kubeclientset.CoreV1().Events(namespace).Create(...)) to signal important state changes or errors related to your Custom Resource. These can be viewed withkubectl describeon the relevant object. - Status Subresource: Ensure your controller updates the
.statusfield of your Custom Resource. This is the primary way for users and other controllers to understand the current observed state, conditions, and any errors. - Metrics: As discussed, Prometheus metrics provide invaluable insights into the controller's internal workings, work queue depth, reconciliation duration, and error rates.
- Remote Debugging: For Go controllers, set up remote debugging with tools like Delve within your IDE (e.g., VS Code). This allows you to set breakpoints and step through your code while it runs in the cluster.
- Local Development: Whenever possible, run your controller locally against a development cluster (like Kind or Minikube) to iterate quickly and easily attach debuggers.
Common Pitfalls and How to Avoid Them
Even with robust frameworks, certain issues frequently arise when watching Custom Resources:
- Missing Deletion Events: If your controller goes down or its watch connection breaks, it might miss a
DELETEDevent. When it comes back up, its cache might still hold the deleted object. The periodic resync helps, but more robust deletion handling might be needed. Solution: Always verify the existence of dependent resources before performing operations. When handling aDELETEDevent, clean up associated resources based onOwnerReferencesor explicit deletion logic. TheFinalizerpattern is crucial for managing external resources during deletion; if a CR manages an external service (e.g., a database), aFinalizerensures the controller gets a chance to delete the external resource before the CR is removed from etcd. - Stale Cache / Race Conditions: Your informer's cache might occasionally be slightly behind the
apiserver's actual state, especially during very high churn. If your reconciliation logic makes decisions based solely on cached data and then performs anapioperation, another controller or user might have changed the object in the interim, leading to a race condition. Solution: When performing anUPDATEoperation on a Kubernetes object, always fetch the latest version of the object from theapiserver, apply your changes, and then send the update. Useclient.Update()(which implicitly handlesresourceVersionconflicts with retries) or manually implement retry logic for conflicts. - Event Storms: A single change to a widely used ConfigMap or Secret that your controller watches could trigger a cascade of reconciliation events if many Custom Resources depend on it. Solution: Use appropriate rate limiting on your work queue. Consider event coalescing (e.g., only process the latest version of an object if multiple updates arrive quickly). Think about how shared dependencies impact reconciliation frequency.
- Inefficient API Calls: Repeatedly fetching the same resource from the
apiserver within a reconciliation loop, or making synchronous blocking calls to external systems, can cripple performance. Solution: Leverage informers' local caches (Listers) forGETandLISToperations on resources. Only hit theapiserver forCREATE,UPDATE,DELETE. For externalapicalls, use asynchronous patterns where possible or ensure robust error handling and backoff. - Lack of
OwnerReferences: If your controller creates standard Kubernetes resources (e.g., Deployments, Services) based on a Custom Resource, these dependent resources should haveOwnerReferencespointing back to the Custom Resource. Solution: SetOwnerReferenceon created resources. This ensures that when the Custom Resource is deleted, its dependent resources are automatically garbage-collected by Kubernetes, preventing resource leaks.
By diligently addressing these implementation details and potential pitfalls, you can build Custom Resource controllers that are not only functional but also resilient, efficient, and maintainable, truly harnessing the power of a reactive cloud-native infrastructure.
Conclusion
The ability to define and, crucially, to watch for changes in Custom Resources stands as a testament to the extensible and dynamic nature of Kubernetes. It's the cornerstone upon which operators build sophisticated, domain-specific, and self-managing applications that embody the true spirit of cloud-native automation. From understanding the foundational api watch mechanisms to leveraging the power of client-go informers and adhering to best practices like idempotency and robust error handling, the journey of building reactive systems is one of meticulous design and implementation.
We've explored how Custom Resources empower organizations to extend the Kubernetes API to meet their unique needs, transforming the orchestrator into a highly specialized platform. The imperative to watch for changes is not merely a technical detail; it's the very engine that drives desired state reconciliation, enables intelligent automation, facilitates self-healing, and ensures configuration consistency across complex environments. The Kubernetes api server, acting as the central api gateway for all cluster interactions, provides the fundamental watch api, while client-go informers elevate this capability into a powerful, efficient, and developer-friendly abstraction.
Furthermore, we've seen how Custom Resources transcend internal cluster management, becoming integral to how services are exposed and governed externally. By using CRs to declaratively define API configurations, organizations can streamline the management of their api gateway infrastructure, bringing GitOps principles to API exposure. In this context, platforms like APIPark emerge as essential partners, enhancing the value of Custom Resource-driven services by providing a comprehensive, intelligent API management layer. APIPark ensures that services, whether they are standard REST APIs or complex AI models defined by CRs, are securely exposed, efficiently managed, and seamlessly integrated into a broader API ecosystem, offering unified formats, robust lifecycle management, and critical observability features.
In essence, mastering the art of watching for changes in Custom Resources is about more than just writing code; it's about embracing a paradigm where infrastructure actively responds to intent, where manual toil is replaced by intelligent automation, and where the declarative promise of Kubernetes is fully realized. By diligently applying the principles and techniques discussed, developers and platform engineers can construct truly resilient, scalable, and adaptive cloud-native applications that continuously evolve with their business needs.
Frequently Asked Questions (FAQ)
- What is a Custom Resource (CR) in Kubernetes, and why is it important to watch for changes in them? A Custom Resource (CR) allows you to extend the Kubernetes API by defining your own object types, enabling you to manage domain-specific concepts (e.g., a "DatabaseCluster" or "AIModelDeployment") within Kubernetes. It's crucial to watch for changes in CRs because they represent the desired state of your custom applications or infrastructure. By detecting additions, modifications, or deletions of CRs, specialized controllers (often called Operators) can take necessary actions to reconcile the actual cluster state with the declared desired state, enabling automation, self-healing, and dynamic resource management.
- How does Kubernetes notify clients about changes in Custom Resources? Kubernetes primarily uses a "watch"
apimechanism. Clients can open a long-lived HTTP connection to the Kubernetesapiserver and request to "watch" a specific resource type. Theapiserver then streams "events" (ADD, MODIFIED, DELETED) to the client as changes occur. For Go-based controllers, theclient-golibrary provides higher-level abstractions called "Informers," which simplify this process by handling connection management, maintaining a local cache of resources, and triggering callback functions for detected changes. - What is the role of an "Informer" in watching Custom Resources, and why is it preferred over directly using the Kubernetes Watch API? An Informer, provided by the
client-golibrary, is a powerful abstraction built on top of the raw Kubernetes Watch API. It's preferred because it significantly simplifies controller development by:- Handling low-level watch details: Manages connection lifecycle,
resourceVersiontracking, and re-establishing watches. - Providing a local cache: Maintains an in-memory copy of all watched resources, reducing
apiserver load forGETandLISToperations. - Offering event-driven callbacks: Exposes
AddFunc,UpdateFunc, andDeleteFuncfor easy reaction to changes. - Supporting shared informers: Allows multiple controllers to efficiently share a single watch connection and cache for the same resource type.
- Handling low-level watch details: Manages connection lifecycle,
- How do Custom Resources relate to API Gateways and API Management, and how can APIPark fit into this? Custom Resources can serve as declarative definitions for APIs. For example, a CR could define how an internal service should be exposed externally (e.g., path, authentication, rate limits). A controller watches this CR and configures an
api gatewayaccordingly. APIPark complements this by providing an intelligentapi gatewayand API management platform that extends the governance and exposure of such CR-defined services. APIPark can standardize API formats for AI models, encapsulate prompts into REST APIs, manage the end-to-end API lifecycle, provide centralized sharing, enforce access permissions, and offer high performance and detailed analytics for your APIs, effectively transforming internal, CR-managed services into discoverable, secure, and valuable external offerings. - What are some best practices for building robust controllers that watch Custom Resources? Several best practices ensure controller robustness:
- Idempotency: Ensure reconciliation logic can be safely re-run multiple times without unintended side effects.
- Work Queues with Rate Limiting: Use work queues to process events concurrently and apply exponential backoff for retries on transient errors.
- Leader Election: For high availability, use leader election to ensure only one controller instance is active at a time.
- Health Checks and Metrics: Implement liveness/readiness probes and instrument with Prometheus metrics for observability.
- RBAC: Adhere to the principle of least privilege by granting only necessary
watch,get,list,create,update,deletepermissions to your controller's ServiceAccount. - OwnerReferences and Finalizers: Properly set
OwnerReferencesfor garbage collection of dependent resources, and useFinalizersfor managing external resources during CR deletion.
🚀You can securely and efficiently call the OpenAI API on APIPark in just two steps:
Step 1: Deploy the APIPark AI gateway in 5 minutes.
APIPark is developed based on Golang, offering strong product performance and low development and maintenance costs. You can deploy APIPark with a single command line.
curl -sSO https://download.apipark.com/install/quick-start.sh; bash quick-start.sh

In my experience, you can see the successful deployment interface within 5 to 10 minutes. Then, you can log in to APIPark using your account.

Step 2: Call the OpenAI API.

