A lightweight Kubernetes controller that replicates ConfigMaps and Secrets across namespaces. Annotate a source object and Replikate copies it into every matching namespace — keeping copies in sync on change, cleaning them up on delete, and populating new namespaces automatically.
replikate.ubixsys.com/sync: "<selector>" "team=web" # → namespaces matching the label "" # → every namespace
Source changed → every copy is updated. Source or annotation removed → copies are deleted via a finalizer. A new namespace appears that matches → it is populated automatically.
Replikate never overwrites an object it does not manage, and only writes when something actually changed. It's annotation-compatible with AppsCode's config-syncer, and adopts existing copies in place during a cutover rather than deleting and recreating them.
# install into the cluster (image built by CI) $ make deploy IMAGE_REPO=ghcr.io/ubixsys/replikate \ IMAGE_TAG=v1.4.0
apiVersion: v1
kind: ConfigMap
metadata:
name: shared-config
annotations:
replikate.ubixsys.com/sync: "team=web"
data:
LOG_LEVEL: "info" # the namespace gets a copy near-instantly $ kubectl label namespace some-ns team=web $ kubectl get configmap shared-config -n some-ns
# on the hub: turn on cross-cluster, register each spoke $ helm upgrade --install replikate charts/replikate \ -n replikate-system --set crossCluster.enabled=true $ kubectl -n replikate-system create secret generic green \ --from-file=kubeconfig=green.kubeconfig $ kubectl -n replikate-system label secret green \ replikate.ubixsys.com/cluster-credential=true # then on a source: repeat a cluster for several namespaces (v1.4+) replikate.ubixsys.com/target-clusters: "green:web,green:live,blue:web,blue:live"
Controller flags and the matching Helm values — sensible defaults, tune what you need.
| Flag | Default | Notes |
|---|---|---|
| -annotation-domain | replikate.ubixsys.com | Primary prefix the controller writes and watches. |
| -previous-annotation-domain | replikate.brainchurts.com | Prior prefix still honored for reads / matching / adoption; copies self-migrate. Set empty to finish the migration. |
| -exclude-namespaces | kube-system,kube-public,kube-node-lease | Never replicate copies into these namespaces. |
| -leader-elect | false | Elect a single active replica (the Helm chart turns this on). |
| -metrics-bind-address | :8080 | Prometheus metrics endpoint. |
| -health-probe-bind-address | :8081 | Liveness / readiness probes. |
| -enable-webhook | false | Optional validating admission webhook. |
| -enable-cross-cluster | false | Opt-in cross-cluster replication (discover spokes from credential Secrets). |
| -cluster-credential-namespace | $POD_NAMESPACE | Where spoke credential Secrets live. |
| Value | Default | Notes |
|---|---|---|
| annotationDomain | replikate.ubixsys.com | Wires the flag above (the primary prefix). |
| previousAnnotationDomain | replikate.brainchurts.com | Non-breaking domain migration — the old prefix stays honored until you set it empty. |
| excludeNamespaces | kube-system, kube-public, kube-node-lease | Namespace skip-list. |
| leaderElection.enabled | true | HA-safe; only one replica reconciles at a time. |
| rbac.create | true | ClusterRole/Binding for ConfigMap + Secret access. |
| resources (req → lim) | 50m/64Mi → 200m/128Mi | Lightweight footprint out of the box. |
| metrics.serviceMonitor.enabled | false | Scrape via the Prometheus Operator. |
| webhook.enabled | false | Validating webhook (port 9443, failurePolicy Fail). |
| crossCluster.enabled | false | Make this install a cross-cluster hub (chart 1.3.1+). |
| crossCluster.credentialNamespace | release namespace | Where spoke credential Secrets live. |
| podDisruptionBudget.enabled | false | Keep at least one replica during disruptions. |
Watch the sources and the namespaces; make the copies match; only write when something changed.
Replikate is a single controller built on controller-runtime. It watches two things — annotated source objects and the set of namespaces — and on any change reconciles the desired state: a managed copy of each source in every namespace its selector matches. It only writes when something actually differs, so the logs stay meaningful.
Ownership is explicit. Every copy is stamped managed-by=replikate, and the controller refuses to touch an object it doesn't own — emitting a Conflict event instead of fighting another source. A finalizer guarantees copies are removed when the source or annotation goes away, and the namespace watch means a new matching namespace is populated automatically.
It runs leader-elected, so multiple replicas are HA-safe with exactly one active reconciler. Cross-cluster (hub-and-spoke) replication is opt-in: set crossCluster.enabled, register each spoke as a labeled credential Secret (apply deploy/spoke-rbac.yaml on the spoke to mint it), and add a target-clusters annotation. Each entry places one copy: a bare cluster uses the source's namespace, cluster:namespace names one, and as of v1.4 a cluster can be repeated to reach several namespaces (green:web,green:live); dropping an entry prunes its copy. For blue/green, a separate infrastructure cluster makes the best hub, so neither colour depends on the other. A down spoke never blocks the others or the local path. Remote copies are stamped with the hub's cluster UID, so two hubs can share a spoke. Deleting the source removes its remote copies too, so deregister a spoke before retiring its hub. Remote copies re-sync on source change and on a ~2-minute recheck; instant remote drift repair and selector fan-out on spokes are next on the roadmap. The sync selector still applies on the hub itself, so give hub-only sources a selector that matches none of its namespaces.
As of v1.3 the annotation prefix moved to replikate.ubixsys.com, but the change is non-breaking: the previous replikate.brainchurts.com prefix is still honored for reads, matching, and adoption, and every reconcile self-migrates a copy to the new prefix in place — no coordinated re-annotation, and live secrets are never deleted and recreated. A replikate_copies_by_domain gauge shows the migration finishing; set previousAnnotationDomain empty once it reaches zero.