A Kubernetes operator that manages fleets of preemptible pods on a time-based schedule.
Flexpod helps manage a fleet of preemptible pods to prepare for steep upscales on Kubernetes clusters. Spawning preemptible pods pre-warms nodes so they are ready when real workloads arrive, but running them continuously has a cost. For predictable workloads with a seasonal pattern (daily peaks, weekly business hours, etc.) Flexpod creates the preemptible pods in advance, before the predicted high-scaling window, and deletes them when the window closes.
Users define one or more PreemptiblePodSchedule resources. Each resource targets a specific set of nodes (via affinity, node selectors, or topology spread constraints), so different node pools can have independent schedules.
Apply all resources in order:
# CRD, PriorityClass, namespace, ServiceAccount
kubectl apply -f config/crd/bases/
kubectl apply -f config/manager/priorityclass.yaml
kubectl apply -f config/manager/namespace.yaml
kubectl apply -f config/manager/serviceaccount.yaml
# RBAC
kubectl apply -f config/rbac/role.yaml
kubectl apply -f config/rbac/role_binding.yaml
# Controller Deployment
kubectl apply -f config/manager/manager.yamlThe config/manager/manager.yaml references the image rg.fr-par.scw.cloud/flexpod/manager:v0.3.0 and expects an imagePullSecret named flexpod-registry in the flexpod-system namespace. Adjust for your own registry as needed.
go run ./cmd/manager/main.go \
--metrics-bind-address=:8080 \
--health-probe-bind-address=:8081 \
--leader-elect=falseThe manager uses the current kubeconfig context. Apply the CRD and PriorityClass first:
kubectl apply -f config/crd/bases/
kubectl apply -f config/manager/priorityclass.yaml- API group/version:
flexpod.nlm.github.io/v1alpha1 - Kind:
PreemptiblePodSchedule - Short name:
pps - Scope: Namespaced
apiVersion: flexpod.nlm.github.io/v1alpha1
kind: PreemptiblePodSchedule
metadata:
name: business-hours-prewarm
namespace: production
spec:
schedule:
# Standard 5-field cron expression (minute hour dom month dow).
# This fires at 08:00 every weekday.
cron: "0 8 * * 1-5"
# How long the window stays open after each cron fire.
# Accepts Go duration strings: ns, us, ms, s, m, h.
# This window runs 08:00–17:00 (9 hours).
duration: "9h"
# Optional IANA timezone name. Defaults to UTC when omitted.
timeZone: "America/New_York"
# Number of preemptible pods to maintain while the window is open.
# Defaults to 1. Set to 0 to keep the schedule without creating pods.
replicas: 3
template:
# Container image. Defaults to registry.k8s.io/pause:3.10.
image: "registry.k8s.io/pause:3.10"
# Standard Kubernetes resource requests and limits.
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
memory: "64Mi"
# Target specific nodes by label.
nodeSelector:
node-pool: "preemptible"
# Full Kubernetes Affinity spec — node affinity, pod affinity, anti-affinity.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: cloud.google.com/gke-spot
operator: In
values:
- "true"
# Allow pods to schedule onto tainted (e.g. spot/preemptible) nodes.
tolerations:
- key: "cloud.google.com/gke-spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
# Priority class for the pods. When omitted, defaults to "flexpod-preemptible"
# (value: -10, preemptionPolicy: Never), which is installed by
# config/manager/priorityclass.yaml. Override to use a different class.
priorityClassName: "low-priority-prewarm"
# Spread pods across failure domains to pre-warm multiple zones.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
flexpod.nlm.github.io/schedule: business-hours-prewarmEach PreemptiblePodSchedule defines a repeating window: the window opens at each cron fire time and closes duration later. For example, cron: "0 8 * * 1-5" with duration: "9h" opens a window at 08:00 and closes it at 17:00 on every weekday.
When a window opens, the controller creates spec.replicas pods in the same namespace as the CR. The pods are labeled flexpod.nlm.github.io/schedule=<name> and owned by the CR via an owner reference, so they are garbage-collected automatically when the CR is deleted. When the window closes, the controller deletes those pods, preferring Pending pods and older pods when scaling down.
The controller requeues itself at each window boundary, so transitions happen promptly without relying on watch events.
When spec.template.priorityClassName is not set, pods are created with flexpod-preemptible (value: -10, preemptionPolicy: Never). This means they will be evicted under resource pressure but will not preempt other pods.
After the first reconcile, kubectl get pps <name> -o yaml shows a populated .status:
| Field | Type | Description |
|---|---|---|
desiredActive |
bool | true when the current time falls inside an open window |
currentReplicas |
int32 | Number of active (non-terminating) pods currently owned by this CR |
currentWindowStart |
time | Start time of the currently open window; absent when no window is open |
currentWindowEnd |
time | End time of the currently open window; absent when no window is open |
nextWindowOpen |
time | Time of the next cron fire (next window start) |
nextWindowClose |
time | End of the next window (nextWindowOpen + duration) |
conditions |
[]Condition | See below |
Conditions:
| Type | Meaning |
|---|---|
ScheduleValid |
True when spec.schedule.cron, duration, and timeZone all parse successfully. False with a reason of InvalidCron, InvalidDuration, or InvalidTimeZone on parse failure — all owned pods are deleted when this is False. |
WindowOpen |
True when the current time is inside an active window (InWindow), False otherwise (OutOfWindow). |
PodsReady |
True when currentReplicas equals the desired replica count (Ready), False while converging (Converging). |
Quick status check:
kubectl get pps -n production
# NAME ACTIVE REPLICAS NEXT OPEN AGE
# business-hours-prewarm true 3 2026-06-22T12:00:00Z 2d
kubectl get pps business-hours-prewarm -n production -o jsonpath='{.status.conditions}'- DST transitions: when
spec.schedule.timeZoneis set to a zone that observes daylight saving time, window boundaries follow the robfig/cron library's wall-clock semantics. A spring-forward transition can shorten a window; a fall-back transition can cause the cron to fire twice within the same wall-clock hour. This is expected behavior in v1alpha1. - replicas: 0: valid. The schedule is tracked and status is updated, but no pods are created during open windows. Use this to temporarily disable a schedule without deleting the CR.