Skip to content

Repository files navigation

Off-hours Schedule Operator

offhours-schedule-operator is a Kubernetes controller that reduces selected Deployment replicas outside configured business hours and restores their previous replica counts when business hours resume.

For the design story and the reasoning behind the final architecture, read From a Simple Schedule to a Memory-Conscious Kubernetes Operator.

Each OffHoursSchedule defines:

  • an IANA timezone and business-hours window;
  • the weekdays on which the window applies;
  • the namespaces the controller may inspect;
  • a Kubernetes label selector used to opt Deployments in;
  • the replica count used during off-hours;
  • Helm releases and individual workloads to exclude.

How reconciliation works

For every namespace in spec.namespaces, the controller sends a direct API request for Deployments matching spec.deploymentSelector. It then skips:

  • Deployments labeled offhours/skip: "true";
  • Deployments with a future RFC 3339 offhours/skip-until annotation;
  • Deployments belonging to a Helm release in spec.excludeHelmReleases.

During off-hours, the controller stores the current replica count in the offhours.erickyataco.github.io/previous-replicas annotation and applies spec.replicasOffHours. During business hours, it restores the saved value and removes the annotation.

The controller does not watch or cache Deployments or ReplicaSets. Workload discovery uses an uncached APIReader, scoped by namespace and label selector. Only OffHoursSchedule resources use a controller informer. Kubernetes' built-in Deployment controller manages the resulting ReplicaSets and Pods.

Prerequisites

  • Go 1.25.3 or newer
  • Docker or another compatible container builder
  • kubectl
  • access to a Kubernetes cluster
  • kind, when running the isolated end-to-end tests

Confirm the cluster targeted by your active kubeconfig before running the operator:

kubectl config current-context

Run locally

make install
make run

make install installs the OffHoursSchedule CRD into the active cluster. make run compiles and starts the controller manager as a process on your local machine. Keep that terminal open while testing.

Important: make run does not create an operator Pod. The local process connects to the active Kubernetes cluster using your kubeconfig, so it will not appear in kubectl get pods output.

Pods such as CoreDNS, etcd, kube-apiserver, kube-scheduler, and kube-controller-manager are cluster system components. They are not this operator.

Deploy the operator as a Pod

Build and push an image that the cluster can pull:

export IMG=<registry>/offhours-schedule-operator:<tag>
make docker-build docker-push IMG="$IMG"
make deploy IMG="$IMG"

The deployment installs the CRD, RBAC resources, service account, and controller manager in the offhours-schedule-operator-system namespace.

Verify the operator Pod:

kubectl get pods -n offhours-schedule-operator-system
kubectl logs \
  -n offhours-schedule-operator-system \
  deployment/offhours-schedule-operator-controller-manager \
  -c manager -f

Deploy to kind without a registry

For a kind cluster named offhours, build and load the image locally:

export IMG=offhours-schedule-operator:dev
make docker-build IMG="$IMG"
kind load docker-image "$IMG" --name offhours
make deploy IMG="$IMG"

If the Deployment attempts to pull the local image instead of using the image loaded into kind, set a non-Always image pull policy in the manager manifest.

Create a schedule

The sample schedule manages opted-in Deployments in dev and staging:

kubectl apply -k config/samples

Equivalent resource:

apiVersion: offhours.erickyataco.github.io/v1alpha1
kind: OffHoursSchedule
metadata:
  name: nonprod-offhours
spec:
  timezone: America/Lima
  businessStart: "08:00"
  businessEnd: "18:00"
  businessDays: [Mon, Tue, Wed, Thu, Fri]
  namespaces:
    - dev
    - staging
  deploymentSelector: "offhours/enabled=true"
  scaleDownOffHours: true
  replicasOffHours: 0
  excludeHelmReleases:
    - argocd
    - external-secrets

Inspect reconciliation status with:

kubectl get offhoursschedules
kubectl get offhoursschedule nonprod-offhours -o yaml

Status reports the current mode, the next transition, the observed generation, matched/skipped/failed Deployment counts, and a standard Ready condition.

Test all workload-selection cases

The scenario in config/test-scenarios/all-cases covers:

  • matching Deployments in configured namespaces;
  • a Deployment that does not match the selector;
  • an excluded Helm release;
  • permanent and temporary workload opt-outs;
  • a matching Deployment outside the configured namespaces;
  • off-hours scale-down and business-hours restoration.

Install the CRD and start or deploy the operator first, then run:

kubectl apply -k config/test-scenarios/all-cases

Inspect the scenario:

kubectl get offhoursschedule -n offhours-system all-cases -o yaml
kubectl get deployments -n offhours-test-dev
kubectl get deployments -n offhours-test-staging
kubectl get deployments -n offhours-test-outside

Development

Generate manifests and code after changing API types or Kubebuilder markers:

make manifests generate

Run formatting, linting, and tests:

make lint-fix
make test

Run isolated end-to-end tests with kind:

make test-e2e
make cleanup-test-e2e

Build the manager binary:

make build

Generate a consolidated installation bundle:

make build-installer IMG=<registry>/offhours-schedule-operator:<tag>

Run make help to list all supported targets.

Uninstall

Remove sample and test resources before deleting the CRD:

kubectl delete -k config/samples --ignore-not-found
kubectl delete -k config/test-scenarios/all-cases --ignore-not-found

Remove the in-cluster controller and CRD:

make undeploy
make uninstall

License

Copyright 2026.

Licensed under the Apache License, Version 2.0. See the license text for details.

About

Memory-efficient Kubernetes operator built with Go and Kubebuilder that scales opt-in Deployments down during off-hours and restores them during business hours.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages