Articom Enterprise CX & AI Kubernetes Operator
3.4K
The Articom Kubernetes License Operator is an enterprise-grade Kubernetes custom controller that automates the deployment, lifecycle management, multi-cloud registry authentication, autoscaling, and networking of the Articom software suite based on validated license keys issued by admin.articom.io.
Deploying and maintaining enterprise microservices across hybrid cloud environments requires synchronized credential distribution, resource provisioning, autoscaling, ingress routing, and secret management.
The Articom License Operator simplifies this process to a single declarative Kubernetes resource (kind: License). When applied, the operator:
https://admin.articom.io/api/licenses/check.Deployments), customized with tuned resource allocations and readiness profiles.ClusterIP Services and configures unified Ingress routing.HorizontalPodAutoscaler (HPA v2) tailored per workload type.The operator follows the standard Kubernetes Operator pattern built with the controller-runtime framework.
┌────────────────────────┐
│ admin.articom.io │
│ (License Server API) │
└───────────▲────────────┘
│
1. Check Key │ 2. Issue Services &
& Expiry │ Multi-Cloud Registry Tokens
│
┌───────────▼────────────┐
│ License Controller │
│ (articom-k8s-crd) │
└───────────┬────────────┘
│
┌─────────────────────┼─────────────────────┐
│ Creates & Manages (Controller OwnerRefs) │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Docker Config │ │ Deployments │ │ ClusterIP Svcs│
│ Registry Secret│ │ (AI, LiveKit,│ │ & Ingress │
│ (GCP/ACR/ECR) │ │ API, SaaS) │ │ (Subdomains) │
└───────────────┘ └───────┬───────┘ └───────────────┘
▼
┌───────────────┐
│ HPA v2 │
│ Autoscaling │
└───────────────┘
internal/controller/license_controller.go)The core reconciliation logic is implemented in LicenseReconciler:
License Custom Resource and owns secondary resources:
ctrl.NewControllerManagedBy(mgr).
For(&articomv1alpha1.License{}).
Owns(&corev1.Secret{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Owns(&autoscalingv2.HorizontalPodAutoscaler{}).
Owns(&networkingv1.Ingress{}).
Named("license").
Complete(r)
admin.articom.io are cached for 50 minutes using the annotation articom.inforwaves.com/last-check. Secondary resource reconciliations triggered by watch events do not hammer the external API.articom.inforwaves.com/content-hash (SHA-256). If credentials have not changed, secret updates are skipped, preventing infinite reconcile loops.Secret, Deployment, Service, HorizontalPodAutoscaler, Ingress) are created or updated using controllerutil.CreateOrUpdate.controllerutil.SetControllerReference(license, child, r.Scheme), ensuring automatic cleanup by the Kubernetes garbage collector when a License is deleted.internal/controller/service_profiles.go)Each Articom service has distinct performance and architecture requirements. DefaultServiceProfiles provisions tailored compute, port, autoscaling, and scheduling policies:
| Service Name | Port | CPU Request / Limit | Memory Request / Limit | Autoscaling (HPA) | Special Configuration |
|---|---|---|---|---|---|
articom-ai-service | 8000 | 50m / 500m | 512Mi / 1000Mi | Min: 1, Max: 3 @ 90% CPU | AI Core microservice |
articom-api-service | 4000 | 20m / 200m | 160Mi / 384Mi | Min: 1, Max: 3 @ 90% CPU | Central REST API gateway |
articom-knowledge-service | 8001 | 20m / 300m | 512Mi / 1Gi | Min: 1, Max: 3 @ 90% CPU | RAG & Knowledge Vector Index |
articom-livekit-api-service | 8000 | 10m / 200m | 64Mi / 128Mi | Min: 1, Max: 3 @ 90% CPU | WebRTC session orchestration |
articom-livekit-dashboard | 8000 | 10m / 250m | 96Mi / 256Mi | Min: 1, Max: 3 @ 90% CPU | LiveKit Administration Portal UI |
articom-saas-service | 3000 | 20m / 250m | 64Mi / 256Mi | Min: 1, Max: 3 @ 90% CPU | Multi-tenant tenant control plane |
articom-voice-livekit-worker | 8002 | 3400m / 3400m | 8Gi / 8Gi | Min: 1, Max: 4 @ 70% CPU | Guaranteed QoS, Dedicated workload: livekit-worker node selector & tolerations, 1800s termination grace period, fast scale-up & stabilized 600s scale-down |
articom-chat-widget-service | 3001 | 10m / 100m | 32Mi / 128Mi | Min: 1, Max: 3 @ 90% CPU | Embedded end-user chat client |
articom-consumer-portal-service | 3000 | 10m / 100m | 64Mi / 128Mi | Min: 1, Max: 3 @ 90% CPU | Customer Self-Service Portal |
articom-kyc-service | 8000 | 20m / 200m | 128Mi / 256Mi | Min: 1, Max: 3 @ 90% CPU | Verification & Identity checks |
articom-license-service | 8000 | 10m / 100m | 64Mi / 128Mi | Min: 1, Max: 3 @ 90% CPU | Internal licensing bridge |
| Default / Fallback | 8080 | 10m / 200m | 64Mi / 256Mi | Min: 1, Max: 3 @ 90% CPU | Fallback for newly introduced services |
When the controller validates a key, it performs an HTTPS GET request to:
GET https://admin.articom.io/api/licenses/check?key=<LICENSE_KEY>
{
"valid": true,
"client": "Acme Enterprise Corp",
"type": "PRODUCTION",
"issuedAt": "2026-01-01T00:00:00Z",
"expiresAt": "2027-01-01T00:00:00Z",
"services": [
"articomacr.azurecr.io/articom-api-service:v2.4.0",
"articomacr.azurecr.io/articom-ai-service:v2.4.0",
"articomacr.azurecr.io/articom-livekit-dashboard:v2.4.0",
"articomacr.azurecr.io/articom-voice-livekit-worker:v2.4.0"
],
"registries": {
"articomacr.azurecr.io": {
"registry": "articomacr.azurecr.io",
"username": "00000000-0000-0000-0000-000000000000",
"token": "eyJhbGciOiJSUzI1NiIs...",
"expiresAt": 1774000000
},
"us-docker.pkg.dev": {
"registry": "us-docker.pkg.dev",
"username": "oauth2accesstoken",
"token": "ya29.a0AfH6SM...",
"expiresAt": 1774000000
}
}
}
The operator seamlessly translates registries into a standard Kubernetes kubernetes.io/dockerconfigjson Secret:
*.pkg.dev): Uses username oauth2accesstoken with temporary bearer tokens.*.azurecr.io): Uses Azure AD Application UUID or token authentication.Group: articom.inforwaves.com | Version: v1alpha1 | Kind: License | Scope: Namespaced
apiVersion: articom.inforwaves.com/v1alpha1
kind: License
metadata:
name: articom-license
namespace: articom-k8s-crd
spec:
key: "<YOUR_LICENSE_KEY>"
secretName: "articom-registry-credentials"
spec)| Field | Type | Required | Default | Description |
|---|---|---|---|---|
key | string | Yes | — | The license key generated from admin.articom.io. |
secretName | string | No | articom-registry-credentials | Name of the kubernetes.io/dockerconfigjson Secret created in the namespace. |
📖 Domain & Ingress Routing: To route public domains to the microservices created by this operator, refer to the Domain & Ingress Routing Guide.
status)| Field | Type | Description |
|---|---|---|
isValid | boolean | true if the license key is valid and active on admin.articom.io. |
client | string | Organization or client name associated with the license. |
type | string | License tier (e.g., DEV, STAGING, PRODUCTION, ENTERPRISE). |
services | []string | Complete list of authorized container images. |
issuedAt | metav1.Time | Timestamp when the license was generated. |
expiresAt | metav1.Time | Expiration timestamp of the license. |
conditions | []metav1.Condition | Kubernetes API condition array tracking status lifecycle (Available, Progressing, Degraded). |
kubectl v1.26+# Install directly from the official OCI registry
helm install articom-operator oci://registry-1.docker.io/articomio/articom-k8s-crd \
--namespace articom-system \
--create-namespace
# Clone the repository
git clone https://github.com/inforwaves/articom-k8s-crd.git
cd articom-k8s-crd
# Install the chart and CRDs
helm install articom-operator ./charts/chart \
--namespace articom-system \
--create-namespace
# 1. Install Custom Resource Definitions (CRDs)
make install
# 2. Deploy the controller manager to your active cluster
make deploy IMG=docker.io/<your-dockerhub-user>/articom-cx-operator:latest
Or deploy directly via the single-file distribution bundle:
kubectl apply -f dist/install.yaml
To test the controller locally against your current kubeconfig context without building container images:
# 1. Install CRDs
make install
# 2. Run controller locally
export ARTICOM_LICENSE_SERVER="https://admin.articom.io/api"
make run
The operator controller manager supports the following configuration options:
| Environment Variable / Flag | Default | Description |
|---|---|---|
ARTICOM_LICENSE_SERVER | https://admin.articom.io/api | Base URL for the license validation server. |
LICENSE_CHECK_INTERVAL | 10m | Periodic interval to check license validity and refresh registry tokens (e.g. 5m, 10m, 1h). |
ENABLE_VAULT | true | Enabled by default. Injects in-namespace Vault Agent annotations into Deployments. Set to "false" to disable. |
VAULT_ROLE | articom-vso-role | Vault Kubernetes auth role used by the Vault Agent injector. |
--leader-elect | false | Enables leader election for high availability with multiple replicas. |
--metrics-bind-address | :8443 | Address the Prometheus metrics server binds to (0 to disable). |
--health-probe-bind-address | :8081 | Address for /healthz and /readyz probes. |
Follow this guide to deploy your licensed Articom services in your Kubernetes cluster.
Create the namespace where your Articom microservices should run:
kubectl create namespace articom-production
License Custom ResourceCreate a manifest named articom-license.yaml.
apiVersion: articom.inforwaves.com/v1alpha1
kind: License
metadata:
name: enterprise-license
namespace: articom-production
spec:
# The license key from admin.articom.io
key: "uzBIqkCs2IjA8SiS.rea_FYC2vtts2n9uqY07Lrsy4TDGmEMyHFRA5qMo6TysirG1M_ApGheafJaXSWj5lA4P6NZ6wTsBM38yRh-0l5g078mopBu9RFp8izSc_2P8XK9q_Jeqgm2YlRNm3zsmZBNQQebN5BgNX9BE0ToA_Dz8qDmUnE3SQkzlLq8jvB2i9j-ArzWOUfemY2sfJzi9qA00rUyuNI2tSaugmDuCiKKTS8-WY-c.YBNLPHzE_s0DEMrVZ_FlRQ"
# Base domain for Ingress
domain: "articom.mycompany.com"
# Ingress class (e.g. cloudflare, nginx, alb)
ingressClassName: "cloudflare"
# Subdomain routing customization
subdomains:
articom-api-service: "api"
articom-livekit-dashboard: "dashboard"
articom-consumer-portal-service: "portal"
articom-chat-widget-service: "chat"
If you only want internal cluster communication and don't need public Ingress:
apiVersion: articom.inforwaves.com/v1alpha1
kind: License
metadata:
name: internal-license
namespace: articom-staging
spec:
key: "FBS_mSn66dK0RMwV.E1h94A6l..."
kubectl:kubectl apply -f articom-license.yaml
Commit articom-license.yaml to your GitOps repository. Argo CD or Flux will sync the resource and the operator will automatically handle the rest.
License Resource Statuskubectl get license -n articom-production
Output:
NAME VALID CLIENT TYPE AGE
enterprise-license true Acme Enterprise Corp PRODUCTION 45s
Inspect detailed conditions, expiration date, and services:
kubectl describe license enterprise-license -n articom-production
# View automatically created Docker Registry Secret
kubectl get secret articom-registry-credentials -n articom-production
# View Deployments and Pods
kubectl get deployments -n articom-production
kubectl get pods -n articom-production
# View ClusterIP Services
kubectl get services -n articom-production
# View Autoscalers (HPAs)
kubectl get hpa -n articom-production
# View Ingress
kubectl get ingress -n articom-production
If you configured domain: "articom.mycompany.com" and subdomains, the operator generated Ingress rules:
https://api.articom.mycompany.comhttps://dashboard.articom.mycompany.comhttps://portal.articom.mycompany.comhttps://chat.articom.mycompany.comEnsure your DNS provider (e.g. Cloudflare, Route 53, Azure DNS) has a wildcard *.articom.mycompany.com or individual CNAME records pointing to your Ingress controller load balancer.
admin.articom.io, the operator automatically detects the updated expiration date and new services during its hourly reconciliation cycle. No manual restart is required.spec.key field in your License YAML and run kubectl apply -f articom-license.yaml.kubectl delete license enterprise-license -n articom-production), Kubernetes garbage collection automatically cleans up all associated Deployments, Services, HPAs, Ingresses, and Secrets.isValid: falseSymptom:
kubectl get license shows isValid: false.
Root Causes & Solutions:
spec.key.https://admin.articom.io/api.
kubectl logs -n articom-system deployment/articom-operator-controller-manager -c manager
admin.articom.io if the license subscription has lapsed.ImagePullBackOff or ErrImagePullSymptom:
Pods fail to pull container images from articomacr.azurecr.io or *.pkg.dev.
Root Causes & Solutions:
kubectl get secret articom-registry-credentials -n articom-production -o jsonpath='{.data.\.dockerconfigjson}' | base64 --decode
kubectl annotate license enterprise-license -n articom-production reconcile.articom.io/force=$(date +%s) --overwrite
Symptom:
502 Bad Gateway or 404 Not Found when accessing service URLs.
Root Causes & Solutions:
ingressClassName matches your cluster's ingress controller:
kubectl get ingressclass
kubectl describe ingress articom-ingress -n articom-production
kubectl get endpoints -n articom-production
Symptom:
articom-voice-livekit-worker pods remain in Pending state.
Reason: The voice worker requires dedicated high-performance nodes with:
nodeSelector:
workload: livekit-worker
tolerations:
- key: "workload"
operator: "Equal"
value: "livekit-worker"
effect: "NoSchedule"
Fix: Ensure your node pool has the corresponding label (workload=livekit-worker) and taint applied, or provision a matching node group.
# Generate WebhookConfiguration, ClusterRole and CRDs from markers
make manifests
# Regenerate DeepCopy code
make generate
# Run unit and integration tests (uses envtest)
make test
# Run code style linter and auto-fix
make lint-fix
# Build container image
export IMG=docker.io/<username>/articom-cx-operator:v1.0.0
make docker-build IMG=$IMG
# Push container image to registry
make docker-push IMG=$IMG
# Build single-file install bundle
make build-installer IMG=$IMG
If expanding to multi-group APIs in the future, follow the steps documented in AGENTS.md.
Copyright © 2026 Inforwaves. Licensed under the Apache License, Version 2.0.
Content type
Image
Digest
sha256:89efda334…
Size
31.2 MB
Last updated
25 days ago
docker pull articomio/articom-cx-operator