Goldilocks VPA 추천값을 Argo CD Pull Request Generator로 자동화하는 GitOps 파이프라인 구축
Kubernetes 클러스터를 운영하다 보면 어느 순간 이런 상황이 찾아옵니다. 분명히 잘 돌아가는 것 같은데, 매달 나오는 클라우드 비용 청구서를 보면 뭔가 과도하게 나온다는 느낌이 드는 거죠. CPU requests는 실제 사용량의 5배, 메모리는 3배로 잡아뒀고, 그게 수십 개 서비스에 걸쳐 복리처럼 쌓여 있는 상태. 저도 처음엔 "일단 넉넉하게 잡아두면 안전하지 않나?" 싶었는데, 이게 노드 자동 프로비저너인 Karpenter 비용까지 직격하더라고요. Karpenter는 파드의 requests 합산값을 보고 노드 크기를 결정하기 때문에, requests가 뻥튀기되어 있으면 실제로 필요한 것보다 훨씬 큰 노드를 잡게 됩니다. 실제로 수십 개 서비스를 점검해보니 절반 이상에서 20% 넘는 과다 할당이 확인됐어요.
그래서 Goldilocks를 도입해 VPA 추천값을 수집하기 시작했는데, 또 다른 문제가 생겼습니다. 추천값은 잘 나오는데, 그걸 매번 사람이 보고 Helm values.yaml에 반영하는 게 현실적으로 안 되는 거예요. 서비스가 수십 개면 이미 반자동화도 아닌 거고. 결국 필요한 건 추천값을 읽어서 자동으로 Git PR을 만들고, Argo CD가 머지 후 클러스터에 반영하는 end-to-end 파이프라인이었습니다.
이 가이드를 따라가면 매주 수동으로 values.yaml을 수정하던 작업이 완전히 사라집니다. Goldilocks VPA 추천값을 CronJob으로 추출해 GitHub PR로 자동 생성하고, Argo CD ApplicationSet Pull Request Generator로 preview 환경까지 구성하는 전체 파이프라인을 단계별로 살펴볼게요. 이 글은 Kubernetes와 Helm을 사용 중이며, Argo CD를 도입한 팀을 대상으로 합니다.
핵심 개념
Goldilocks가 하는 일: VPA를 "안전하게" 쓰는 방법
VPA(Vertical Pod Autoscaler)를 그냥 활성화하면 추천값이 생기는 순간 파드를 재시작해버립니다. 프로덕션 환경에서 이건 꽤 위험한 동작이에요. Goldilocks는 이 문제를 updateMode: "Off"로 우회합니다. VPA 객체를 생성하되 실제로 파드에 적용하지는 않고, 추천값만 .status.recommendation에 쌓아두는 거죠.
한 가지 전제 조건이 있는데, 저도 처음에 이걸 빠뜨려서 "왜 추천값이 안 나오지?" 하고 한참 헤맸어요. VPA Recommender는 Metrics Server(또는 Prometheus Adapter)가 설치되어 있어야 동작합니다. 클러스터에 kubectl top pods가 정상 작동하는지 먼저 확인해보시면 좋습니다.
# Goldilocks가 자동 생성하는 VPA 객체 예시
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-api-server
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-api-server
updatePolicy:
updateMode: "Off" # 핵심: 재시작 없이 추천만
status:
recommendation:
containerRecommendations:
- containerName: app
target:
cpu: "250m"
memory: "512Mi"
lowerBound:
cpu: "100m"
memory: "256Mi"
upperBound:
cpu: "500m"
memory: "1Gi"추천값에는 target, lowerBound, upperBound 세 가지가 있는데, 어떤 값을 쓸지가 의외로 중요한 선택입니다. target은 평균적인 사용량 기반이고, upperBound는 피크 트래픽을 반영한 상한선이에요. 배치 작업이 많거나 트래픽 스파이크가 잦은 서비스라면 target * 1.2 수준의 안전 마진을 두거나 upperBound를 참고하는 게 더 안전합니다.
사용법은 간단합니다. 모니터링하고 싶은 네임스페이스에 레이블 하나만 붙여주면 Goldilocks Controller가 해당 네임스페이스의 Deployment를 감지해 VPA 객체를 자동으로 만들어줍니다.
kubectl label namespace production goldilocks.fairwinds.com/enabled=trueVPA 추천값 수렴 시간: VPA가 신뢰할 수 있는 추천값을 제공하려면 최소 7~14일의 실제 트래픽 데이터가 필요합니다. 배포 직후 바로 추천값을 뽑으면 의미 없는 숫자가 나올 수 있어요.
GitOps PR 파이프라인의 전체 흐름
핵심 아이디어는 간단합니다. 추천값을 사람이 복사하는 대신, 스크립트가 VPA 객체를 폴링해서 Helm values.yaml을 업데이트하고 PR을 제출하는 거예요. Argo CD는 그 PR이 머지되면 알아서 클러스터에 Sync합니다.
Goldilocks Controller
↓ (네임스페이스 감시 → VPA 객체 생성)
VPA Recommender
↓ (실제 사용 패턴 기반 추천값 축적)
Kubernetes CronJob
↓ (kubectl/API로 추천값 추출)
GitHub PR 자동 생성
↓ (values.yaml 수정 + PR 제출)
Argo CD ApplicationSet
↓ (PR Generator → Preview 환경 자동 배포)
코드 리뷰 & 머지
↓
Argo CD Sync → 클러스터 적용
↓
Prometheus + Grafana (비용/성능 모니터링)각 구성 요소의 역할을 정리하면 이렇습니다.
| 구성 요소 | 역할 |
|---|---|
| Goldilocks Controller | 네임스페이스 감시 → VPA 객체 자동 생성 |
| VPA Recommender | 실제 사용 패턴 분석 → .status.recommendation 축적 |
| CronJob + 스크립트 | VPA 추천값 추출 → values.yaml 업데이트 → PR 생성 |
| Argo CD ApplicationSet | PR Generator로 preview 환경 자동 배포 |
| Argo CD | 머지된 PR을 클러스터에 Sync |
실전 적용
Step 1: CronJob으로 자동 PR 생성
가장 진입 장벽이 낮은 패턴입니다. 클러스터 안에 CronJob을 하나 띄워서, 매주 VPA 추천값을 읽어 PR을 생성하는 방식이에요. 별도의 상용 툴 없이 kubectl, jq, yq, gh CLI만 있으면 됩니다.
먼저 GitHub Token을 Secret으로 등록해야 합니다. Token은 contents: write와 pull-requests: write 권한이 필요하고, fine-grained PAT를 권장합니다.
kubectl create secret generic github-token \
--from-literal=token=<YOUR_FINE_GRAINED_PAT> \
-n goldilocks그 다음, CronJob이 VPA 객체를 읽을 수 있도록 ServiceAccount와 RBAC을 설정해줍니다. CronJob이 API 서버에서 VPA 객체를 읽으려면 별도의 접근 권한이 필요하기 때문이에요.
# vpa-reader-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: vpa-reader
namespace: goldilocks
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: vpa-reader
rules:
- apiGroups: ["autoscaling.k8s.io"]
resources: ["verticalpodautoscalers"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vpa-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: vpa-reader
subjects:
- kind: ServiceAccount
name: vpa-reader
namespace: goldilocks그 다음이 핵심인 CronJob입니다. 매주 월요일 오전 9시에 실행되어 추천값을 추출하고 PR 생성 스크립트를 호출합니다. bitnami/kubectl:latest는 jq, yq, gh가 기본 포함되어 있지 않기 때문에, 실제로는 이 세 도구를 포함한 커스텀 이미지를 별도로 빌드하는 것을 권장합니다. 아래 예시에서는 개념 전달을 위해 이미지 이름을 my-registry/pr-bot:1.0.0으로 표기했어요.
# goldilocks-pr-bot-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: goldilocks-pr-bot
namespace: goldilocks
spec:
schedule: "0 9 * * 1" # 매주 월요일 오전 9시
jobTemplate:
spec:
template:
spec:
serviceAccountName: vpa-reader
containers:
- name: pr-bot
image: my-registry/pr-bot:1.0.0 # kubectl + jq + yq + gh 포함 커스텀 이미지
env:
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: github-token
key: token
- name: DELTA_THRESHOLD
value: "0.2" # 20% 이상 차이날 때만 PR 생성
command:
- /bin/sh
- -c
- |
# 1. VPA 추천값 추출 (사이드카 컨테이너 제외)
kubectl get vpa -A -o json | jq -r '
.items[] |
.metadata.namespace + "/" + .metadata.name + " " +
(.status.recommendation.containerRecommendations[]? |
select(.containerName != "istio-proxy") |
select(.containerName != "linkerd-proxy") |
.containerName + " cpu=" + .target.cpu +
" mem=" + .target.memory)
' > /tmp/recommendations.txt
# 2. PR 생성 스크립트 호출
/scripts/create-pr.sh /tmp/recommendations.txt
restartPolicy: OnFailurePR 생성 스크립트에서 가장 중요한 부분은 delta 필터링입니다. 솔직히 이 부분을 처음에 빠뜨렸다가 매주 수십 개의 PR이 쏟아지는 상황을 경험했어요. 현재값 대비 변화가 20% 미만이면 그냥 넘어가도록 해야 PR 노이즈를 잡을 수 있습니다.
또 하나 실수하기 쉬운 부분은 git clone 디렉토리 처리예요. CronJob이 재실행될 때 이전 실행에서 디렉토리가 남아있으면 clone이 실패하거든요. 아래 스크립트에서는 이 부분을 rm -rf로 명시적으로 처리하고, 전체적으로 set -euo pipefail을 걸어 오류 발생 시 즉시 실패하도록 했습니다.
#!/bin/bash
# create-pr.sh
set -euo pipefail
BRANCH="chore/resource-update-$(date +%Y%m%d)"
REPO_DIR="/workspace/k8s-manifests"
THRESHOLD=${DELTA_THRESHOLD:-0.2}
# Git 설정
git config --global user.email "bot@example.com"
git config --global user.name "Goldilocks Bot"
# 이전 실행에서 남은 디렉토리 정리
rm -rf "$REPO_DIR"
git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/my-org/k8s-manifests" "$REPO_DIR" \
|| { echo "ERROR: git clone failed"; exit 1; }
cd "$REPO_DIR"
git checkout -b "$BRANCH"
CHANGED=false
while IFS=' ' read -r ns_name container cpu_rec mem_rec; do
NAMESPACE=$(echo "$ns_name" | cut -d/ -f1)
APP=$(echo "$ns_name" | cut -d/ -f2)
VALUES_FILE="helm/${APP}/values.yaml"
[ ! -f "$VALUES_FILE" ] && continue
# 현재값과 추천값 비교 (delta 필터링)
CURRENT_CPU=$(yq e ".resources.requests.cpu" "$VALUES_FILE") \
|| { echo "WARN: yq parse failed for $VALUES_FILE, skipping"; continue; }
CPU_VAL=$(echo "$cpu_rec" | sed 's/cpu=//')
# 변화폭이 임계값 이상일 때만 업데이트 (target * 1.2 안전 마진 적용)
if python3 -c "
import sys
def parse_cpu(v):
if v.endswith('m'): return float(v[:-1])
return float(v) * 1000
cur = parse_cpu('$CURRENT_CPU')
rec = parse_cpu('$CPU_VAL') * 1.2 # 20% 안전 마진
diff = abs(cur - rec) / cur if cur > 0 else 1
sys.exit(0 if diff >= $THRESHOLD else 1)
"; then
MEM_VAL=$(echo "$mem_rec" | sed 's/mem=//')
# 안전 마진 적용된 값으로 업데이트
SAFE_CPU=$(python3 -c "
def parse_cpu(v):
if v.endswith('m'): return float(v[:-1])
return float(v) * 1000
print(str(int(parse_cpu('$CPU_VAL') * 1.2)) + 'm')
")
yq e ".resources.requests.cpu = \"${SAFE_CPU}\"" -i "$VALUES_FILE"
yq e ".resources.requests.memory = \"${MEM_VAL}\"" -i "$VALUES_FILE"
CHANGED=true
echo "Updated: $APP (cpu: $CURRENT_CPU → $SAFE_CPU)"
fi
done < "$1"
# 변경 사항이 있을 때만 PR 생성
if [ "$CHANGED" = true ]; then
git add -A
git commit -m "chore: update resource requests from Goldilocks recommendations $(date +%Y-%m-%d)"
git push origin "$BRANCH" \
|| { echo "ERROR: git push failed"; exit 1; }
gh pr create \
--title "chore: Goldilocks resource right-sizing $(date +%Y-%m-%d)" \
--body-file /scripts/pr-template.md \
--label "resource-optimization" \
--label "automated"
fiPR 본문 템플릿도 미리 준비해두면 리뷰어 입장에서 훨씬 편합니다. /scripts/pr-template.md 파일은 이런 형식으로 만들어두시면 좋아요.
## Goldilocks Resource Right-sizing
이 PR은 VPA 추천값 기반으로 자동 생성되었습니다.
### 변경 내용
| 서비스 | 변경 전 CPU | 변경 후 CPU | 변경 전 Memory | 변경 후 Memory |
|--------|------------|------------|---------------|---------------|
| (스크립트가 채워넣는 영역) | | | | |
### 예상 비용 절감
- 월 예상 절감: $XXX (Karpenter 노드 크기 감소 포함)
### 검증 방법
- [ ] Preview 환경 정상 배포 확인
- [ ] 파드 재시작 후 CrashLoopBackOff 없음 확인
- [ ] 주요 메트릭 (응답시간, 에러율) 이상 없음 확인| 코드 포인트 | 설명 |
|---|---|
select(.containerName != "istio-proxy") |
사이드카 컨테이너 추천값 필터링 |
DELTA_THRESHOLD=0.2 |
20% 미만 변화는 PR 생성 안 함 (노이즈 방지) |
target * 1.2 안전 마진 |
평균 기반 추천값에 스파이크 여유분 추가 |
set -euo pipefail |
오류 발생 시 즉시 실패, 잘못된 상태로 계속 실행 방지 |
rm -rf "$REPO_DIR" |
CronJob 재실행 시 이전 clone 디렉토리 충돌 방지 |
$CHANGED = true 체크 |
변경 없으면 빈 PR 생성 방지 |
Step 2: Argo CD ApplicationSet으로 Preview 환경 추가
PR이 자동 생성되는 건 좋은데, "이 변경 사항이 실제로 안전한가?"를 검증하는 과정이 없으면 불안하죠. 직접 겪었을 때 가장 당황했던 케이스는 추천값이 과소 설정된 상태로 머지됐을 때였어요. ApplicationSet의 Pull Request Generator를 쓰면 PR이 열리는 순간 preview 네임스페이스에 변경 사항을 자동 배포해 머지 전에 검증할 수 있습니다.
한 가지 주의할 점이 있는데, {{branch}} 값을 네임스페이스에 그대로 사용하면 두 가지 문제가 생깁니다. 첫째로 브랜치명에 슬래시(/)가 포함되어 있으면 Kubernetes 네임스페이스로 사용할 수 없고, 둘째로 63자 길이 제한에 걸릴 수 있어요. 아래처럼 브랜치명을 정제해서 사용하는 게 실제로 동작하는 패턴입니다.
# resource-update-preview-appset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: resource-update-previews
namespace: argocd
spec:
generators:
- pullRequest:
github:
owner: my-org
repo: k8s-manifests
labels:
- resource-optimization # Goldilocks PR 레이블만 필터링
tokenRef:
secretName: github-token
key: token
requeueAfterSeconds: 300 # 5분마다 PR 상태 재확인
template:
metadata:
# 슬래시 제거 + 50자 이내로 잘라서 네임스페이스 이름 유효성 확보
name: "preview-{{branch | replace '/' '-' | truncate 50}}"
spec:
project: default
source:
repoURL: https://github.com/my-org/k8s-manifests
targetRevision: "{{head_sha}}"
path: helm/my-app
helm:
valueFiles:
- values.yaml
destination:
server: https://kubernetes.default.svc
namespace: "preview-{{branch | replace '/' '-' | truncate 50}}"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=truePull Request Generator: Argo CD가 지정한 레이블이 붙은 PR을 감지해 자동으로 Application 객체를 생성합니다.
requeueAfterSeconds: 300은 5분마다 PR 상태를 재확인해 새로 열린 PR을 감지하는 주기입니다. PR이 닫히면 Application도 자동 삭제됩니다.
이 ApplicationSet은 기존 Argo CD 프로젝트 설정과 함께 사용하게 되는데, project: default 대신 팀의 RBAC 정책에 맞는 프로젝트를 지정하는 것이 좋습니다. preview 네임스페이스에 대한 접근을 별도 AppProject로 격리하면 프로덕션 환경과 충돌 없이 운영할 수 있어요.
이렇게 하면 PR 리뷰어가 "이 추천값으로 변경했을 때 앱이 정상 기동되는가"를 preview 환경에서 직접 확인하고 머지할 수 있습니다. 머지 후에는 Argo CD가 production 네임스페이스에 자동 Sync하는 흐름이 완성됩니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 감사 추적 | 모든 리소스 변경이 Git 커밋으로 기록되어 언제, 왜 변경했는지 추적 가능 |
| 안전한 검토 프로세스 | VPA가 파드를 직접 재시작하지 않고 추천값을 PR로 제안하므로 팀 검토 후 적용 |
| 점진적 확장 | 네임스페이스 레이블로 대상 선별, 확신이 생긴 서비스부터 단계적으로 적용 |
| 비용 가시화 | PR 설명에 예상 비용 절감액을 포함해 비즈니스 가치 수치화 가능 |
| 롤백 용이성 | git revert 한 줄로 이전 리소스 설정으로 즉시 복구 |
단점 및 주의사항
직접 겪었을 때 가장 당황했던 케이스는 HPA 충돌이었어요. CPU 기반 HPA와 VPA를 동시에 쓰면 서로 반대 방향으로 스케일링하려는 상황이 생기거든요. 아래 표에 주요 함정과 대응 방안을 정리해뒀습니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 추천값 수렴 시간 | 신규 배포 직후 추천값이 부정확함 | 배포 후 최소 14일 경과한 워크로드만 PR 생성 대상으로 필터링 |
| 스파이크 트래픽 | 배치 작업·이벤트성 트래픽에 과소 설정 위험 | target * 1.2 안전 마진 적용, upperBound 기반 설정 고려 |
| PR 노이즈 | 매주 수십~수백 개 PR 생성 가능 | delta 임계값(±20%) 필터링으로 의미 있는 변화만 PR 생성 |
| HPA 충돌 | HPA + VPA 동시 사용 시 CPU 스케일링 충돌 | VPA는 메모리에만 적용하거나 resourcePolicy로 CPU 제외 |
| 파드 재시작 | Argo CD 자동 Sync 시 파드 재시작 발생 | ServerSideApply=true + PodDisruptionBudget 설정으로 가용성 보호 |
| 멀티 컨테이너 | 사이드카 추천값이 메인 컨테이너 설정에 혼입 위험 | 컨테이너명 기반 필터링 (select(.containerName != "istio-proxy")) |
QualityOfService (QoS): Kubernetes는
requests == limits이면Guaranteed,requests < limits이면Burstable, requests 미설정이면BestEffort클래스를 부여합니다. 리소스 설정을 변경하면 QoS 클래스가 바뀔 수 있어요. 안정성이 중요한 서비스라면Guaranteed설정을 유지할 수 있도록 limits도 함께 업데이트하는 것을 권장합니다.
실무에서 가장 흔한 실수
-
VPA 데이터 수렴 전에 PR 자동화를 붙이는 경우: Goldilocks를 설치하자마자 CronJob도 같이 돌리면, 며칠치 데이터만 쌓인 상태에서 의미 없는 추천값으로 PR이 생성됩니다. 최소 2주는 데이터를 모은 다음 자동화를 켜는 것을 권장합니다.
-
delta 필터링 없이 운영하는 경우: 처음엔 괜찮아 보여도 서비스가 늘어나면 매주 수십 개의 "1m CPU 줄인" PR이 쏟아집니다. PR 피로도가 올라가면 팀에서 아예 무시하게 되어 자동화의 의미가 사라져요.
-
PodDisruptionBudget 없이 Argo CD AutoSync 켜는 경우: 리소스 PR이 머지되는 순간 Argo CD가 전체 파드를 동시에 재시작할 수 있습니다. 트래픽이 들어오는 시간대에 이게 발생하면 순간적인 서비스 중단으로 이어질 수 있어요.
마치며
이 파이프라인을 팀에 도입하고 나면 달라지는 게 하나 있는데, 리소스 설정이 "한 번 잡으면 건드리기 무서운 것"에서 "매주 PR로 제안받고 검토하는 것"으로 바뀐다는 점이에요. 클러스터가 알려주는 적정값을 사람이 최종 검토해 Git 이력과 함께 안전하게 반영하는 문화가 자연스럽게 만들어집니다.
지금 바로 시작해볼 수 있는 3단계입니다.
-
Goldilocks 설치 및 데이터 수집 시작: 아래 명령어로 설치 후, 가장 비용이 많이 나오는 네임스페이스 한 곳에만 레이블을 붙여 2주간 데이터를 쌓아볼 수 있습니다.
bashhelm repo add fairwinds-stable https://charts.fairwinds.com/stable helm install goldilocks fairwinds-stable/goldilocks \ -n goldilocks --create-namespace kubectl label namespace <ns> goldilocks.fairwinds.com/enabled=true -
추천값 확인 및 필터링 기준 수립:
kubectl get vpa -A -o json | jq '.items[].status.recommendation.containerRecommendations'로 추천값을 직접 확인해보고, 현재values.yaml설정과 비교해 delta 임계값(20~30%)을 팀 상황에 맞게 결정할 수 있습니다. -
CronJob 배포 및 파일럿 운영: 위의 CronJob YAML을 적용하되, 처음엔
--draft플래그로 Draft PR만 생성하도록 설정해두는 것이 좋습니다. 그리고 2~3주만 지켜보면 노이즈 수준과 추천값 품질이 어느 정도인지 감이 잡힙니다.
다음에 다룰 내용
Argo CD ApplicationSet의 Matrix Generator와 Cluster Generator를 활용해 멀티 클러스터 환경에서 리소스 최적화 PR을 환경별(dev/staging/prod)로 단계적으로 배포하는 방법을 다뤄볼 예정입니다. (출판 후 링크 업데이트 예정)
참고 자료
- GitHub - FairwindsOps/goldilocks
- Goldilocks 공식 문서 | Fairwinds
- Kubernetes right-sizing with metrics-driven GitOps automation | AWS Blog
- Goldilocks: Fairwinds Insights 통합 문서
- Fairwinds Insights Automated Fix PR Release Notes
- Argo CD Pull Request Generator 공식 문서
- Automate CI/CD on pull requests with Argo CD ApplicationSets | Red Hat Developer
- Mastering Argo CD Image Updater with Helm | CNCF Blog
- GitHub - wI2L/kubectl-vpa-recommendation
- Grafana VPA Recommendations Dashboard (ID: 14588)
- What is Goldilocks? | CNCF Blog
- Kubernetes Resource Optimization & Best Practices with Goldilocks | Security Boulevard