
Intro
안녕하세요. 환이s입니다 👋
지난 글에서는 데이터 레이크하우스 플랫폼을 GitOps 방식으로 관리하기 위해
ArgoCD를 Kubernetes 위에 구축하고 App of Apps 패턴으로 전체 스택을 운영하는 과정을 정리해 봤습니다.
이번 글에서는 그 위에 얹을 오토스케일링 스택,
KEDA(Kubernetes Event-driven Autoscaling) 를 Harbor 레지스트리와 ArgoCD 기반으로 구축하는 과정을 정리해보려고 합니다.

단순히 CPU/Memory 기반으로 동작하는 HPA와 달리,
KEDA는 Redis 큐 길이나 Prometheus 메트릭 같은 실제 이벤트를 기준으로 Pod를 0~N개까지 스케일 할 수 있습니다.
Airflow처럼 "작업이 몰릴 때만 Worker가 필요한" 워크로드에 딱 맞는 도구죠.
이번 글에서는
- KEDA 이미지·차트를 공식 소스(또는 사내 Harbor)로 준비하는 방법
- Helm OCI 방식으로 KEDA 본체를 설치하는 방법
- CRD를 클러스터에서 추출해 Git + ArgoCD로 별도 관리하는 방법
- 662KB짜리 CRD가 ArgoCD 적용에 실패할 때 대응하는 방법
- ArgoCD sync-wave로 CRD·Operator·RBAC 기동 순서를 제어하는 방법
- 외부 모듈에서 K8s API로 KEDA 리소스를 조회하기 위한 RBAC·Secret 구성
까지 순서대로 확인해 보겠습니다.
이번 포스팅의 전제: KEDA를 완전히 백지에서 올린다기보다,
최초엔 Helm으로 한 번 설치해 클러스터에 띄운 KEDA를, CRD만 떼어내 Git + ArgoCD로 편입(흡수)하는 관점으로 정리했습니다. 즉 "이미 클러스터에 떠 있는 CRD를 GitOps 소스로 역이관하는" 흐름이 섹션 4의 핵심 맥락입니다.
이번 편은 "설치"에 집중하고, 실제로 Worker를 오토스케일링하는 방법은 다음 포스팅에서 다뤄보겠습니다.
1. KEDA 아키텍처 개요
본격적인 설치에 앞서, 이번 구성의 전체 그림을 먼저 이해하는 것이 중요합니다.

KEDA는 크게 세 가지 컴포넌트로 구성됩니다.
- keda-operator : ScaledObject를 감시하고 HPA를 생성/조정하는 핵심 컨트롤러
- keda-operator-metrics-apiserver : 외부 메트릭(Redis, Prometheus)을 K8s 메트릭 API로 노출
- keda-admission-webhooks : ScaledObject 리소스 유효성 검증
즉, 여기서 확인할 수 있는 핵심은
KEDA 자체가 스케일을 직접 수행하는 게 아니라,
이벤트를 읽어서 표준 HPA를 만들어주는 "브릿지" 역할을 한다는 점입니다.
실제 저장소의 KEDA 디렉토리 구조는 다음과 같습니다.
services/keda/
├── crds/
│ ├── keda-crds-only.yaml # CRD 5개 (클러스터에서 추출)
│ └── keda-crds-scaledjobs.yaml # scaledjobs CRD 1개 (662KB, 별도 분리)
├── helm/
│ ├── install.sh # OCI 방식 Helm 설치 스크립트
│ └── values.yaml # Harbor 이미지 + nodeSelector 설정
└── manifests/
└── rbac/
├── 10_keda_api_rbac.yaml # ClusterRole + Binding (API 연동 권한)
└── 20_keda_api_secret.yaml # keda-operator SA 토큰 Secret
정리하면 KEDA는 크게 세 갈래로 구성했습니다.
- crds/ — CRD는 Helm에서 빼내 클러스터에서 추출한 yaml로 별도 관리 (섹션 4)
- helm/ — KEDA 본체는 Helm OCI 방식으로 설치 (섹션 3)
- manifests/rbac/ — 외부 모듈에서 K8s API로 KEDA 리소스를 조회하기 위한 RBAC + Secret 구성 (섹션 7)
💡참고: maxReplicaCount:4 같은 스케일 값은 KEDA 설치 디렉토리(services/keda/)가 아니라, Airflow Worker를 대상으로 하는 ScaledObject 리소스에 들어갑니다.
ScaledObject는 스케일 대상(Airflow)과 함께 두는 것이 관리상 자연스러워, services/airflow/쪽에 위치합니다. 즉 "KEDA 엔진"과 "스케일 규칙(ScaledObject)"의 경로가 분리되어 있는 구조입니다.
2. 환경 준비 - 이미지·차트 소스 정하기
KEDA는 공식 Helm 저장소와 컨테이너 이미지를 제공하므로,
기본적으로는 공식 소스에서 바로 받아 설치하는 것이 가장 간단합니다.
따라 하시는 분들은 별도 레지스트리 없이 아래 공식 소스만으로 충분합니다.
# 공식 Helm 저장소 (KEDA)
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
# 공식 이미지 (ghcr.io)
ghcr.io/kedacore/keda:2.19.0
ghcr.io/kedacore/keda-metrics-apiserver:2.19.0
ghcr.io/kedacore/keda-admission-webhooks:2.19.0
다만 글쓴이는 이미지를 사내 Harbor로 미러링해 관리하고 있어,
이 글의 이후 예시는 Harbor 기준으로 작성돼 있습니다.
버전 고정·보안 스캔·내부 배포 일관성 측면에서 유리해서 택한 방식이고,
공식 소스를 쓰신다면 뒤에 나오는 harbor.local/... 경로만 위 공식 경로로 바꿔 읽으시면 됩니다.
먼저 K8s 버전과 KEDA 호환성부터 확인했습니다.
- K8s 버전: v1.30.14 → KEDA 2.19.0 호환 확인
- 특정 worker 노드에 keda.role=keda 레이블 추가 (KEDA Pod 전용 배치)
글쓴이 환경 기준으로는, 공식 이미지·차트를 사내 Harbor로 옮겨(mirror) 아래처럼 두고 사용합니다.
# Helm Chart (OCI 형식으로 push)
harbor.local/helm-chart/keda:2.19.0
# 이미지 3종 (ghcr.io → Harbor)
harbor.local/library/keda/keda:2.19.0
harbor.local/library/keda/keda-metrics-apiserver:2.19.0
harbor.local/library/keda/keda-admission-webhooks:2.19.0
즉, 여기서 확인할 수 있는 핵심은
KEDA 이미지·차트는 공식 소스에서 바로 받아도 되고, 레지스트리를 사내에서 운영한다면 Harbor 등으로 미러링해 내부 경로만 바라보게 통일할 수도 있다는 점입니다.
3. Helm 설정 - values.yaml
Harbor 이미지를 바라보도록 하고,
keda.role=keda 노드에만 배치되도록 nodeSelector를 지정한 yaml입니다.
operator:
nodeSelector:
keda.role: keda
image:
registry: harbor.local
repository: library/keda/keda
tag: "2.19.0"
metricsServer:
nodeSelector:
keda.role: keda
image:
registry: harbor.local
repository: library/keda/keda-metrics-apiserver
tag: "2.19.0"
webhooks:
nodeSelector:
keda.role: keda
image:
registry: harbor.local
repository: library/keda/keda-admission-webhooks
tag: "2.19.0"
logging:
operator:
level: info
metricServer:
level: 0
webhooks:
level: info
crds:
install: false
💡 참고: crds.install: false가 핵심 포인트입니다.
KEDA Helm Chart는 기본적으로 CRD를 함께 설치하지만, 포스팅 작성할 때는 CRD를 ArgoCD로 별도 관리하기 위해 여기서 비활성화했습니다. 이유는 4·5번 섹션에서 이어집니다.
설치 스크립트는 OCI 레지스트리에서 차트를 가져오도록 구성했습니다.
#!/bin/bash
NAMESPACE=keda
RELEASE_NAME=keda
REGISTRY=harbor.local
CHART_VERSION=2.19.0
helm registry login $REGISTRY
helm upgrade --install $RELEASE_NAME \
oci://$REGISTRY/helm-chart/keda \
--version $CHART_VERSION \
--namespace $NAMESPACE \
--create-namespace \
-f values.yaml
4. CRD 설치 - 클러스터에서 추출해 GitOps로 이관하기
앞서 values.yaml에서 crds.install: false로 CRD를 Helm 설치 대상에서 뺐습니다.
목적은 CRD를 Operator 라이프사이클과 분리해 Git + ArgoCD로 독립 관리하기 위함입니다.
Helm이 CRD까지 함께 관리하면
차트를 업그레이드·삭제할 때 CRD가 영향을 받을 수 있고,
sync-wave로 기동 순서를 제어하기도 어렵기 때문입니다.
방식은 이렇습니다.
글쓴이 환경은 KEDA가 이미 Helm으로 한 번 설치돼 CRD가 클러스터에 등록된 상태였습니다.
그래서 신규로 CRD yaml을 어디선가 받아오는 대신,
클러스터에 이미 떠 있는 CRD를 kubectl로 그대로 추출해 Git 저장소에 올리고,
이후부터는 Helm이 아닌 ArgoCD가 이 CRD를 관리하도록 소유권을 넘겼습니다.
"이미 잘 돌던 리소스를 그대로 Git으로 역이관"하니,
외부에서 새로 받아온 매니페스트와의 버전·필드 불일치 걱정 없이 깔끔했습니다.
💡 참고(순서 정리):
① 최초 1회 Helm으로 CRD 포함 설치(crds.install: true 또는 --include-crds)
② 클러스터에서 CRD 추출·정제 후 Git 커밋
③ crds.install: false로 전환해 Helm에서 CRD를 떼어냄
④ 이후 CRD 소유권은 ArgoCD가 전담. 이 글의 values.yaml(섹션 3)은 ③ 이후 상태 기준입니다. 완전 신규(그린필드) 구축이라면, 클러스터 추출 대신 helm template --include-crds로 순수 yaml을 렌더링해 Git에 올리는 방법도 동일하게 가능합니다.
Step 1. 클러스터에서 KEDA CRD 목록 확인
kubectl get crd | grep keda.sh
cloudeventsources.eventing.keda.sh
clustercloudeventsources.eventing.keda.sh
clustertriggerauthentications.keda.sh
scaledjobs.keda.sh
scaledobjects.keda.sh
triggerauthentications.keda.sh
Step 2. scaledjobs를 제외한 CRD 5개 추출
for crd in \
cloudeventsources.eventing.keda.sh \
clustercloudeventsources.eventing.keda.sh \
clustertriggerauthentications.keda.sh \
scaledobjects.keda.sh \
triggerauthentications.keda.sh; do
kubectl get crd $crd -o yaml >> keda-crds-only.yaml
echo "---" >> keda-crds-only.yaml
done
Step 3. scaledjobs CRD 단독 추출
# scaledjobs는 662KB로 용량이 커서 ArgoCD 적용 실패 → 별도 파일로 분리
kubectl get crd scaledjobs.keda.sh -o yaml > keda-crds-scaledjobs.yaml
Step 4. 런타임 필드 정제 (중요)
kubectl get -o yaml로 뽑은 CRD에는 클러스터가 실행 중에 붙인 런타임 메타데이터가 섞여 있습니다.
이걸 그대로 Git에 올리면 ArgoCD가 계속 OutOfSync로 감지하거나 적용에 실패하므로,
아래 필드들을 제거해 "선언적 스펙"만 남겨야 합니다.
- metadata.annotations의 kubectl.kubernetes.io/last-applied-configuration
- metadata.managedFields
- metadata.resourceVersion, metadata.uid, metadata.generation, metadata.creationTimestamp
- status 블록 전체
가장 간편한 건 kubectl-neat(krew 플러그인)로, 추출과 동시에 런타임 필드를 알아서 걷어내 줍니다.
# 방법 A) kubectl-neat 사용 (권장) — 추출과 동시에 정제
kubectl get crd scaledjobs.keda.sh -o yaml | kubectl neat > keda-crds-scaledjobs.yaml
# krew/neat 설치가 어려운 환경이라면, --show-managed-fields=false로 managedFields만이라도 제외
kubectl get crd scaledjobs.keda.sh -o yaml --show-managed-fields=false > keda-crds-scaledjobs.yaml
kubectl-neat 없이 yq로 직접 정제한다면, keda-crds-only.yaml은 ---로 이어진
멀티도큐먼트(5개 문서)라는 점에 주의해야 합니다.
일반 yq eval ... -i는 첫 문서만 처리될 수 있어
eval-all로 모든 문서를 순회해야 합니다.
# 방법 B) yq로 정제 — eval-all로 멀티도큐먼트 전체 순회
yq eval-all '
del(.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") |
del(.metadata.managedFields) |
del(.metadata.resourceVersion) |
del(.metadata.uid) |
del(.metadata.generation) |
del(.metadata.creationTimestamp) |
del(.status)
' -i keda-crds-only.yaml
# scaledjobs 단일 파일도 동일하게 정제
yq eval-all '
del(.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") |
del(.metadata.managedFields) |
del(.metadata.resourceVersion) |
del(.metadata.uid) |
del(.metadata.generation) |
del(.metadata.creationTimestamp) |
del(.status)
' -i keda-crds-scaledjobs.yaml
💡 참고: 정제와 ServerSideApply는 역할이 다릅니다.
정제(Step 4)는 Git에 올라가는 소스 파일을 깨끗하게 만드는 작업이고(OutOfSync 방지),
뒤 섹션 5의 ServerSideApply=true는 ArgoCD가 apply 하는 시점에 last-applied-configuration 어노테이션으로 스펙 전체를 다시 써넣지 않게 하는 장치입니다.
하나는 소스 정리, 하나는 적용 방식이므로, 둘 다 있어야 662KB scaledjobs가 안정적으로 처리됩니다.
결과적으로 CRD 파일은 두 개로 나뉘었습니다.
- keda-crds-only.yaml — CRD 5개 (cloudeventsources, clustercloudeventsources, clustertriggerauthentications, scaledobjects, triggerauthentications)
- keda-crds-scaledjobs.yaml — scaledjobs CRD 1개 (용량 이슈로 별도 분리)
⚠️ 주의: scaledjobs CRD는 파일 크기가 662KB에 달합니다.
특히 last-applied-configuration 어노테이션은 CRD 스펙 전체를 문자열로 담기 때문에, 이걸 정제하지 않으면 이 어노테이션만으로도 Kubernetes의 어노테이션 크기 제한(262144 B)을 넘겨 적용에 실패합니다.
Step 4의 정제와, 뒤에서 다룰 ServerSideApply=true를 함께 써야 662KB CRD가 안정적으로 적용됩니다.
즉, 여기서 확인할 수 있는 핵심은
CRD는 Helm 관리에서 떼어내 클러스터에서 추출한 순수 선언 yaml로 Git에 두되,
last-applied-configuration 같은 런타임 필드는 반드시 정제하고, 용량이 큰 CRD는 쪼개야 한다는 점입니다.
설치 후 CRD 6개가 정상 등록됐는지 확인했습니다.
cloudeventsources.eventing.keda.sh
clustercloudeventsources.eventing.keda.sh
clustertriggerauthentications.keda.sh
scaledjobs.keda.sh
scaledobjects.keda.sh
triggerauthentications.keda.sh
5. ArogCD App 등록 - sync-wave로 기동 순서 제어
이제 앞서 준비한 KEDA를 이전 포스팅에서 구축한 ArgoCD에서 App으로 관리할 수 있게 설정합니다.
지난 글에서 ArgoCD를 App of Apps 패턴으로 올려뒀으니,
KEDA도 그 아래에 Application으로 등록해 GitOps 흐름에 편입시키는 것이죠.
여기가 이번 설치에서 가장 중요한 부분입니다.
KEDA Operator는 기동 시점에 CRD가 이미 존재해야 정상 동작합니다.
CRD보다 Operator가 먼저 뜨면 CrashLoopBackOff에 빠집니다.
그래서 KEDA를 3개의 ArgoCD Application으로 쪼개고, sync-wave로 순서를 명시적으로 갈랐습니다.
gitops/apps/keda/
├── keda_crds.yaml (sync-wave: 130) → CRD 먼저
├── keda_helm.yaml (sync-wave: 131) → Operator 나중
└── keda_rbac.yaml (sync-wave: 132) → RBAC 마지막 (namespace 생성 후)
💡 참고: sync-wave 숫자가 낮을수록 먼저 적용됩니다.
CRD(130) → Helm(131) → RBAC(132) 순서로 배치해서 "CRD가 완전히 준비된 뒤 Operator가 기동 되고, namespace/SA가 생긴 뒤 RBAC이 붙도록" 보장한 것입니다.
① keda_crds — CRD 먼저 (wave 130)
CRD는 services/keda/crds 디렉토리를 통째로(recurse: true) 적용합니다.
662KB짜리 scaledjobs 대응을 위해 ServerSideApply=true가 핵심입니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keda-crds
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "130"
spec:
project: default
sources:
- repoURL: 'http://://data-platform.git'
targetRevision: main
path: services/keda/crds
directory:
recurse: true
destination:
server: '<https://kubernetes.default.svc>'
namespace: keda
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true # 662KB scaledjobs 어노테이션 제한 회피
⚠️ 주의: ServerSideApply=true는 섹션 4 정제와 짝을 이루는 적용 시점의 안전장치입니다.
정제(Step 4)로 소스 파일에서 어노테이션을 걷어내도,
ArgoCD가 client-side apply로 붙이면 apply 과정에서 last-applied-configuration에 스펙 전체를 다시 써넣습니다.
ServerSideApply를 켜면 이 어노테이션을 아예 만들지 않고 서버 측에서 필드를 병합하므로,
662KB scaledjobs처럼 큰 CRD도 어노테이션 크기 제한에 걸리지 않고 안정적으로 적용됩니다.
즉 "파일 분리(섹션 4) + 소스 정제(Step 4) + ServerSideApply(적용)" 세 겹이 함께 작동합니다.
② keda_helm — Operator (wave 131, multi-source)
KEDA 본체는
Harbor OCI 차트 + Git values.yaml을 분리 참조하는 multi-source 패턴으로 설치합니다.
차트는 Harbor에서, 설정값은 Git 저장소에서 각각 가져와 조합하는 방식입니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keda-helm
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "131"
spec:
project: default
sources:
- repoURL: 'harbor.local/helm-chart' # Harbor OCI 레지스트리의 차트
chart: keda
targetRevision: 2.19.0
helm:
valueFiles:
- $values/services/keda/helm/values.yaml # ↓ Git values 참조
- repoURL: 'http://://data-platform.git'
targetRevision: main
ref: values # $values 로 위에서 참조됨
destination:
server: '<https://kubernetes.default.svc>'
namespace: keda
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
💡 참고: multi-source의 ref: values + $values/... 조합이 포인트입니다.
"차트는 Harbor에, values.yaml은 Git에" 두면서도 하나의 Application으로 묶을 수 있어, 차트 버전과 설정을 각자의 저장소에서 독립적으로 관리할 수 있습니다.
③ keda_rbac — API 연동용 RBAC (wave 132)
앞서 만든 services/keda/manifests/rbac를 마지막 wave로 적용합니다.
namespace와 keda-operator ServiceAccount가
생성된 뒤에 붙어야 하므로 132로 뒤에 뒀고, 타이밍 이슈에 대비해 retry를 넣었습니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keda-rbac
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "132" # namespace 생성 후 rbac
spec:
project: default
source:
repoURL: http://://data-platform.git
targetRevision: main
path: services/keda/manifests/rbac
destination:
server: <https://kubernetes.default.svc>
namespace: keda
syncPolicy:
automated:
prune: true
selfHeal: true
retry:
limit: 5
backoff:
duration: 15s
factor: 2
maxDuration: 2m
syncOptions:
- CreateNamespace=true
3개 App이 모두 Healthy + Synced 상태가 되고,
Pod 3개(keda-operator, keda-admission-webhooks, keda-operator-metrics-apiserver)가 Running이면 설치 완료입니다.
즉, 여기서 확인할 수 있는 핵심은
CRD·Operator·RBAC를 한 App에 묶지 말고 3개로 쪼갠 뒤
sync-wave(130→131→132)로 순서를 강제해야,
CrashLoop과 SA 미존재 같은 순서 의존 문제를 피할 수 있다는 점입니다.
6. 설치 트러블슈팅 정리
글쓴이도 여러 착오 끝에 구축할 수 있었는데, 오는 과정에서 마주친 이슈들을 표로 정리했습니다.
| 문제 | 원인 | 해결 |
| keda-operator CrashLoopBackOff |
CRD보다 Operator가 먼저 기동 | CRD App(wave 130) → Helm App(wave 131) 순서 분리 |
| scaledjobs CRD 미설치 |
파일 크기 662KB로 ArgoCD 적용 실패 | 별도 파일로 분리 + ServerSideApply=true 적용 |
| CRD 적용 후 계속 OutOfSync |
kubectl get -o yaml 추출물에 런타임 필드(managedFields, resourceVersion 등) 잔존 | Step 4 정제로 런타임 필드 제거 후 커밋 |
| scaledjobs 어노테이션 제한 초과 |
last-applied-configuration에 CRD 스펙 전체가 문자열로 포함 | 해당 어노테이션 제거 + ServerSideApply=true |
| RBAC 적용 시 SA 미존재 에러 |
keda-operator SA 생성 전 RBAC 먼저 적용 | RBAC을 wave 132로 뒤에 두고 retry 설정 |
7. 외부 모듈에서 KEDA 리소스를 조회하기 위한 RBAC·Secret 구성
앞선 섹션들에서 KEDA 리소스는 전부 kubectl이나 ArgoCD를 통해 다뤘습니다.
그런데 플랫폼을 운영하다 보면,
kubectl이 아니라 별도 모듈(백오피스·대시보드·운영 자동화 등)에서 KEDA 리소스 상태를 직접 조회하고 싶은 상황이 생깁니다.
예를 들면,
- 사내 백오피스/대시보드에서 현재 ScaledObject 목록과 스케일 상태를 보여주고 싶을 때
- 운영 자동화 모듈이 특정 조건에서 ScaledObject 상태를 확인하고 싶을 때
- CI 파이프라인에서 KEDA 리소스 상태를 점검하고 싶을 때
Kubernetes API 서버는 모든 리소스를 API 엔드포인트로 노출하므로,
모듈이 kubectl 없이 HTTP 요청 + 토큰만으로 KEDA 리소스(scaledobjects 등)를 조회할 수 있습니다.
문제는 인증/인가입니다.
아무 토큰이나 API 서버에 보낸다고 되는 게 아니라,
"이 토큰이 keda.sh 리소스에 접근할 권한이 있는가"를 RBAC으로 열어주고,
그 권한을 담은 토큰(Secret)을 발급해줘야 합니다.
그래서 이번에는
그 연동에 앞서 필요한 전용 RBAC + SA 토큰(Secret)을 추가로 구성해봤습니다.
아직 실제 모듈 연동까지 붙인 단계는 아니고,
"모듈이 붙으면 쓸 권한·토큰을 미리 열어두고,
그게 의도대로 동작하는지 검증"하는 데까지가 이번 범위입니다.
OSS 데이터 플랫폼 리소스를
API로 연동하려는 분들도 아래 패턴을 그대로 적용하면 됩니다.
파일은 services/keda/manifests/rbac/ 아래 두 개, ArgoCD keda-rbac App(wave 132)으로 배포됩니다.
① ClusterRole + Binding — 권한 열기
keda.sh API 그룹의 리소스에 대한 권한을 정의하고,
keda-operator ServiceAccount에 바인딩합니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: keda-api-reader
rules:
- apiGroups: ["keda.sh"]
resources: ["scaledobjects", "scaledjobs", "triggerauthentications", "clustertriggerauthentications"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: keda-api-reader-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: keda-api-reader
subjects:
- kind: ServiceAccount
name: keda-operator
namespace: keda
⚠️ 주의: 예시는 create/update/patch/delete까지 전부 열려 있어 KEDA 리소스를 변경할 수 있는 강한 권한입니다.
조회만 필요하다면 verbs를 ["get", "list", "watch"]로 최소화하세요.
최소 권한 원칙(least privilege)이 보안의 기본입니다.
② SA 토큰 Secret — 인증 수단 발급
keda-operator ServiceAccount에 대한 장기 토큰(long-lived token) 을 발급받기 위한 Secret입니다.
apiVersion: v1
kind: Secret
metadata:
name: keda-operator-token
namespace: keda
annotations:
kubernetes.io/service-account.name: keda-operator
type: kubernetes.io/service-account-token
③ 토큰 꺼내기
발급된 토큰과 API 서버 주소를 확인합니다.
# SA 토큰 추출
TOKEN=$(kubectl get secret keda-operator-token -n keda \
-o jsonpath='{.data.token}' | base64 -d)
# API 서버 주소 확인
APISERVER=$(kubectl config view --minify \
-o jsonpath='{.clusters[0].cluster.server}')
echo $APISERVER # 예: https://<APISERVER_HOST>:6443
④ 권한이 제대로 열렸는지 검증 (Postman)
모듈을 붙이기 전에,
발급한 RBAC·토큰으로 실제 조회가 되는지부터 확인해 봅니다.
모듈 코드를 짜기 전에 권한 설정만 먼저 검증해 두면,
나중에 연동할 때 "권한 문제인지 코드 문제인지" 헷갈릴 일이 줄어듭니다.
Postman으로 확인한 설정은 다음과 같습니다.
- Method / URL
GET https://<APISERVER_HOST>:6443/apis/keda.sh/v1alpha1/namespaces/airflow/scaledobjects
- Authorization 탭 → Type: Bearer Token → Token 칸에 위에서 꺼낸 $TOKEN 붙여 넣기
- Settings → 사내 클러스터가 자체 서명 인증서를 쓰면 SSL certificate verification을 끄거나, CA 인증서를 등록
정상이라면 airflow 네임스페이스의 ScaledObject 목록이 JSON으로 돌아옵니다.
{
"apiVersion": "keda.sh/v1alpha1",
"kind": "ScaledObjectList",
"items": [
{
"metadata": { "name": "airflow-worker-scaler", "namespace": "airflow" },
"spec": { "minReplicaCount": 2, "maxReplicaCount": 19 }
}
]
}

curl로도 동일하게 확인할 수 있습니다.
실제 모듈에서 호출할 때도 결국 이 형태(엔드포인트 + Bearer 토큰)를 그대로 쓰게 됩니다.
curl -sk -H "Authorization: Bearer $TOKEN" \
"$APISERVER/apis/keda.sh/v1alpha1/namespaces/airflow/scaledobjects" | jq .
즉, 여기서 확인할 수 있는 핵심은
K8s API 서버가 모든 리소스를 API로 노출하므로, 외부 모듈이 KEDA 리소스를 조회하려면 전용 RBAC + SA 토큰(Secret)만 미리 열어두면 되고, 연동 전에 Postman/curl로 그 권한이 제대로 동작하는지 검증해 둘 수 있다는 점입니다.
그리고 그 편리함만큼 권한 최소화와 토큰 관리가 보안의 핵심이 됩니다.
📝 마무리
이번 글에서는 KEDA를 Harbor + ArgoCD 기반으로 구축하는 과정을 정리해 봤습니다.
정리하면,
- CRD는 Helm 관리에서 떼어내 클러스터에서 추출한 뒤 런타임 필드를 정제해 Git에 두고, 용량 큰 CRD(scaledjobs 662KB)는 쪼갠 뒤 ServerSideApply로 적용한다
- CRD·Operator·RBAC를 3개 App으로 나누고 sync-wave(130→131→132)로 순서를 강제해야 CrashLoop·SA 미존재를 피한다
- 외부 모듈에서 KEDA 리소스를 조회하려면 전용 RBAC + SA 토큰(Secret)을 미리 열어두면 되고, 연동 전 Postman/curl로 권한이 제대로 동작하는지 검증해 둘 수 있다
정도가 되겠습니다.
다음 글에서는 이렇게 설치한 KEDA를 실제로 활용해서,
Airflow Celery Worker를 Redis + Prometheus 멀티 트리거로 오토스케일링하는 과정을 다뤄보려고 합니다.
궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있다면 언제든지 편하게 말씀해 주세요😊
'[ Infra ] > Data Platform' 카테고리의 다른 글
| [Data Platform] KEDA 오토스케일링 가이드 (멀티 트리거 설계 feat. Airflow·Spark) (1) | 2026.07.13 |
|---|---|
| [Data Platform] ArgoCD GitOps 운영 가이드(App of Apps 패턴으로 플랫폼 관리) (0) | 2026.07.07 |
| [Data Platform] ArgoCD 구축 가이드 (Helm + Private Registry + Ingress 구성) (0) | 2026.07.06 |
| [Data Platform] Superset 구축 가이드 (feat. Trino 연동) (0) | 2026.06.10 |
| [Data Platform] Prometheus + Grafana 구축 가이드 (feat. kube-prometheus-stack) (0) | 2026.06.09 |