AWS

AWS EKS 비용 절감하기: kube-green SleepInfo와 KEDA CronJob으로 업무 시간 외 파드 종료

황동리 2026. 7. 8. 17:13
반응형

개요

현재 스타트업에 재직 중 이다보니, 비용 절감에 큰 중점을 두고 있는데요.


개발 환경에서는 어플리케이션 워크로드가 동작하는 노드는 karpenternodepool 을 사용하고 있어서 Replica 개수가 0이 되면 Worker 노드가 사라지게 되어있습니다.


이에 따라 업무 시간 외에는 어플리케이션 워크로드의 Replica 개수를 0으로 만들어 노드 비용을 절감하려고 하였습니다.

초기 구성

처음에는 kube-green Helm 차트를 설치하여 sleepinfo 리소스를 사용해서 업무 시간에만 어플리케이션 워크로드를 동작 시키고


업무 시간 외에는 어플리케이션 워크로드을 꺼두려고 하였습니다.

ex)
apiVersion: kube-green.com/v1alpha1
kind: SleepInfo
metadata:
  name: sleepinfo
  namespace: <어플리케이션 파드들이 있는 네임스페이스>
spec:
  weekdays: "1-5"
  wakeUpAt: "09:30" # 출근 30분 전에 파드 깨우기
  sleepAt: "19:30" # 퇴근 30분 후 파드 종료
  timeZone: "Asia/Seoul"
  suspendDeployments: true
  suspendStatefulSets: false
  suspendCronJobs: false
---
apiVersion: kube-green.com/v1alpha1
kind: SleepInfo
metadata:
  name: sleepinfo-rollout
  namespace: <어플리케이션 파드들이 있는 네임스페이스>
spec:
  weekdays: "1-5"
  wakeUpAt: "09:30" # 출근 30분 전에 파드 깨우기
  sleepAt: "19:30" # 퇴근 30분 후 파드 종료
  timeZone: "Asia/Seoul"
  suspendDeployments: false
  suspendStatefulSets: false
  suspendCronJobs: false
  includeRef: # rollout 리소스는 아래와 같이 따로 커스텀이 필요함
    - apiVersion: argoproj.io/v1alpha1
      kind: Rollout
      name: api
  patches:
    - target:
        group: argoproj.io
        kind: Rollout
      patch: |-
        - op: add
          path: /spec/replicas
          value: 0

첫번째 문제 상황

sleepinfo는 정의한 리소스들의 Replica 개수를 0으로 만들기 전 상태를 Secret에 저장해두고
Wake 시간에 맞춰 Sleep 시키기 이전 Replica 개수로 patch 하는 방식으로 동작을 합니다.


문제는, ArgoCD를 사용해서 어플리케이션 워크로드를 배포 했는데,


live state랑 git state랑 Replica 개수가 일치하지 않아서 Self Heal 옵션이 git에 저장된 Replica 개수로 돌리는 문제가 발생을 하였습니다.

첫번째 문제 해결 방안

그래서 개발 환경이기도 하다보니 ArgoCD Application의 spec.replicas 의 차이를 무시하도록 아래와 같이 설정했습니다.

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas

    - group: argoproj.io
      kind: Rollout
      jsonPointers:
        - /spec/replicas

이렇게 설정을 해두면 ArgoCD가 Replica 개수 차이를 OutOfSync로 판단하지 않게 되어 파드를 살리지 않게 됩니다.


따라서 정상적으로 Deployment, Rollout 리소스들은 업무시간에 동작하고 업무 시간 외에는 꺼지게 되었습니다.


그러나 두번째 문제가 발생을 했습니다.

두번째 문제 상황

오토스케일링이 필요한 워크로드들이 있어서 Keda의 ScaledObject 리소스를 사용하고 있었습니다.


ScaledObject 리소스에서 minReplica 설정을 사용하다보니 sleepinfo로 Replica 개수를 0으로 만들어버려도


ScaledObject에서 정의한 minReplica 개수로 파드를 다시 띄워버리니, sleepinfo 설정이 정상적으로 동작이 되질 않았습니다.

두번째 문제 해결 방안

따라서 Cronjob 리소스를 사용해서 업무 시간 외에는


ScaledObject 를 Pause 시키고 paused-replica 개수도 0 개로 만들어주는 방식으로 해결하였습니다.


Cronjob 리소스는 아래와 같이 작성을 하였습니다.


주된 동작은 scaledobject 설정을 patch 해서 변경하도록 하였습니다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: keda-sleep
  namespace: kube-green
spec:
  schedule: "30 19 * * 1-5"
  timeZone: "Asia/Seoul"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: after-hours-override
          restartPolicy: Never
          containers:
            - name: pause-api-autoscale
              image: rancher/kubectl:v1.30.3
              command: ["kubectl"]
              args:
                - -n
                - <api 리소스가 존재하는 네임스페이스>
                - patch
                - scaledobject
                - <api 리소스가 사용하는 scaledobject 이름>
                - --type
                - merge
                - -p
                - '{"metadata":{"annotations":{"autoscaling.keda.sh/paused":"true","autoscaling.keda.sh/paused-replicas":"0"}}}'
            - name: scale-api-to-zero
              image: rancher/kubectl:v1.30.3
              command: ["kubectl"]
              args:
                - -n
                - <api 리소스가 존재하는 네임스페이스>
                - patch
                - <api 리소스의 타입, ex: deployment, rollout>
                - api
                - --type
                - merge
                - -p
                - '{"spec":{"replicas":0}}'
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: keda-wake
  namespace: kube-green
spec:
  schedule: "30 09 * * 1-5"
  timeZone: "Asia/Seoul"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: after-hours-override
          restartPolicy: Never
          containers:
            - name: scale-api-to-one
              image: rancher/kubectl:v1.30.3
              command: ["kubectl"]
              args:
                - -n
                - <api 리소스가 존재하는 네임스페이스>
                - patch
                - <api 리소스의 타입, ex: deployment, rollout>
                - api
                - --type
                - merge
                - -p
                - '{"spec":{"replicas":1}}'
            - name: resume-api-autoscale
              image: rancher/kubectl:v1.30.3
              command: ["kubectl"]
              args:
                - -n
                - <api 리소스가 존재하는 네임스페이스>
                - patch
                - scaledobject
                - <api 리소스가 사용하는 scaledobject 이름>
                - --type
                - merge
                - -p
                - '{"metadata":{"annotations":{"autoscaling.keda.sh/paused":null,"autoscaling.keda.sh/paused-replicas":null}}}'

이렇게 19시 30분이 되면 ScaledObject를 pause 시키고 paused-replica 개수도 0으로 만들어서 Replica 개수를 모두 0으로 만들고,


아침 9시 30분이 되면 다시 ScaledObject pause를 풀어줍니다.


이때, patch 동작을 하기 위해 ServiceAccount의 role, rolebinding도 아래와 같이 해줍니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: after-hours-override
  namespace: kube-green
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: after-hours-override
  namespace: <api 리소스가 존재하는 네임스페이스>
rules:
  - apiGroups: ["keda.sh"]
    resources: ["scaledobjects"]
    verbs: ["get", "list", "patch", "update"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "patch", "update"]
  - apiGroups: ["argoproj.io"]
    resources: ["rollouts"]
    verbs: ["get", "list", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: after-hours-override
  namespace: <api 리소스가 존재하는 네임스페이스>
subjects:
  - kind: ServiceAccount
    name: after-hours-override
    namespace: kube-green
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: after-hours-override

이렇게 해주면 업무시간에는 Workload들이 정상적으로 동작을 하게 되지만,


업무 시간 외에는 동작을 하지 않으므로 비용이 절감되게 됩니다.



이상 입니다.


감사합니다.

반응형