개요
현재 스타트업에 재직 중 이다보니, 비용 절감에 큰 중점을 두고 있는데요.
개발 환경에서는 어플리케이션 워크로드가 동작하는 노드는 karpenter 의 nodepool 을 사용하고 있어서 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들이 정상적으로 동작을 하게 되지만,
업무 시간 외에는 동작을 하지 않으므로 비용이 절감되게 됩니다.
이상 입니다.
감사합니다.
'AWS' 카테고리의 다른 글
| EKS 내부 서비스들을 하나의 Internal NLB로 통합하여 비용 개선 (ECS 연동 개선기) (0) | 2026.02.12 |
|---|---|
| AWS NAT Gateway 비용 분석: S3 트래픽과 VPC Endpoint의 중요성 (0) | 2025.12.16 |
| AWS Lambda Layer란? Node.js 기반으로 직접 만들어보고 Lambda에서 적용하기 (1) | 2025.11.13 |
| Packer 사용해서 AWS ami 커스텀 해보기 (0) | 2025.09.30 |
| Karpenter로 EKS에서 Spot 인스턴스 자동 프로비저닝하기 (0) | 2025.09.03 |