Skip to content
This repository was archived by the owner on Oct 3, 2020. It is now read-only.

Commit 0f65896

Browse files
author
Vishal Tak
committed
Add ability to downscale HPA's minReplicas
1 parent 80aee07 commit 0f65896

File tree

5 files changed

+123
-6
lines changed

5 files changed

+123
-6
lines changed

README.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ Kubernetes Downscaler
1818
:target: http://calver.org/
1919
:alt: CalVer
2020

21-
Scale down Kubernetes deployments and/or statefulsets during non-work hours.
21+
Scale down Kubernetes deployments and/or statefulsets and/or horizontalpodautoscalers during non-work hours.
2222

23-
Deployments are interchangeable by statefulset for this whole guide.
23+
Deployments are interchangeable by statefulset/horizontalpodautoscalers for this whole guide unless explicitly stated otherwise.
2424

2525
It will scale down the deployment's replicas if all of the following conditions are met:
2626

@@ -39,6 +39,7 @@ It will scale down the deployment's replicas if all of the following conditions
3939
* there are no active pods that force the whole cluster into uptime (annotation ``downscaler/force-uptime: "true"``)
4040

4141
The deployment by default will be scaled down to zero replicas. This can be configured with a deployment or its namespace's annotation of ``downscaler/downtime-replicas`` (e.g. ``downscaler/downtime-replicas: "1"``) or via CLI with ``--downtime-replicas``.
42+
In case of horizontalpodautoscalers, the `minReplicas` field cannot be set to zero and thus ``downscaler/downtime-replicas`` should be atleast ``1``.
4243

4344
Example use cases:
4445

@@ -81,6 +82,10 @@ The downscaler will eventually log something like:
8182

8283
INFO: Scaling down Deployment default/nginx from 1 to 0 replicas (uptime: Mon-Fri 09:00-17:00 America/Buenos_Aires, downtime: never)
8384

85+
Note that in cases where HPA is used along with Deployments, consider the following:
86+
87+
* If downscale to 0 is desired, annotation is applied at Deployment. (Special case, since minReplicas of 0 at HPA is not allowed. Setting Deployment replica to 0, essentially makes the HPA behave as disabled. In such a case, the HPA will emit events like `` failed to get memory utilization: unable to get metrics for resource memory: no metrics returned from resource metrics API`` as there is no Pod to retrieve metric from.)
88+
* If downscale greater than 0 is desired, annotation is applied at HPA. This allows for dynamic scaling of the Pods even during downtime based upon the external traffic as well as maintain a lower minReplicas during downtime if there is no/low traffic. If the Deployment is annotated instead of the HPA, it leads to a race condition where kube-downscaler downscales the Deployment and HPA upscales it as its minReplica is higher.
8489

8590
Configuration
8691
=============
@@ -134,7 +139,7 @@ Available command line options:
134139
``--namespace``
135140
Restrict the downscaler to work only in a single namespace (default: all namespaces). This is mainly useful for deployment scenarios where the deployer of kube-downscaler only has access to a given namespace (instead of cluster access).
136141
``--include-resources``
137-
Downscale resources of this kind as comma separated list. [deployments, statefulsets, stacks] (default: deployments)
142+
Downscale resources of this kind as comma separated list. [deployments, statefulsets, stacks, horizontalpodautoscalers] (default: deployments)
138143
``--grace-period``
139144
Grace period in seconds for new deployments before scaling them down (default: 15min). The grace period counts from time of creation of the deployment, i.e. updated deployments will immediately be scaled down regardless of the grace period.
140145
``--upscale-period``

helm-chart/templates/clusterrole.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ rules:
3030
- list
3131
- update
3232
- patch
33+
- apiGroups:
34+
- autoscaling
35+
resources:
36+
- horizontalpodautoscalers
37+
verbs:
38+
- get
39+
- watch
40+
- list
41+
- update
42+
- patch
3343
- apiGroups:
3444
- ""
3545
resources:

kube_downscaler/cmd.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import argparse
22
import os
33

4-
VALID_RESOURCES = frozenset(["deployments", "statefulsets", "stacks", "cronjobs"])
4+
VALID_RESOURCES = frozenset(
5+
["deployments", "statefulsets", "stacks", "cronjobs", "horizontalpodautoscalers"]
6+
)
57

68

79
def check_include_resources(value):
@@ -36,7 +38,7 @@ def get_parser():
3638
"--include-resources",
3739
type=check_include_resources,
3840
default="deployments",
39-
help="Downscale resources of this kind as comma separated list. [deployments, statefulsets, stacks] (default: deployments)",
41+
help="Downscale resources of this kind as comma separated list. [deployments, statefulsets, stacks, horizontalpodautoscalers] (default: deployments)",
4042
)
4143
parser.add_argument(
4244
"--grace-period",

kube_downscaler/scaler.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pykube
77
from pykube import CronJob
88
from pykube import Deployment
9+
from pykube import HorizontalPodAutoscaler
910
from pykube import StatefulSet
1011

1112
from kube_downscaler import helper
@@ -22,7 +23,7 @@
2223
DOWNTIME_ANNOTATION = "downscaler/downtime"
2324
DOWNTIME_REPLICAS_ANNOTATION = "downscaler/downtime-replicas"
2425

25-
RESOURCE_CLASSES = [Deployment, StatefulSet, Stack, CronJob]
26+
RESOURCE_CLASSES = [Deployment, StatefulSet, Stack, CronJob, HorizontalPodAutoscaler]
2627

2728
TIMESTAMP_FORMATS = [
2829
"%Y-%m-%dT%H:%M:%SZ",
@@ -200,6 +201,17 @@ def autoscale_resource(
200201
"suspended" if original_replicas == 0 else "not suspended",
201202
uptime,
202203
)
204+
elif resource.kind == "HorizontalPodAutoscaler":
205+
replicas = resource.obj["spec"]["minReplicas"]
206+
logger.debug(
207+
"%s %s/%s has %s minReplicas (original: %s, uptime: %s)",
208+
resource.kind,
209+
resource.namespace,
210+
resource.name,
211+
replicas,
212+
original_replicas,
213+
uptime,
214+
)
203215
else:
204216
replicas = resource.replicas
205217
logger.debug(
@@ -232,6 +244,18 @@ def autoscale_resource(
232244
uptime,
233245
downtime,
234246
)
247+
elif resource.kind == "HorizontalPodAutoscaler":
248+
resource.obj["spec"]["minReplicas"] = int(original_replicas)
249+
logger.info(
250+
"Scaling up %s %s/%s from %s to %s minReplicas (uptime: %s, downtime: %s)",
251+
resource.kind,
252+
resource.namespace,
253+
resource.name,
254+
replicas,
255+
original_replicas,
256+
uptime,
257+
downtime,
258+
)
235259
else:
236260
resource.replicas = int(original_replicas)
237261
logger.info(
@@ -279,6 +303,18 @@ def autoscale_resource(
279303
uptime,
280304
downtime,
281305
)
306+
elif resource.kind == "HorizontalPodAutoscaler":
307+
resource.obj["spec"]["minReplicas"] = target_replicas
308+
logger.info(
309+
"Scaling down %s %s/%s from %s to %s minReplicas (uptime: %s, downtime: %s)",
310+
resource.kind,
311+
resource.namespace,
312+
resource.name,
313+
replicas,
314+
target_replicas,
315+
uptime,
316+
downtime,
317+
)
282318
else:
283319
resource.replicas = target_replicas
284320
logger.info(

tests/test_autoscale_resource.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pykube
88
import pytest
99
from pykube import Deployment
10+
from pykube import HorizontalPodAutoscaler
1011

1112
from kube_downscaler.resources.stack import Stack
1213
from kube_downscaler.scaler import autoscale_resource
@@ -613,3 +614,66 @@ def test_upscale_stack_with_autoscaling():
613614
assert stack.obj["spec"]["replicas"] is None
614615
assert stack.replicas == 4
615616
assert stack.annotations[ORIGINAL_REPLICAS_ANNOTATION] is None
617+
618+
619+
def test_downscale_hpa_with_autoscaling():
620+
hpa = HorizontalPodAutoscaler(
621+
None,
622+
{
623+
"metadata": {
624+
"name": "my-hpa",
625+
"namespace": "my-ns",
626+
"creationTimestamp": "2018-10-23T21:55:00Z",
627+
"annotations": {DOWNTIME_REPLICAS_ANNOTATION: str(1)},
628+
},
629+
"spec": {"minReplicas": 4},
630+
},
631+
)
632+
now = datetime.strptime("2018-10-23T21:56:00Z", "%Y-%m-%dT%H:%M:%SZ").replace(
633+
tzinfo=timezone.utc
634+
)
635+
autoscale_resource(
636+
hpa,
637+
upscale_period="never",
638+
downscale_period="never",
639+
default_uptime="never",
640+
default_downtime="always",
641+
forced_uptime=False,
642+
dry_run=True,
643+
now=now,
644+
)
645+
assert hpa.obj["spec"]["minReplicas"] == 1
646+
assert hpa.obj["metadata"]["annotations"][ORIGINAL_REPLICAS_ANNOTATION] == str(4)
647+
648+
649+
def test_upscale_hpa_with_autoscaling():
650+
hpa = HorizontalPodAutoscaler(
651+
None,
652+
{
653+
"metadata": {
654+
"name": "my-hpa",
655+
"namespace": "my-ns",
656+
"creationTimestamp": "2018-10-23T21:55:00Z",
657+
"annotations": {
658+
DOWNTIME_REPLICAS_ANNOTATION: str(1),
659+
ORIGINAL_REPLICAS_ANNOTATION: str(4),
660+
},
661+
},
662+
"spec": {"minReplicas": 1},
663+
},
664+
)
665+
now = datetime.strptime("2018-10-23T22:15:00Z", "%Y-%m-%dT%H:%M:%SZ").replace(
666+
tzinfo=timezone.utc
667+
)
668+
autoscale_resource(
669+
hpa,
670+
upscale_period="never",
671+
downscale_period="never",
672+
default_uptime="always",
673+
default_downtime="never",
674+
forced_uptime=False,
675+
dry_run=True,
676+
now=now,
677+
)
678+
assert hpa.obj["spec"]["minReplicas"] == 4
679+
assert hpa.obj["metadata"]["annotations"][ORIGINAL_REPLICAS_ANNOTATION] is None

0 commit comments

Comments
 (0)