이번 포스트에서는 EKS의 오토스케일링(Autoscaling) 옵션을 살펴보겠습니다.

 

기본적인 쿠버네티스 환경의 스케일링 옵션을 전반적으로 살펴보겠습니다. 이후 EKS의 오토스케일링 옵션을 살펴보고, 이를 실습을 통해 확인 해보도록 하겠습니다. 마지막으로 AKS의 오토스케일링 옵션을 EKS와 비교해 보겠습니다.

 

글을 작성하는 과정에서 분량이 너무 길어져, Part1에서는 HPA, KEDA, VPA까지의 내용을 다루고 Part2에서 Cluster Autoscaler 부터 이어서 설명하도록 하겠습니다.

 

목차

EKS의 오토스케일링 Part1

  1. 쿠버네티스 환경의 스케일링
  2. EKS의 오토스케일링 개요
  3. 실습 환경 생성
  4. HPA(Horizontal Pod Autoscaler)
  5. KEDA(Kubernetes Event-driven Autoscaler)
  6. VPA(Vertical Pod Autoscaler)

EKS의 오토스케일링 Part2 (https://a-person.tistory.com/39)

  1. CA(Cluster Autoscaler)
  2. Karpenter
  3. AKS의 오토스케일링
  4. 오토스케일링에 대한 주의사항

 

1. 쿠버네티스 환경의 스케일링

쿠버네티스 패턴(책만, 2020)에서는 쿠버네티스 환경의 애플리케이션의 스케일링 레벨을 아래와 같이 설명하고 있습니다.

출처: 빌긴 이브리암/롤란트 후스, 안승규/서한배, 책만, 2020, 272.

 

먼저 애플리케이션 튜닝이 필요합니다. 쿠버네티스 환경이라고 할지라도 애플리케이션 자체가 최대한의 성능을 사용하도록 동작해야 합니다. 애플리케이션에 할당된 리소스를 증가시키거나 복제본을 증가시키는 것은 부차적인 일입니다.

 

두번째는 수직 파드 오토스케일러(VPA, Vertical Pod Autoscaler)입니다. 파드의 리소스가 부족할 때 설정된 리소스(CPU/MEM) 자체를 증가시키는 방식입니다. 쿠버네티스 환경에서는 request와 limit을 지정할 수 있고, 이 값이 변경됩니다.

 

세번째는 수평 파드 오토스케일러(HPA, Horizontal Pod Autoscaler)입니다. 파드의 리소스가 임계치 이상 사용되면, 동일한 파드의 복제본을 증가시키는 방식입니다.

 

네번째는 VPA나 HPA로 애플리케이션이 용량이나 갯수가 증가했을 때, 노드의 할당 가능한 자원(Allocatable resource)을 모두 소진하면 파드가 스케줄링 불가능(Unschedurable Pod)한 상황이 발생할 수 있습니다. 이때는 노드 자체를 증가시켜야 합니다. 이것을 클러스터 오토스케일러(CA, Cluster Autoscaler)에서 지원합니다.

 

쿠버네티스 환경은 애플리케이션의 스케일링을 하기 위해서 위와 같은 기법을 사용할 수 있으며, 이후 EKS에서는 이들을 어떤식으로 활용할 수 있는지 살펴보겠습니다.

 

일반적으로 애플리케이션 튜닝은 쿠버네티스에 국한되지 않은 별개의 영역이므로 설명에 제외하도록 하겠습니다.

 

 

2. EKS의 오토스케일링 개요

EKS에서는 설명한 HPA, VPA, CA를 모두 지원하고 있으며, Karpenter이라는 노드 오토스케일링을 방식을 추가로 제공하고 있습니다. 또한 그림에는 없지만 KEDA를 통해서 이벤트를 통해 HPA를 확장할 수 있는 방식도 있습니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

HPA는 쿠버네티스 환경에서 기본으로 제공되기 때문에 별도의 설치 과정 없이 hpa 오브젝트를 생성하여 사용할 수 있습니다.

 

EKS에서는 KEDA, VPA, CA, Karpenter는 helm이나 yaml을 배포하는 방식으로 사용자가 설치 및 구성해야 합니다. 애드온과 같은 방식이 아니라 오픈 소스를 배포하므로 직접 라이프사이클을 관리하며, 제공되는 모든 옵션을 활용할 수 있습니다.

 

컨트롤러에 해당하는 컴포넌트가 데이터 플레인에 직접 배포되기 때문에 동작 과정을 이해할 수 있습니다. 다만 이 또한 데이터 플레인의 리소스를 사용한다는 점과 사용자 책임의 관리가 필요한 점을 유의해야 합니다.

 

참고로 AKS의 KEDA, VPA, CA, Karpenter는 애드온이나 기능으로 제공되기 때문에 Managed의 영역으로 이해할 수 있습니다.

 

 

3. 실습 환경 생성

실습 환경은 아래와 같습니다.

 

CloudFormation을 바탕으로 실습 환경을 구성하도록 하겠습니다.

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-5week.yaml

# 변수 지정
CLUSTER_NAME=myeks
SSHKEYNAME=<SSH 키 페이 이름>
MYACCESSKEY=<IAM Uesr 액세스 키>
MYSECRETKEY=<IAM Uesr 시크릿 키>

# CloudFormation 스택 배포
aws cloudformation deploy --template-file myeks-5week.yaml --stack-name $CLUSTER_NAME --parameter-overrides KeyName=$SSHKEYNAME SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=$MYACCESSKEY MyIamUserSecretAccessKey=$MYSECRETKEY ClusterBaseName=$CLUSTER_NAME --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# EC2 접속
ssh -i ~/.ssh/ekskey.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

 

CloudFormation에서 운영서버를 배포하고, 이후 EKS 까지 배포를 하게 되어 있습니다. 대략 15~20분 가량이 소요됩니다.

그리고 생성된 EKS를 확인하고 kubeconfig을 받습니다.

# 변수 지정
CLUSTER_NAME=myeks

# 클러스터 확인
eksctl get cluster

# kubeconfig 생성
aws sts get-caller-identity --query Arn
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias <위 출력된 자격증명 사용자>

# 클러스터 기본 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -A
NAME                                               STATUS   ROLES    AGE     VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
ip-192-168-1-87.ap-northeast-2.compute.internal    Ready    <none>   3m12s   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2a
ip-192-168-2-195.ap-northeast-2.compute.internal   Ready    <none>   3m9s    v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2b
ip-192-168-3-136.ap-northeast-2.compute.internal   Ready    <none>   3m8s    v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2c

kubectl get pod -A
NAMESPACE     NAME                                  READY   STATUS    RESTARTS   AGE
kube-system   aws-node-b7vsh                        2/2     Running   0          3m16s
kube-system   aws-node-fnrj8                        2/2     Running   0          3m13s
kube-system   aws-node-ltkxn                        2/2     Running   0          3m12s
kube-system   coredns-86f5954566-96j8r              1/1     Running   0          9m10s
kube-system   coredns-86f5954566-j6dvp              1/1     Running   0          9m10s
kube-system   ebs-csi-controller-549bf6879f-6h7jg   6/6     Running   0          49s
kube-system   ebs-csi-controller-549bf6879f-p2gml   6/6     Running   0          49s
kube-system   ebs-csi-node-bm94h                    3/3     Running   0          49s
kube-system   ebs-csi-node-h5ntq                    3/3     Running   0          49s
kube-system   ebs-csi-node-n7s2s                    3/3     Running   0          49s
kube-system   kube-proxy-brf4v                      1/1     Running   0          3m16s
kube-system   kube-proxy-x7sbw                      1/1     Running   0          3m13s
kube-system   kube-proxy-zm6ht                      1/1     Running   0          3m12s
kube-system   metrics-server-6bf5998d9c-bzxn6       1/1     Running   0          9m9s
kube-system   metrics-server-6bf5998d9c-zs5mg       1/1     Running   0          9m10s

 

그리고 이후 실습에 활용하기 위한 일부 컴포넌트들을 설치합니다.

# 환경 변수
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
MyDomain=aperson.link # 각자 자신의 도메인 이름 입력
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "$MyDomain." --query "HostedZones[0].Id" --output text)

# AWS LoadBalancerController
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller

# ExternalDNS
echo $MyDomain
curl -s https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml | MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst | kubectl apply -f -

# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=ClusterIP  --set env.TZ="Asia/Seoul" --namespace kube-system

# kubeopsview 용 Ingress 설정 : group 설정으로 1대의 ALB를 여러개의 ingress 에서 공용 사용
echo $CERT_ARN
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-name: $CLUSTER_NAME-ingress-alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/success-codes: 200-399
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app.kubernetes.io/name: kubeopsview
  name: kubeopsview
  namespace: kube-system
spec:
  ingressClassName: alb
  rules:
  - host: kubeopsview.$MyDomain
    http:
      paths:
      - backend:
          service:
            name: kube-ops-view
            port:
              number: 8080  # name: http
        path: /
        pathType: Prefix
EOF

 

Prometheus와 Grafana도 설치를 진행합니다.

# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

# 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하여 PV/PVC 미사용 하도록 수정
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    scrapeInterval: "15s"
    evaluationInterval: "15s"
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  # Enable vertical pod autoscaler support for prometheus-operator
  verticalPodAutoscaler:
    enabled: true

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - prometheus.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

grafana:
  defaultDashboardsTimezone: Asia/Seoul
  adminPassword: xxx # Grafana 패스워드
  defaultDashboardsEnabled: false

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - grafana.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

kube-state-metrics:
  rbac:
    extraRules:
      - apiGroups: ["autoscaling.k8s.io"]
        resources: ["verticalpodautoscalers"]
        verbs: ["list", "watch"]
  customResourceState:
    enabled: true
    config:
      kind: CustomResourceStateMetrics
      spec:
        resources:
          - groupVersionKind:
              group: autoscaling.k8s.io
              kind: "VerticalPodAutoscaler"
              version: "v1"
            labelsFromPath:
              verticalpodautoscaler: [metadata, name]
              namespace: [metadata, namespace]
              target_api_version: [apiVersion]
              target_kind: [spec, targetRef, kind]
              target_name: [spec, targetRef, name]
            metrics:
              - name: "vpa_containerrecommendations_target"
                help: "VPA container recommendations for memory."
                each:
                  type: Gauge
                  gauge:
                    path: [status, recommendation, containerRecommendations]
                    valueFrom: [target, memory]
                    labelsFromPath:
                      container: [containerName]
                commonLabels:
                  resource: "memory"
                  unit: "byte"
              - name: "vpa_containerrecommendations_target"
                help: "VPA container recommendations for cpu."
                each:
                  type: Gauge
                  gauge:
                    path: [status, recommendation, containerRecommendations]
                    valueFrom: [target, cpu]
                    labelsFromPath:
                      container: [containerName]
                commonLabels:
                  resource: "cpu"
                  unit: "core"
  selfMonitor:
    enabled: true

alertmanager:
  enabled: false
defaultRules:
  create: false
kubeControllerManager:
  enabled: false
kubeEtcd:
  enabled: false
kubeScheduler:
  enabled: false
prometheus-windows-exporter:
  prometheus:
    monitor:
      enabled: false
EOT
cat monitor-values.yaml

# helm 배포
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 69.3.1 \
-f monitor-values.yaml --create-namespace --namespace monitoring

# helm 확인
helm get values -n monitoring kube-prometheus-stack

# PV 사용하지 않음
kubectl get pv,pvc -A

# 프로메테우스 웹 접속
echo -e "https://prometheus.$MyDomain"

# 그라파나 웹 접속
echo -e "https://grafana.$MyDomain"

# TargetGroup binding 확인
kubectl get targetgroupbindings.elbv2.k8s.aws -A
NAMESPACE     NAME                               SERVICE-NAME                       SERVICE-PORT   TARGET-TYPE   AGE
kube-system   k8s-kubesyst-kubeopsv-b2ecfd420f   kube-ops-view                      8080           ip            2m54s
monitoring    k8s-monitori-kubeprom-40399c957e   kube-prometheus-stack-grafana      80             ip            45s
monitoring    k8s-monitori-kubeprom-826f25cbb8   kube-prometheus-stack-prometheus   9090           ip            45s

 

 

4. HPA(Horizontal Pod Autoscaler)

HPA는 지정된 워크로드의 특정 메트릭이 임계치를 초과하는 경우 복제본(Replicas)를 증가시킵니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

HPA는 쿠버네티스 환경에서 기본으로 제공되기 때문에 어떤 환경에 있는 쿠버네티스에서도 사용이 가능합니다. 주의할 점은 HPA가 metrics-server에서 제공되는 core system metrics에 의해서 판단하게 되므로, metrics-server가 정상적이지 않으면 HPA가 동작하지 않는 점만 기억하시면 됩니다.

 

HPA는 EKS 특화된 기술이 아니기 때문에 EKS 환경의 실습은 진행하지 않으며, 다음 절에서 KEDA를 통해 HPA를 사용하는 사례를 통해 살펴보도록 하겠습니다.

 

HPA 실습

HPA에 대한 실습은 쿠버네티스 공식 문서를 참고하실 수 있습니다.

https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/

 

1) 샘플 애플리케이션

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  selector:
    matchLabels:
      run: php-apache
  template:
    metadata:
      labels:
        run: php-apache
    spec:
      containers:
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
          requests:
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
  labels:
    run: php-apache
spec:
  ports:
  - port: 80
  selector:
    run: php-apache

 

2) HPA 정의

kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10

 

3) Load 발생

# Run this in a separate terminal
# so that the load generation continues and you can carry on with the rest of the steps
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"

 

이후 kubectl get hpa php-apache --watch를 통해 메트릭의 증가와 복제본의 증가를 확인하실 수 있습니다.

 

HPA의 메트릭 확장

HPA는 Metric API를 통해서 값을 수집하는데, 이는 쿠버네티스에서 기본 제공되는 API가 아니며, 이를 노출하기 위한 metrics-server가 필요합니다. 또한 HPA는 이미 정의된 Resource Metric에 의해서 스케일링을 판단하는데 이는 metrics-server에 의해서 기본적으로 제공됩니다.

 

이러한 Metrics API를 확장하기 위해서 Custom Metric, External Metric을 사용할 수 있습니다. 보통 Prometheus를 통해서 추가 메트릭을 수집하고, Prometheus에서 수집된 메트릭을 기반으로 Prometheus Adapter이 Custom Metric API Server의 역할을 해 Custom Metric와 External Metric을 노출합니다.

 

출처: https://itnext.io/autoscaling-apps-on-kubernetes-with-the-horizontal-pod-autoscaler-798750ab7847

 

이후 살펴볼 KEDA 또한 자체적으로 Metrics API Server를 가지고 External Metircs를 노출하게 됩니다.

 

 

5. KEDA(Kubernetes Event-driven Autoscaler)

HPA는 metrics-server에 의해서 수집된 CPU, Memory와 같은 메트릭을 기반으로 스케일링을 결정합니다. 이러한 리소스 기반이 아닌 다른 메트릭을 참조하여 HPA를 동작하도 도와주는 컴포넌트가 KEDA(Kubernetes Event-driven Autoscaler)입니다.

KEDA는 다양한 Event Source로 부터 발생하는 이벤트를 기반으로 스케일링 여부를 결정할 수 있습니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

EKS에서는 helm을 통해서 KEDA를 설치할 수 있습니다. (해당 실습에서 prometheus를 통해 모니터링을 하는 부분이 포함되어 있어, 사전 Prometheus가 설치되어 있어야 합니다)

# 설치 전 기존 metrics-server 제공 Metris API 확인
kubectl get --raw "/apis/metrics.k8s.io" | jq
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "metrics.k8s.io",
  "versions": [
    {
      "groupVersion": "metrics.k8s.io/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {
    "groupVersion": "metrics.k8s.io/v1beta1",
    "version": "v1beta1"
  }
}

# external metrics는 없음
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq
Error from server (NotFound): the server could not find the requested resource


# KEDA 설치
cat <<EOT > keda-values.yaml
metricsServer:
  useHostNetwork: true

prometheus:
  metricServer:
    enabled: true
    port: 9022
    portName: metrics
    path: /metrics
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus Operator
      enabled: true
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true
  operator:
    enabled: true
    port: 8080
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus Operator
      enabled: true
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true
  webhooks:
    enabled: true
    port: 8020
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus webhooks
      enabled: true
EOT

helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --version 2.16.0 --namespace keda --create-namespace -f keda-values.yaml

# apiservice가 생성된 것을 알 수 있습니다.
kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  annotations:
    meta.helm.sh/release-name: keda
    meta.helm.sh/release-namespace: keda
  creationTimestamp: "2025-03-06T13:23:54Z"
  labels:
    app.kubernetes.io/component: operator
    app.kubernetes.io/instance: keda
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: v1beta1.external.metrics.k8s.io
    app.kubernetes.io/part-of: keda-operator
    app.kubernetes.io/version: 2.16.0
    helm.sh/chart: keda-2.16.0
  name: v1beta1.external.metrics.k8s.io
  resourceVersion: "7353"
  uid: 26d3a3b1-7487-4086-84f9-1fd3105aa89d
spec:
  caBundle: <생략>
  group: external.metrics.k8s.io
  groupPriorityMinimum: 100
  service:
    name: keda-operator-metrics-apiserver
    namespace: keda
    port: 443
  version: v1beta1
  versionPriority: 100
status:
  conditions:
  - lastTransitionTime: "2025-03-06T13:24:25Z"
    message: all checks passed
    reason: Passed
    status: "True"
    type: Available

# 설치 후 KEDA Metrics Server의해 노출된 External Metrics를 확인합니다.
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq
{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "external.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "externalmetrics",
      "singularName": "",
      "namespaced": true,
      "kind": "ExternalMetricValueList",
      "verbs": [
        "get"
      ]
    }
  ]
}

 

KEDA를 설치한 이후 생성된 파드를 살펴보면 KEDA의 구성요소를 알 수 있는데, 각 Agent, Metrics, Admission Webhook의 역할을 합니다.

$ kubectl get pod -n keda
NAME                                                   READY   STATUS    RESTARTS     AGE
pod/keda-admission-webhooks-86cffccbf5-nq7kw           1/1     Running   0            4m11s
pod/keda-operator-6bdffdc78-zrhmg                      1/1     Running   1 (4m ago)   4m11s
pod/keda-operator-metrics-apiserver-74d844d769-2rbqk   1/1     Running   0            4m11s

 

공식 문서를 보면 각 컴포넌트에 해당하는 역할을 확인하실 수 있습니다.

https://keda.sh/docs/2.10/concepts/#how-keda-works

  1. Agent — KEDA activates and deactivates Kubernetes Deployments to scale to and from zero on no events. This is one of the primary roles of the keda-operator container that runs when you install KEDA.
  2. Metrics — KEDA acts as a Kubernetes metrics server that exposes rich event data like queue length or stream lag to the Horizontal Pod Autoscaler to drive scale out. It is up to the Deployment to consume the events directly from the source. This preserves rich event integration and enables gestures like completing or abandoning queue messages to work out of the box. The metric serving is the primary role of the keda-operator-metrics-apiserver container that runs when you install KEDA.
  3. Admission Webhooks - Automatically validate resource changes to prevent misconfiguration and enforce best practices by using an admission controller. As an example, it will prevent multiple ScaledObjects to target the same scale target.

 

그리고 생성된 CRD를 확인해보면 아래와 같은 CRD가 생성된 것을 알 수 있습니다.

$ kubectl get crd | grep keda
cloudeventsources.eventing.keda.sh           2025-03-06T13:23:51Z
clustercloudeventsources.eventing.keda.sh    2025-03-06T13:23:51Z
clustertriggerauthentications.keda.sh        2025-03-06T13:23:51Z
scaledjobs.keda.sh                           2025-03-06T13:23:53Z
scaledobjects.keda.sh                        2025-03-06T13:23:51Z
triggerauthentications.keda.sh               2025-03-06T13:23:51Z

 

이 중 ScaledObjects 이 중요한 역할을 합니다. 이는 Event Source(예를 들어, Rabbit MQ)와 쿠버네티스 리소스(예를 들어, Deployment) 간의 의도하는 맵핑(desired mapping)을 나타냅니다.

아래 실습에 사용되는 명세를 살펴보면, cron을 바탕으로 트리거triggers되며, php-apache라는 Deployment를 대상 지정scaleTargetRef하고, spec에 HPA 오브젝트에 필요한 값이나 스케일링 속성을 지정합니다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron-scaled
spec:
  minReplicaCount: 0
  maxReplicaCount: 2
  pollingInterval: 30
  cooldownPeriod: 300
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 00,15,30,45 * * * *
      end: 05,20,35,50 * * * *
      desiredReplicas: "1"

 

참고로 scaledObject를 생성하면 HPA도 같이 생성이 됩니다. 아래에서 다시 살펴보겠습니다.

이제 샘플 애플리케이션과 ScaledObject를 생성합니다.

# keda 네임스페이스에 디플로이먼트 생성
cat << EOF > php-apache.yaml
apiVersion: apps/v1
kind: Deployment
metadata: 
  name: php-apache
spec: 
  selector: 
    matchLabels: 
      run: php-apache
  template: 
    metadata: 
      labels: 
        run: php-apache
    spec: 
      containers: 
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports: 
        - containerPort: 80
        resources: 
          limits: 
            cpu: 500m
          requests: 
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata: 
  name: php-apache
  labels: 
    run: php-apache
spec: 
  ports: 
  - port: 80
  selector: 
    run: php-apache
EOF

kubectl apply -f php-apache.yaml -n keda
kubectl get pod -n keda
...
php-apache-d87b7ff46-bbp8c                         0/1     ContainerCreating   0               3s

# ScaledObject 정책 생성 : cron
cat <<EOT > keda-cron.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron-scaled
spec:
  minReplicaCount: 0
  maxReplicaCount: 2  # Specifies the maximum number of replicas to scale up to (defaults to 100).
  pollingInterval: 30  # Specifies how often KEDA should check for scaling events
  cooldownPeriod: 300  # Specifies the cool-down period in seconds after a scaling event
  scaleTargetRef:  # Identifies the Kubernetes deployment or other resource that should be scaled.
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:  # Defines the specific configuration for your chosen scaler, including any required parameters or settings
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 00,15,30,45 * * * *
      end: 05,20,35,50 * * * *
      desiredReplicas: "1"
EOT
kubectl apply -f keda-cron.yaml -n keda

# 모니터링
kubectl get ScaledObject,hpa,pod -n keda
kubectl get ScaledObject -n keda -w

# HPA 확인 -> external 유형으로 생성되어 있음
 kubectl get hpa -o jsonpath="{.items[0].spec}" -n keda | jq
{
  "maxReplicas": 2,
  "metrics": [
    {
      "external": {
        "metric": {
          "name": "s0-cron-Asia-Seoul-00,15,30,45xxxx-05,20,35,50xxxx",
          "selector": {
            "matchLabels": {
              "scaledobject.keda.sh/name": "php-apache-cron-scaled"
            }
          }
        },
        "target": {
          "averageValue": "1",
          "type": "AverageValue"
        }
      },
      "type": "External"
    }
  ],
  "minReplicas": 1,
  "scaleTargetRef": {
    "apiVersion": "apps/v1",
    "kind": "Deployment",
    "name": "php-apache"
  }
}

 

이 예시는 cron을 통해서 00, 15, 30, 45분에 desiredReplicas에 지정한 1개의 파드가 증가하고, 이후 05, 20, 35, 40에 minReplicaCount에 지정된 0개로 파드가 줄어 듭니다. (이 예제에서 maxReplicaCount는 큰 의미가 없지만 생성되는 HPA 오브젝트에서 사용되기 때문에 작성이 필요함)

 

결과를 아래와 같이 확인할 수 있습니다. 다만 테스트를 해보면 Cron으로 정의한 45분(start), 50분(end)에 정시에 트리거가 되는 것은 아닌 것으로 확인되기 때문에 정확도에 대해서는 기대치를 다소 낮춰야 할 것 같습니다.

# 이전 시점
Thu Mar  6 22:44:53 KST 2025
NAME                                          SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
scaledobject.keda.sh/php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                12m

NAME                                                                  REFERENCE               TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/keda-hpa-php-apache-cron-scaled   Deployment/php-apache   <unknown>/1 (avg)   1         2         0          12m

NAME                                                   READY   STATUS    RESTARTS      AGE
pod/keda-admission-webhooks-86cffccbf5-nq7kw           1/1     Running   0             21m
pod/keda-operator-6bdffdc78-zrhmg                      1/1     Running   1 (20m ago)   21m
pod/keda-operator-metrics-apiserver-74d844d769-2rbqk   1/1     Running   0             21m

# 45분 이후
Thu Mar  6 22:45:29 KST 2025
NAME                                          SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
scaledobject.keda.sh/php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True     False      Unknown                                12m

NAME                                                                  REFERENCE               TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/keda-hpa-php-apache-cron-scaled   Deployment/php-apache   <unknown>/1 (avg)   1         2         0          12m

NAME                                                   READY   STATUS    RESTARTS      AGE
pod/keda-admission-webhooks-86cffccbf5-nq7kw           1/1     Running   0             21m
pod/keda-operator-6bdffdc78-zrhmg                      1/1     Running   1 (21m ago)   21m
pod/keda-operator-metrics-apiserver-74d844d769-2rbqk   1/1     Running   0             21m
pod/php-apache-d87b7ff46-gblgb                         1/1     Running   0             9s


# ScaledObject 의 ACTIVE 상태가 45분 시점 True로 변경됨
kubectl get ScaledObject -n keda -w
NAME                     SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                2m30s
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                7m
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                12m
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True     False      Unknown                                12m
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True     False      Unknown                                13m
...

# 40분 쯤에 ScaleTarget을 minReplicaCount로 변경 함
kubectl logs -f -n keda keda-operator-6bdffdc78-zrhmg
...
2025-03-06T13:39:52Z    INFO    scaleexecutor   Successfully set ScaleTarget replicas count to ScaledObject minReplicaCount     {"scaledobject.Name": "php-apache-cron-scaled", "scaledObject.Namespace": "keda", "scaleTarget.Name": "php-apache", "Original Replicas Count": 1, "New Replicas Count": 0}

# 45분 쯤에 keda-operator에서 ScaleTarget을 업데이트 함
kubectl logs -f -n keda keda-operator-6bdffdc78-zrhmg
...
2025-03-06T13:45:22Z    INFO    scaleexecutor   Successfully updated ScaleTarget        {"scaledobject.Name": "php-apache-cron-scaled", "scaledObject.Namespace": "keda", "scaleTarget.Name": "php-apache", "Original Replicas Count": 0, "New Replicas Count": 1}

# 54분에 ScaleTarget을 minReplicaCount로 변경 함
2025-03-06T13:54:52Z    INFO    scaleexecutor   Successfully set ScaleTarget replicas count to ScaledObject minReplicaCount     {"scaledobject.Name": "php-apache-cron-scaled", "scaledObject.Namespace": "keda", "scaleTarget.Name": "php-apache", "Original Replicas Count": 1, "New Replicas Count": 0}

 

참고로 Grafana에서 아래의 json으로 대시보드를 만들어 모니터링 할 수 있습니다.

https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json

 

실습을 마무리하고 생성된 리소스를 삭제합니다.

# KEDA 및 deployment 등 삭제
kubectl delete ScaledObject -n keda php-apache-cron-scaled && kubectl delete deploy php-apache -n keda && helm uninstall keda -n keda
kubectl delete namespace keda

 

 

6. VPA(Vertical Pod Autoscaler)

VPA는 대상이 되는 리소스의 과거 사용률을 기반으로 대상의 컨테이너 스펙의 request와 limits 자체를 변경하는 오토스케일러 입니다.

VPA의 동작과정을 아래에서 설명하고 있습니다. 단, 아래 설명은 Update Mode: Auto이므로 파드가 재생성되는 과정까지를 설명하고 있습니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

그림에서 표시된 바와 같이 VPA에는 아래와 같은 주요 컴포넌트가 있습니다.

참고: https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/docs/components.md

  • Recommender - monitors the current and past resource consumption and, based on it, provides recommended values for the containers' cpu and memory requests.
  • Updater - checks which of the managed pods have correct resources set and, if not, kills them so that they can be recreated by their controllers with the updated requests.
  • Admission Controller - sets the correct resource requests on new pods (either just created or recreated by their controller due to Updater's activity).

VPA에서 추천값을 계산하는 방식은 과거 사용률을 바탕으로 기준값을 정하고 여기에 Margin을 더하여 결정합니다. 상세한 내용은 아래 링크를 참고 부탁드립니다.

https://devocean.sk.com/blog/techBoardDetail.do?ID=164786

 

또한 VPA에는 updateMode를 설정할 수 있는데 Auto(Default), Recreate, Initial, Off 입니다.

이해하기로는 현 시점 Auto와 Recreate은 동일하게 동작을 합니다. 즉 생성되는 파드와 실행 중인 파드를 변경하고, 필요하면 현재 실행 중인 파드를 재시작 합니다. 다만 이후 in-place resource resize(Kubernetes 1.27, Alpha)가 가능해지면, Auto 모드에서는 재시작을 하지않고, 현재 파드의 리소스를 수정만하는 방식으로 사용될 수 있습니다.

Initial은 파드가 생성되는 시점에만 추천 값을 적용하는 모드이고, Off는 추천 값을 VPA 오브젝트를 통해서 확인만 가능한 모드입니다.

 

상세한 설명은 아래 문서를 참고 부탁드립니다.

https://learn.microsoft.com/en-us/azure/aks/vertical-pod-autoscaler#vpa-object-operation-modes

 

이제 VPA를 설치하도록 하겠습니다.

참고: https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/docs/installation.md#install-command

# VPA 코드 다운로드
git clone https://github.com/kubernetes/autoscaler.git


# 만약 openssl 이 1.1.1 이하 버전인 경우, 1.1.1 이상 버전으로 업그레이드 필요함
openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017

# (필요한 경우) 1.0 제거 
yum remove openssl -y

# (필요한 경우) openssl 1.1.1 이상 버전 확인 
yum install openssl11 -y

# 스크립트 파일 내에 openssl11 수정 (commit 까지 해야 수행됨)
cd autoscaler/vertical-pod-autoscaler/
sed -i 's/openssl/openssl11/g' pkg/admission-controller/gencerts.sh
git status
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
git add .
git commit -m "openssl version modify"

# Deploy the Vertical Pod Autoscaler to your cluster with the following command.
watch -d kubectl get pod -n kube-system
./hack/vpa-up.sh

# (필요한 경우) openssl 관련 에러가 발생한다면 재실행!
sed -i 's/openssl/openssl11/g' pkg/admission-controller/gencerts.sh
./hack/vpa-up.sh

# 설치 후 확인
kubectl get pod -n kube-system |grep vpa
vpa-admission-controller-659c978dcd-zwn24     1/1     Running   0          106s
vpa-recommender-9bb6d98b7-gjqc7               1/1     Running   0          112s
vpa-updater-68db47986b-jqnnh                  1/1     Running   0          116s

# mutating webhook을 통해서 파드 생성 시점 설정을 변경함
kubectl get mutatingwebhookconfigurations vpa-webhook-config

NAME                 WEBHOOKS   AGE
vpa-webhook-config   1          101s

kubectl get mutatingwebhookconfigurations vpa-webhook-config -o json | jq
{
  "apiVersion": "admissionregistration.k8s.io/v1",
  "kind": "MutatingWebhookConfiguration",
  "metadata": {
    "creationTimestamp": "2025-03-06T14:14:31Z",
    "generation": 1,
    "name": "vpa-webhook-config",
    "resourceVersion": "22754",
    "uid": "03b88fcf-c1ff-4079-b33c-38c998829d50"
  },
  "webhooks": [
    {
      "admissionReviewVersions": [
        "v1"
      ],
      "clientConfig": {
        "caBundle": "<생략>",
        "service": {
          "name": "vpa-webhook",
          "namespace": "kube-system",
          "port": 443
        }
      },
      "failurePolicy": "Ignore",
      "matchPolicy": "Equivalent",
      "name": "vpa.k8s.io",
      "namespaceSelector": {
        "matchExpressions": [
          {
            "key": "kubernetes.io/metadata.name",
            "operator": "NotIn",
            "values": [
              ""
            ]
          }
        ]
      },
      "objectSelector": {},
      "reinvocationPolicy": "Never",
      "rules": [
        {
          "apiGroups": [
            ""
          ],
          "apiVersions": [
            "v1"
          ],
          "operations": [
            "CREATE"
          ],
          "resources": [
            "pods"
          ],
          "scope": "*"
        },
        {
          "apiGroups": [
            "autoscaling.k8s.io"
          ],
          "apiVersions": [
            "*"
          ],
          "operations": [
            "CREATE",
            "UPDATE"
          ],
          "resources": [
            "verticalpodautoscalers"
          ],
          "scope": "*"
        }
      ],
      "sideEffects": "None",
      "timeoutSeconds": 30
    }
  ]
}

 

설치를 마치면 VPA와 관련된 파드와 CRD를 확인하실 수 있습니다.

kubectl get crd | grep autoscaling
verticalpodautoscalercheckpoints.autoscaling.k8s.io   2025-03-06T14:13:55Z
verticalpodautoscalers.autoscaling.k8s.io             2025-03-06T14:13:55Z

 

아래와 같이 샘플 예제를 배포하여 VPA를 테스트 하겠습니다.

# 공식 예제 배포 (autoscaler/vertical-pod-autoscaler 위치에서 수행)
kubectl apply -f examples/hamster.yaml && kubectl get vpa -w
verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa created
deployment.apps/hamster created
NAME          MODE   CPU   MEM   PROVIDED   AGE
hamster-vpa   Auto                          3s
hamster-vpa   Auto   511m   262144k   True       44s # VPA의 추천 값

# 파드 리소스 Requestes 확인
kubectl describe pod | grep Requests: -A2
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        100m
      memory:     50Mi      


# VPA에 의해 기존 파드 삭제되고 신규 파드가 생성됨
kubectl get events --sort-by=".metadata.creationTimestamp" | grep VPA
19s         Normal   EvictedByVPA        pod/hamster-598b78f579-8gjfh        Pod was evicted by VPA Updater to apply resource recommendation.
19s         Normal   EvictedPod          verticalpodautoscaler/hamster-vpa   VPA Updater evicted Pod hamster-598b78f579-8gjfh to apply resource recommendation.

# 파드 리소스 Requestes 가 변경 됨
kubectl describe pod | grep Requests: -A2
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        511m
      memory:     262144k

 

실습을 마무리하고 아래와 같이 리소스를 삭제하겠습니다.

kubectl delete -f examples/hamster.yaml && ./hack/vpa-down.sh

 

 

마무리

생각보다 길이 길어져서 오토스케일링 개요, HPA, KEDA, VPA 까지만 이번 포스트에서 작성하고, CA, Karpenter, AKS의 오토스케일링, 스케일링 주의사항은 다음 포스트에서 이어서 작성하겠습니다.

'EKS' 카테고리의 다른 글

[6] EKS의 Security - EKS 인증/인가와 Pod IAM 권한 할당  (0) 2025.03.16
[5-2] EKS의 오토스케일링 Part2  (0) 2025.03.07
[4] EKS의 모니터링과 로깅  (0) 2025.03.01
[3-2] EKS 노드 그룹  (0) 2025.02.23
[3-1] EKS 스토리지 옵션  (0) 2025.02.23

+ Recent posts