ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [AEWS 3기] 5주차 - EKS Autoscaling
    AWS 2025. 3. 8. 22:49
    1. 실습환경 준비

    0.2. AWS LoadBalancer Controller, ExternalDNS, gp3 storageclass, kube-ops-view(Ingress) 설치

    • 설치
    • 확인

    0.3. 프로메테우스 & 그라파나(admin / prom-operator) 설치 : 대시보드 Import 17900 - Link

    Copy
    # repo 추가
    helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
    
    # 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하니, 4주차 실습과 다르게 PV/PVC 미사용
    cat < 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: prom-operator
      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
    kubectl df-pv
    
    # 프로메테우스 웹 접속
    echo -e "https://prometheus.$MyDomain"
    open "https://prometheus.$MyDomain" # macOS
    
    # 그라파나 웹 접속 : admin / prom-operator
    echo -e "https://grafana.$MyDomain"
    open "https://grafana.$MyDomain" # macOS
    
    #
    kubectl get targetgroupbindings.elbv2.k8s.aws -A
    
    
    # 상세 확인
    kubectl get pod -n monitoring -l app.kubernetes.io/name=kube-state-metrics
    kubectl describe pod -n monitoring -l app.kubernetes.io/name=kube-state-metrics
    ...
    Service Account:  kube-prometheus-stack-kube-state-metrics
    ...
        Args:
          --port=8080
          --resources=certificatesigningrequests,configmaps,cronjobs,daemonsets,deployments,endpoints,horizontalpodautoscalers,ingresses,jobs,leases,limitranges,mutatingwebhookconfigurations,namespaces,networkpolicies,nodes,persistentvolumeclaims,persistentvolumes,poddisruptionbudgets,pods,replicasets,replicationcontrollers,resourcequotas,secrets,services,statefulsets,storageclasses,validatingwebhookconfigurations,volumeattachments
          --custom-resource-state-config-file=/etc/customresourcestate/config.yaml
        ...
    Volumes:
      customresourcestate-config:
        Type:      ConfigMap (a volume populated by a ConfigMap)
        Name:      kube-prometheus-stack-kube-state-metrics-customresourcestate-config
        Optional:  false
    ...
    
    kubectl describe cm -n monitoring kube-prometheus-stack-kube-state-metrics-customresourcestate-config
    ...
    
    # 
    kubectl get clusterrole kube-prometheus-stack-kube-state-metrics
    kubectl describe clusterrole kube-prometheus-stack-kube-state-metrics
    kubectl describe clusterrole kube-prometheus-stack-kube-state-metrics | grep verticalpodautoscalers
      verticalpodautoscalers.autoscaling.k8s.io                     []                 []              [list watch]
    
    • (옵션) 4주차 노션 확인하여 17900 대시보드에 PromQL/Variables 수정 해둘 것

    0.4. EKS Node Viewer : 노드 할당 가능 용량요청 request 리소스 표시, 실제 파드 리소스 사용량 X - 링크

    • 동작
    • 설치
    • 사용
    1. HPA

    1.2. HPA custom metrics - 링크

    참고: https://medium.com/@api.test9989/aews-5주차-eks-autoscaling-4f68154a02a8

    Copy
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: php-apache
    spec:
      minReplicas: 1
      maxReplicas: 10
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: php-apache
      metrics:
      - type: Resource
        resource:
          name: cpu
          target:
            type: Utilization
            averageUtilization: 50
      - type: Pods
        pods:
          metric:
            name: packets-per-second
          target:
            type: AverageValue
            averageValue: 1k
      - type: Object
        object:
          metric:
            name: requests-per-second
          describedObject:
            apiVersion: networking.k8s.io/v1
            kind: Ingress
            name: main-route
          target:
            type: Value
            value: 10k

    HPA V2에서부터는 custom metric을 지원해 다양한 기준들로 HPA 설정을 진행할수있습니다.

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

    1.2.1. 자원 메트릭에 대한 퍼센트 대신 값 지정

    이 기능은 CPU와 같은 자원의 사용량에 대해 퍼센트 값을 사용하는 대신, 실제 값으로 명시할 수 있게 해줍니다. 더욱 세밀한 자원 사용률을 기반으로 HPA를 조정 가능해집니다.

    예를 들어, 아래는 CPU 사용률을 값으로 명시한 HPA 설정의 예입니다:

    Copy
    metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: AverageValue
          averageValue: 200m

    이 설정은 각 Pod가 평균적으로 200 mill의 CPU를 사용할 때까지 Pod의 수를 늘립니다.

    1.2.2. 사용자 정의 메트릭

    HPA에서 사용자 정의 메트릭을 지원하는 것은 매우 중요합니다. 이를 통해 애플리케이션에 특화된 메트릭에 기반하여 자동 스케일링을 수행할 수 있습니다. 두 가지 주요 사용자 정의 메트릭 타입이 있습니다: Pod 메트릭과 Object 메트릭.

    • Pod 메트릭: 이는 각 Pod에 대한 메트릭을 설명합니다. 이 메트릭은 Pod 간의 평균을 내고, 그 평균값을 대상 값과 비교하여 레플리카 수를 조정합니다. 예를 들어, ‘packets-per-second’라는 메트릭이 있다면, 그 값을 특정 임계치와 비교하여 Pod의 수를 조절할 수 있습니다.
    • Object 메트릭: 오브젝트 메트릭을 이용해 HPA를 구성할 때, 특정 오브젝트(예: Ingress, Service 등)의 메트릭을 이용합니다. 이는 ‘Value’ 또는 ‘AverageValue’ target 타입을 지원하며, 이들은 각각 API에서 반환된 메트릭 값을 직접적으로 비교하거나, 메트릭 값을 Pod의 수로 나눈 값을 대상 값과 비교하는 방식으로 동작합니다.
    Copy
    type: Object
    object:
      metric:
        name: requests-per-second
      describedObject:
        apiVersion: networking.k8s.io/v1
        kind: Ingress
        name: main-route
      target:
        type: Value
        value: 2k

    예를 들어, “requests-per-second”라는 오브젝트 메트릭이 있다고 가정해보겠습니다. 이 메트릭은 Ingress 오브젝트 ‘main-route’에 연관되어 있지만, 이 메트릭 값은 실제로는 해당 Ingress 오브젝트에 도달하는 트래픽의 요청 수를 나타냅니다. 이런 값은 일반적으로 로드 밸런서나 인그레스 컨트롤러 같은 외부 시스템에서 모니터링하고 수집합니다.

    코드에 메트릭이 설정된 대상 값이 2k라는 것은, 초당 2000개의 요청이 이 Ingress 오브젝트에 도달했을 때, HPA가 스케일링을 수행하도록 설정되어 있다는 것을 의미합니다.

    이 설정은, Ingress ‘main-route’에 도달하는 트래픽이 많아지면, 자동으로 더 많은 Pod를 생성하여 트래픽을 처리하도록 하기 위한 것입니다.

    HPA는 이 설정을 보고 Kubernetes API 또는 외부 메트릭 시스템으로부터 해당 메트릭의 현재 값을 가져옵니다. 가져온 현재 메트릭 값과 설정된 대상 값을 비교하고, 필요에 따라 Pod의 수를 증가 또는 감소 시킵니다.

    1.2.3. 커스텀 메트릭 블록

    HorizontalPodAutoscaler 연습

    HPA는 여러 메트릭 블록을 포함할 수 있으며, 이를 통해 여러 메트릭을 기반으로 스케일링 결정을 내릴 수 있습니다. HPA는 각 메트릭에 대해 제안된 레플리카 수를 계산하고, 그중 가장 높은 레플리카 수를 선택합니다. 이를 통해 다양한 메트릭을 종합적으로 고려하여 더욱 정교한 스케일링 결정을 내릴 수 있습니다.

    Copy
    # External type metrics
    metrics:
    - type: External
      external:
        metric:
          name: queue_messages_ready
          selector:
            matchLabels:
              queue: "worker_tasks"
        target:
          type: AverageValue
          averageValue: 30
    
    # Object type metrics
    metrics:
    - type: Object
      object:
        describedObject:
          kind: Pod
          name: nginx-pod-xyz
          apiVersion: v1
        metric:
          name: nginx_http_requests_total
          selector: {}
          target:
            type: Value
            value: "1000"  # Scale when the total number of requests exceeds 1000

    CRD처럼 커스텀 메트릭에 의해 HPA 동작 조건을 설정할 수 있습니다. message queue나 특정 상황의 아키텍처에서 서비스 개발을 할 때 고려해볼 만한 방식의 HPA 조건 설정입니다.

    위에서 보는 것 처럼 hpa의 custome metrics block에서 type으로 object type을 사용할때 .metric.target.type으로 Value, AverageValue 두가지를 사용 할 수 있으며, 두개의 차이점은 아래와 같다.

    Feature
    Value
    AverageValue
    Metric Calculation
    Compares the exact value of the metric for a single object (e.g., pod) to a specified target.
    Compares the average value of the metric across multiple objects (e.g., pods) to a specified target.
    Usage
    Suitable when you want scaling based on a specific, absolute metric value for an individual pod or resource.
    Suitable when you want scaling based on the average of the metric value across multiple pods or replicas.
    Scaling Behavior
    Triggers scaling based on the exact value of the metric (e.g., nginx_http_requests_total exceeding a specific count).
    Triggers scaling based on the average value of the metric across all selected objects (e.g., average requests across all pods).
    Example Scenario
    Scale based on the total number of requests a pod handles.
    Scale based on the average number of requests across all pods in the deployment.
    • Value:
    • AverageValue:

    1.2.4. Behavior

    autoscaling/v2 부터 (beta는 제외) behavior 필드라는것이 새로 생겼습니다. 스케일 업 동작 / 스케일 다운 동작을 별도로 구성할 수 있습니다. 우선 간단한 예제코드를 보면서 이해를 해보겠습니다.

    Copy
    behavior:
      scaleDown:
        policies:
        - type: Pods
          value: 4
          periodSeconds: 60
        - type: Percent
          value: 10
          periodSeconds: 60

    periodSeconds 는 폴리시가 참(true)으로 유지되어야 하는 기간을 나타냅니다. 첫 번째는 (Pods)가 1분 내에 최대 4개의 Replicas를 스케일 다운할 수 있도록 허용하는 정책입니다. 두 번째는 현재 Replicas의 최대 10%를 1분 내에 스케일 다운할 수 있도록 허용하는 정책입니다.

    이제 조금 더 종합적으로 여러가지 변수들을 담았습니다.

    Copy
    behavior:
      scaleDown:
        stabilizationWindowSeconds: 300
        policies:
        - type: Pods
          value: 4
          periodSeconds: 60
        - type: Percent
          value: 10
          periodSeconds: 60
      scaleUp:
        stabilizationWindowSeconds: 0
        policies:
        - type: Percent
          value: 100
          periodSeconds: 15
        - type: Pods
          value: 4
          periodSeconds: 15
        selectPolicy: Max

    behavior 섹션에는 scaleUp과 scaleDown 두 가지 하위 섹션이 있습니다.

    • scaleUp: 파드의 수를 늘리는 방식을 제어합니다.
    • scaleDown: 파드의 수를 줄이는 방식을 제어합니다.

    각 하위 섹션에는 다음과 같은 필드가 있습니다:

    • policies: 스케일링 정책을 정의하는 목록입니다. 정책은 typevalueperiodSeconds 세 가지 필드로 구성됩니다.
    • typePods 또는 Percent 중 하나를 지정합니다. Pods는 정수의 수를, Percent는 현재 replica 수의 백분율을 나타냅니다.
    • value: 스케일링 기간 동안 최대로 늘릴 수 있는 파드의 수를 나타냅니다.
    • periodSeconds: 스케일링 정책이 적용되는 기간을 초 단위로 나타냅니다.
    • selectPolicyMaxMinDisabled 중 하나를 선택합니다. Max는 주어진 시간 동안 가능한 최대 스케일링을, Min은 가능한 최소 스케일링을 나타냅니다. Disabled는 스케일링을 사용하지 않도록 설정합니다.
    • stabilizationWindowSeconds: 스케일링 동작이 안정화되는 시간을 초 단위로 나타냅니다.

    각 옵션에따라 조절한 스케일 전략을 미리 구성해둬서 최적화된 pod 확장을 구성해 둘 수 있습니다.

    이제 이 Custom Metric 을 쓰기 위해선 prometheus-adapter를 설치해 custom metric 에 대한 수집을 진행합니다. (기존의 CPU / Memory 를 metrics server에서 하던것과 동일한 맥락입니다)

    ingress count를 기준으로 proemtheus 에서 수집하기위해 다음과 같은 proemtheus rules를 넣은상태로 프로메테우스를 프로비저닝합니다

    Copy
    apiVersion: monitoring.coreos.com/v1
    kind: PrometheusRule
    metadata:
      name: nginx-ingress-rules
      namespace: monitoring
    spec:
      groups:
      - name: nginx-ingress
        rules:
        - record: nginx_ingress_http_requests_total
          expr: sum(rate(nginx_ingress_controller_requests{status=~"2.."}[5m])) by (namespace)

    이 설정은 성공적인 HTTP 요청(2XX 상태 코드)의 총 수를 5분 간격으로 계산하고, nginx_ingress_http_requests_total이라는 이름으로 이를 기록합니다. 이 메트릭은 특정 네임스페이스로 그룹화됩니다.

    프로메테우스에서 정상적으로 메트릭을 수집할수있는 상태가 된다면, hpa v2의 custom metric을 관리해주는 prometheus adapter의 설정도 같이 변경해줍니다.

    Copy
    rules:
      custom:
      - seriesQuery: 'nginx_ingress_http_requests_total{namespace!="",service!=""}'
        resources:
          template: <<.Resource>>
        name:
          matches: "^(.*)_total"
          as: "${1}_per_second"
        metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,service=~"<<.Service>>"}[2m])) by (<<.GroupBy>>)

    nginx_ingress_http_requests_total 메트릭을 기반으로, 서비스 이름과 일치하는 레이블을 가진 메트릭의 속도(rate)를 계산합니다. 여기서는 2분 동안의 데이터를 사용하여 초당 요청 수를 나타내는 메트릭을 생성합니다.

    Copy
    kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | . jq | grep "nginx_ingress_http_requests_total"

    명령어를 통해 설정된 메트릭이 잘 수집되고있는지 확인합니다. 이렇게 prometheus rule 과 adapater 설정을 변경하면 hpa에서 설정가능한 custom metric에 대한 설정이 완료됩니다.

    1.2.5. Prometheus-adapter & HPA Custom Metric 실습

    참고: https://joeunvit.tistory.com/14#Additional Custom %3A prometheus-adapter API(custom.metrics.k8s.io) %2B HPA-1-1

    실제로, prometheus-adapter를 설치하여 custom metric을 정의하고 해당 custom metric을 통해서 hpa의 autoscaling 기능을 활용해본다.

    이 예제에서는 prometheus의 serviceMonitor 등록을 통해 nginx pod의 nginx_http_request_total을 수집하고, 해당 metric을 prometheus-adapter를 통해 custom.metrics.k8s.io(=custom metric)으로 등록하고, 해당 custom metric을 통해 hpa v2를 생성해 pod autoscaling이 정상적으로 진행되는지 확인한다. 정리해보자면 아래와 같다.

    1. bitnami/nginx helm chart를 배포하고 serviceMonitor를 등록(nginx_http_request_total prometheus에서 scapre)
    2. prometheus-adapter helm chart 배포 및 custom metrics 정의
    3. custom metric을 통해 hpa v2 생성
    4. hpa 동작 확인
    • bitnami.nginx helm chart 배포 및 serviceMonitor 등록
    • prometheus-adapter helm chart 배포 및 custom metrics 정의
    • custom metrics를 이용하여 hpa 생성
    1. KEDA

    2.2. KEDA with Helm : 특정 이벤트(cron 등)기반의 파드 오토 스케일링 - Chart , Grafana , Cron , SQS_Scale , aws-sqs-queue

    Copy
    # 설치 전 기존 metrics-server 제공 Metris API 확인
    kubectl get --raw "/apis/metrics.k8s.io" -v=6 | jq
    kubectl get --raw "/apis/metrics.k8s.io" | jq
    {
      "kind": "APIGroup",
      "apiVersion": "v1",
      "name": "metrics.k8s.io",
      ...
    
    
    # KEDA 설치 : serviceMonitor 만으로도 충분할듯..
    cat < 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
    
    # KEDA 설치 확인
    kubectl get crd | grep keda
    kubectl get all -n keda
    kubectl get validatingwebhookconfigurations keda-admission -o yaml
    kubectl get podmonitor,servicemonitors -n keda
    kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml
    
    # CPU/Mem은 기존 metrics-server 의존하여, KEDA metrics-server는 외부 이벤트 소스(Scaler) 메트릭을 노출 
    ## https://keda.sh/docs/2.16/operate/metrics-server/
    kubectl get pod -n keda -l app=keda-operator-metrics-apiserver
    
    # Querying metrics exposed by KEDA Metrics Server
    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 네임스페이스에 디플로이먼트 생성
    kubectl apply -f php-apache.yaml -n keda
    kubectl get pod -n keda
    
    # ScaledObject 정책 생성 : cron
    cat < 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
    
    # 그라파나 대시보드 추가 : 대시보드 상단에 namespace : keda 로 변경하기!
    # KEDA 대시보드 Import : https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json
    
    # 모니터링
    watch -d 'kubectl get ScaledObject,hpa,pod -n keda'
    kubectl get ScaledObject -w
    
    # 확인
    kubectl get ScaledObject,hpa,pod -n keda
    
    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                                22m
    
    ## 여기서 keda의 cron triggers를 통해서 생성된 hpa는 current 값을 계산 할 수 없으므로 unkonwn으로 표시된다.
    NAME                                                                  REFERENCE               TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
    horizontalpodautoscaler.autoscaling/keda-hpa-php-apache-cron-scaled   Deployment/php-apache   /1 (avg)   1         2         0          22m
    
    NAME                                                   READY   STATUS    RESTARTS       AGE
    pod/keda-admission-webhooks-86cffccbf5-wp46z           1/1     Running   0              102m
    pod/keda-operator-6bdffdc78-5tc4r                      1/1     Running   1 (102m ago)   102m
    pod/keda-operator-metrics-apiserver-74d844d769-fdt82   1/1     Running   0              102m
    pod/php-apache-d87b7ff46-6v7fq                         1/1     Running   0              27s
    
    
    kubectl get hpa -o jsonpath="{.items[0].spec}" -n keda | jq
    ...
    "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"
        }
    
    # 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
    • ScaledObject cron type triggers에서 hpa가 unkonwn인 이유는?
    • [도전과제2] KEDA 활용 : Karpenter + KEDA로 특정 시간에 AutoScaling - 링크 , Youtube , Airflow , Blog
    • [도전과제] KEDA HTTP Add-on 사용해보기 - Docs , Github
    • [도전과제] AWS EKS Addon 에 CoreDNS 에 AutoScaling 를 적용해보자 - Docs
    • [도전과제] 기존 HPA를 유지하면서, KEDA 로 마이그레이션 - Blog
    1. VPA

    3.2. VPC 실습

    Copy
    # [운영서버 EC2] 코드 다운로드
    git clone https://github.com/kubernetes/autoscaler.git # userdata 로 설치 되어 있음
    cd ~/autoscaler/vertical-pod-autoscaler/
    tree hack
    
    # openssl 버전 확인
    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 version
    OpenSSL 1.1.1g FIPS  21 Apr 2020
    
    # 스크립트파일내에 openssl11 수정
    sed -i 's/openssl/openssl11/g' ~/autoscaler/vertical-pod-autoscaler/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
    cat hack/vpa-up.sh
    ./hack/vpa-up.sh
    
    # VPA가 정상적으로 설치되지 않으면 재실행!
    sed -i 's/openssl/openssl11/g' ~/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/gencerts.sh
    ./hack/vpa-up.sh
    
    kubectl get crd | grep autoscaling
    kubectl get mutatingwebhookconfigurations vpa-webhook-config
    kubectl get mutatingwebhookconfigurations vpa-webhook-config -o json | jq
    • 공식 예제 : pod가 실행되면 약 2~3분 뒤에 pod resource.reqeust가 VPA에 의해 수정 - 링크
    Copy
    # 모니터링
    Every 2.0s: kubectl top pod;echo ----------------------;kubectl describe pod | grep Requests: -A2                                                                          Sat Mar  8 00:09:54 2025
    
    NAME                         CPU(cores)   MEMORY(bytes)
    hamster-598b78f579-hmc7b     409m         0Mi
    hamster-598b78f579-m75h4     408m         0Mi
    php-apache-d87b7ff46-x9ztz   1m           8Mi
    ----------------------
        Requests:
          cpu:        476m # vpa에 새롭게 생성된 hamster pod
          memory:     262144k
    --
        Requests:
          cpu:        100m # 기존 hamster pod
          memory:     50Mi
    --
        Requests:
          cpu:        476m # vpa에 새롭게 생성된 hamster pod
          memory:     262144k
    --
        Requests:
          cpu:        200m
        Environment:  <none>
    
    
    verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa created
    deployment.apps/hamster created
    NAME          MODE   CPU   MEM   PROVIDED   AGE
    hamster-vpa                                 1s
    hamster-vpa          476m   262144k   True       31s
    
    # 공식 예제 배포
    cd ~/autoscaler/vertical-pod-autoscaler/
    cat examples/hamster.yaml
    kubectl apply -f examples/hamster.yaml && kubectl get vpa -w
    
    # 파드 리소스 Requestes 확인
    kubectl describe pod | grep Requests: -A2
        Requests:
          cpu:        100m
          memory:     50Mi
    --
        Requests:
          cpu:        587m
          memory:     262144k
    --
        Requests:
          cpu:        587m
          memory:     262144k
    
    # VPA에 의해 기존 파드 삭제되고 신규 파드가 생성됨
    kubectl get events --sort-by=".metadata.creationTimestamp" | grep VPA
    
    37s         Normal   EvictedByVPA        pod/hamster-598b78f579-pggcg        Pod was evicted by VPA Updater to apply resource recommendation.
    37s         Normal   EvictedPod          verticalpodautoscaler/hamster-vpa   VPA Updater evicted Pod hamster-598b78f579-pggcg to apply resource recommendation.
    • 삭제: kubectl delete -f examples/hamster.yaml && cd ~/autoscaler/vertical-pod-autoscaler/ && ./hack/vpa-down.sh

    3.3. KRR : Prometheus-based Kubernetes Resource Recommendations - 링크 & Youtube - 링크 ⇒ Krr을 통한 최적화 작업 경험 - Blog

    • KRR(Kubernetes Resource Recommender)을 사용하면 실제 사용량 기준으로 Request와 Limit 사용량을 추천
    • Difference with Kubernetes VPA
    • [도전과제3] k8s 1.27: In-place Resource Resize for Kubernetes Pods (alpha) pod재실행안하면서 resource변경 - Link1 Link2
    1. CAS - Cluster Autoscaler

    4.2. Cluster Autoscaler(CAS) 설정 - Workshop , Helm

    설정 전 확인

    Copy
    # EKS 노드에 이미 아래 tag가 들어가 있음
    # k8s.io/cluster-autoscaler/enabled : true
    # k8s.io/cluster-autoscaler/myeks : owned
    aws ec2 describe-instances  --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Reservations[*].Instances[*].Tags[*]" --output json | jq
    aws ec2 describe-instances  --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Reservations[*].Instances[*].Tags[*]" --output yaml
    ...
    - Key: k8s.io/cluster-autoscaler/myeks
          Value: owned
    - Key: k8s.io/cluster-autoscaler/enabled
          Value: 'true'
    ...
    태그가 많을 경우 2페이지 이상에서 확인 할 것!

    Cluster Autoscaler for AWS provides integration with Auto Scaling groups. It enables users to choose from four different options of deployment:

    • One Auto Scaling group
    • Multiple Auto Scaling groups
    • Auto-Discovery : Auto-Discovery is the preferred method to configure Cluster Autoscaler. Click here for more information.
    • Control-plane Node setup

    Cluster Autoscaler will attempt to determine the CPU, memory, and GPU resources provided by an Auto Scaling Group based on the instance type specified in its Launch Configuration or Launch Template.

    Copy
    # 현재 autoscaling(ASG) 정보 확인
    # aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='클러스터이름']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
    aws autoscaling describe-auto-scaling-groups \
        --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
        --output table
    -----------------------------------------------------------------
    |                   DescribeAutoScalingGroups                   |
    +------------------------------------------------+----+----+----+
    |  eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb  |  0 |  3 |  3 |
    +------------------------------------------------+----+----+----+
    
    # MaxSize 6개로 수정
    export ASG_NAME=$(aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].AutoScalingGroupName" --output text)
    aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 3 --desired-capacity 3 --max-size 6
    
    # 확인
    aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
    -----------------------------------------------------------------
    |                   DescribeAutoScalingGroups                   |
    +------------------------------------------------+----+----+----+
    |  eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb  |  3 |  6 |  3 |
    +------------------------------------------------+----+----+----+
    
    # 배포 : Deploy the Cluster Autoscaler (CAS)
    curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
    ...
                - ./cluster-autoscaler
                - --v=4
                - --stderrthreshold=info
                - --cloud-provider=aws
                - --skip-nodes-with-local-storage=false # 로컬 스토리지를 가진 노드를 autoscaler가 scale down할지 결정, false(가능!)
                - --expander=least-waste # 노드를 확장할 때 어떤 노드 그룹을 선택할지를 결정, least-waste는 리소스 낭비를 최소화하는 방식으로 새로운 노드를 선택.
                - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/
    ...
    
    sed -i -e "s||$CLUSTER_NAME|g" cluster-autoscaler-autodiscover.yaml
    kubectl apply -f cluster-autoscaler-autodiscover.yaml
    serviceaccount/cluster-autoscaler created
    clusterrole.rbac.authorization.k8s.io/cluster-autoscaler created
    role.rbac.authorization.k8s.io/cluster-autoscaler created
    clusterrolebinding.rbac.authorization.k8s.io/cluster-autoscaler created
    rolebinding.rbac.authorization.k8s.io/cluster-autoscaler created
    deployment.apps/cluster-autoscaler created
    
    # 확인
    kubectl get pod -n kube-system | grep cluster-autoscaler
    kubectl describe deployments.apps -n kube-system cluster-autoscaler
    kubectl describe deployments.apps -n kube-system cluster-autoscaler | grep node-group-auto-discovery
          --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/myeks
    
    # (옵션) cluster-autoscaler 파드가 동작하는 워커 노드가 퇴출(evict) 되지 않게 설정
    kubectl -n kube-system annotate deployment.apps/cluster-autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict="false"

    4.3. CAS 동작 확인

    Copy
    # 모니터링 
    kubectl get nodes -w
    while true; do kubectl get node; echo "------------------------------" ; date ; sleep 1; done
    while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------"; date; sleep 1; done
    
    # Deploy a Sample App
    # We will deploy an sample nginx application as a ReplicaSet of 1 Pod
    cat << EOF > nginx.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-to-scaleout
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            service: nginx
            app: nginx
        spec:
          containers:
          - image: nginx
            name: nginx-to-scaleout
            resources:
              limits:
                cpu: 500m
                memory: 512Mi
              requests:
                cpu: 500m
                memory: 512Mi
    EOF
    kubectl apply -f nginx.yaml
    kubectl get deployment/nginx-to-scaleout
    
    # Scale our ReplicaSet
    # Let’s scale out the replicaset to 15
    kubectl scale --replicas=15 deployment/nginx-to-scaleout && date
    
    deployment.apps/nginx-to-scaleout scaled
    Sat Mar  8 00:20:14 KST 2025
    
    # 확인
    kubectl get pods -l app=nginx -o wide --watch
    
    kubectl -n kube-system logs -f deployment/cluster-autoscaler
    
    I0307 15:20:19.360067       1 klogx.go:87] Pod default/nginx-to-scaleout-7cfb655fb5-xh6ls is unschedulable
    I0307 15:20:19.360070       1 klogx.go:87] Pod default/nginx-to-scaleout-7cfb655fb5-pvd6z is unschedulable
    I0307 15:20:19.360073       1 klogx.go:87] Pod default/nginx-to-scaleout-7cfb655fb5-p9qrl is unschedulable
    I0307 15:20:19.360260       1 scale_up.go:194] Upcoming 0 nodes
    I0307 15:20:19.360852       1 waste.go:55] Expanding Node Group eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb would waste 25.00% CPU, 59.87% Memory, 42.44% Blended
    I0307 15:20:19.360866       1 scale_up.go:282] Best option to resize: eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb
    I0307 15:20:19.360872       1 scale_up.go:286] Estimated 3 nodes needed in eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb # 현재 필요한 node 수 계산
    I0307 15:20:19.360889       1 scale_up.go:405] Final scale-up plan: [{eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb 3->6 (max: 6)}] # 6개로 node 수 확정 후 scale out
    I0307 15:20:19.360904       1 scale_up.go:608] Scale-up: setting group eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb size to 6 # scale out됨
    I0307 15:20:19.360947       1 auto_scaling_groups.go:248] Setting asg eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb size to 6
    I0307 15:20:19.361028       1 event_sink_logging_wrapper.go:48] Event(v1.ObjectReference{Kind:"ConfigMap", Namespace:"kube-system", Name:"cluster-autoscaler-status", UID:"0cb74c85-7760-47bc-8a9c-7e8dd5a55857", APIVersion:"v1", ResourceVersion:"1129016", FieldPath:""}): type: 'Normal' reason: 'ScaledUpGroup' Scale-up: setting group eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb size to 6 instead of 3 (max: 6)
    I0307 15:20:19.485137       1 eventing_scale_up_processor.go:47] Skipping event processing for unschedulable pods since there is a ScaleUp attempt this loop
    I0307 15:20:19.485439       1 event_sink_logging_wrapper.go:48] Event(v1.ObjectReference{Kind:"ConfigMap", Namespace:"kube-system", Name:"cluster-autoscaler-status", UID:"0cb74c85-7760-47bc-8a9c-7e8dd5a55857", APIVersion:"v1", ResourceVersion:"1129016", FieldPath:""}): type: 'Normal' reason: 'ScaledUpGroup' Scale-up: group eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb size set to 6 instead of 3 (max: 6)
    I0307 15:20:19.496433       1 event_sink_logging_wrapper.go:48] Event(v1.ObjectReference{Kind:"Pod", Namespace:"default", Name:"nginx-to-scaleout-7cfb655fb5-548tk", UID:"6a892479-a9d8-4248-81e0-8a512203dc1c", APIVersion:"v1", ResourceVersion:"1129099", FieldPath:""}): type: 'Normal' reason: 'TriggeredScaleUp' pod triggered scale-up: [{eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb 3->6 (max: 6)}]
    ...
    I0307 15:20:19.564176       1 event_sink_logging_wrapper.go:48] Event(v1.ObjectReference{Kind:"Pod", Namespace:"default", Name:"nginx-to-scaleout-7cfb655fb5-pvd6z", UID:"7067820c-f029-46e0-88d7-ba1affc71a5a", APIVersion:"v1", ResourceVersion:"1129108", FieldPath:""}): type: 'Normal' reason: 'TriggeredScaleUp' pod triggered scale-up: [{eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb 3->6 (max: 6)}]
    I0307 15:20:19.573028       1 event_sink_logging_wrapper.go:48] Event(v1.ObjectReference{Kind:"Pod", Namespace:"default", Name:"nginx-to-scaleout-7cfb655fb5-p9qrl", UID:"226abc42-c506-4a4e-b205-f9251bda6b45", APIVersion:"v1", ResourceVersion:"1129118", FieldPath:""}): type: 'Normal' reason: 'TriggeredScaleUp' pod triggered scale-up: [{eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb 3->6 (max: 6)}]
    I0307 15:20:29.509209       1 static_autoscaler.go:276] Starting main loop
    I0307 15:20:29.510226       1 filter_out_schedulable.go:63] Filtering out schedulables
    # scale out된 node에 unscheduled 였던 pod들이 배치됨
    I0307 15:20:29.510412       1 hinting_simulator.go:77] Pod default/nginx-to-scaleout-7cfb655fb5-czz7p can be moved to template-node-for-eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb-3902890183311134652-upcoming-0
    I0307 15:20:29.510514       1 hinting_simulator.go:77] Pod default/nginx-to-scaleout-7cfb655fb5-548tk can be moved to template-node-for-eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb-3902890183311134652-upcoming-0
    ...
    # 노드 자동 증가 확인
    kubectl get nodes
    aws autoscaling describe-auto-scaling-groups \
        --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
        --output table
    
    NAME                                               STATUS   ROLES    AGE     VERSION
    ip-192-168-1-116.ap-northeast-2.compute.internal   Ready       2m46s   v1.31.5-eks-5d632ec
    ip-192-168-1-13.ap-northeast-2.compute.internal    Ready       38h     v1.31.5-eks-5d632ec
    ip-192-168-2-115.ap-northeast-2.compute.internal   Ready       38h     v1.31.5-eks-5d632ec
    ip-192-168-2-227.ap-northeast-2.compute.internal   Ready       2m49s   v1.31.5-eks-5d632ec
    ip-192-168-3-106.ap-northeast-2.compute.internal   Ready       38h     v1.31.5-eks-5d632ec
    ip-192-168-3-161.ap-northeast-2.compute.internal   Ready       2m50s   v1.31.5-eks-5d632ec
    -----------------------------------------------------------------
    |                   DescribeAutoScalingGroups                   |
    +------------------------------------------------+----+----+----+
    |  eks-ng1-18cab0c2-321a-d736-4f15-d31b91eff0cb  |  3 |  6 |  6 |
    +------------------------------------------------+----+----+----+
    
    eks-node-viewer --resources cpu,memory
    혹은
    eks-node-viewer
    
    # [운영서버 EC2] 최근 1시간 Fleet API 호출 확인 - Link
    # https://ap-northeast-2.console.aws.amazon.com/cloudtrailv2/home?region=ap-northeast-2#/events?EventName=CreateFleet
    aws cloudtrail lookup-events \
      --lookup-attributes AttributeKey=EventName,AttributeValue=CreateFleet \
      --start-time "$(date -d '1 hour ago' --utc +%Y-%m-%dT%H:%M:%SZ)" \
      --end-time "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"
    
    # (참고) Event name : UpdateAutoScalingGroup
    # https://ap-northeast-2.console.aws.amazon.com/cloudtrailv2/home?region=ap-northeast-2#/events?EventName=UpdateAutoScalingGroup
    
    
    # 디플로이먼트 삭제
    kubectl delete -f nginx.yaml && date
    
    # [scale-down] 노드 갯수 축소 : 기본은 10분 후 scale down 됨, 물론 아래 flag 로 시간 수정 가능 >> 그러니 디플로이먼트 삭제 후 10분 기다리고 나서 보자!
    # By default, cluster autoscaler will wait 10 minutes between scale down operations, 
    # you can adjust this using the --scale-down-delay-after-add, --scale-down-delay-after-delete, 
    # and --scale-down-delay-after-failure flag. 
    # E.g. --scale-down-delay-after-add=5m to decrease the scale down delay to 5 minutes after a node has been added.
    
    # 터미널1
    watch -d kubectl get node
    • CloudTrail 에 CreateFleet 이벤트 확인 - Link
    Copy
    # CloudTrail 에 CreateFleet 이벤트 조회 : 최근 90일 가능
    aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=CreateFleet

    즉, eks cluster의 cluster-autoscaler pod가 unscheduled된 pod를 발견 → 해당 resource의 pod를 배포 할 수 있는 node spec 및 수 계산 → unscheduled pod 정상 배포 순으로 동작된다.

    이 과정에서 cluster-autoscaler pod는 계산된 결과는 ec2 autoscaling api를 호출 하고, ec2 autoscaling에서 다시 CreateFleet을 요청한다. 즉, unscheduled pod가 탐지된 즉시 CreateFleet을 하는것이 아니라 autoscaling api를 호출하는 중간 과정을 거치기 때문에 불필요한 하나의 hop이 있다고 느낄 수 있다.

     

    • cluster-autocaler pod의 계산 방식

    4.4. CAS 문제점 : 하나의 자원에 대해 두군데 (AWS ASG vs AWS EKS)에서 각자의 방식으로 관리 ⇒ 관리 정보가 서로 동기화되지 않아 다양한 문제 발생

    [참고 영상] 오픈 소스 Karpenter를 활용한 Amazon EKS 확장 운영 전략 (신재현) 무신사 - 링크 , 원본영상

    • CA 문제점 : ASG에만 의존하고 노드 생성/삭제 등에 직접 관여 안함
    • EKS에서 노드를 삭제 해도 인스턴스는 삭제 안됨
    • 노드 축소 될 때 특정 노드가 축소 되도록 하기 매우 어려움 : pod이 적은 노드 먼저 축소, 이미 드레인 된 노드 먼저 축소
    • 특정 노드를 삭제 하면서 동시에 노드 개수를 줄이기 어려움 : 줄일때 삭제 정책 옵션이 다양하지 않음
    • 특정 노드를 삭제하면서 동시에 노드 개수를 줄이기 어려움
    • 폴링 방식이기에 너무 자주 확장 여유를 확인 하면 API 제한에 도달할 수 있음
    • 스케일링 속도가 느림
    • Cluster Autoscaler 는 쿠버네티스 클러스터 자체의 오토 스케일링을 의미하며, 수요에 따라 워커 노드를 자동으로 추가하는 기능
    • 언뜻 보기에 클러스터 전체나 각 노드의 부하 평균이 높아졌을 때 확장으로 보인다 → 함정! 🚧
    • Pending 상태의 파드가 생기는 타이밍에 처음으로 Cluster Autoscaler 이 동작한다
    • 기본적으로 리소스에 의한 스케줄링은 Requests(최소)를 기준으로 이루어진다. 다시 말해 Requests 를 초과하여 할당한 경우에는 최소 리소스 요청만으로 리소스가 꽉 차 버려서 신규 노드를 추가해야만 한다. 이때 실제 컨테이너 프로세스가 사용하는 리소스 사용량은 고려되지 않는다.
    • 반대로 Request 를 낮게 설정한 상태에서 Limit 차이가 나는 상황을 생각해보자. 각 컨테이너는 Limits 로 할당된 리소스를 최대로 사용한다. 그래서 실제 리소스 사용량이 높아졌더라도 Requests 합계로 보면 아직 스케줄링이 가능하기 때문에 클러스터가 스케일 아웃하지 않는 상황이 발생한다.
    • 여기서는 CPU 리소스 할당을 예로 설명했지만 메모리의 경우도 마찬가지다.
    1. CPA - Cluster Proportional Autoscaler
    2. Karpenter
    • 관리 간소화
    • 노드 오버 프로비저닝 전략 : 우선순위 낮은 더미 파드 배치 활용
    • [영상] [CNKCD2024] 유연한 클라우드 운영을 위한 Karpenter 의 내부 메커니즘과 사례 분석 (강인호) : 정리 예정
    • [영상] [CNKCD2024] 쿠버네티스 스케줄러는 노드를 어떻게 선택하는가? (임찬식)

    6.2. Getting Started with Karpenter 실습 - Docs

    1. Install utilities
    2. Set environment variables
    3. Create a Cluster
    4. Install Karpenter
    5. 프로메테우스 / 그라파나 설치 - Docs
    6. Create NodePool (구 Provisioner) - Workshop , Docs , NodeClaims
    7. Scale up deployment : This deployment uses the pause image and starts with zero replicas.
    8. Scale Down deployment

    6.3. Disruption: Expiration , Drift , Consolidation - Workshop , Docs , Spot-to-Spot

    • Expiration 만료 : 기본 720시간(30일) 후 인스턴스를 자동으로 만료하여 강제로 노드를 최신 상태로 유지
    • Drift 드리프트 : 구성 변경 사항(NodePool, EC2NodeClass)를 감지하여 필요한 변경 사항을 적용
    • Consolidation 통합 : 비용 효율적인 컴퓨팅 최적화 선택
    • 스팟 인스턴스 시작 시 Karpenter는 AWS EC2 Fleet Instance API를 호출하여 NodePool 구성 기반으로 선택한 인스턴스 유형을 전달.
    • AWS EC2 Fleet Instance API는 시작된 인스턴스 목록과 시작할 수 없는 인스턴스 목록을 즉시 반환하는 API로, 시작할 수 없을 경우 Karpenter는 대체 용량을 요청하거나 워크로드에 대한 soft 일정 제약 조건을 제거할 수 있음
    • Spot-to-Spot Consolidation 에는 주문형 통합과 다른 접근 방식이 필요했습니다. 온디맨드 통합의 경우 규모 조정 및 최저 가격이 주요 지표로 사용됩니다.
    • 스팟 간 통합이 이루어지려면 Karpenter에는 최소 15개의 인스턴스 유형이 포함된 다양한 인스턴스 구성(연습에 정의된 NodePool 예제 참조)이 필요합니다. 이러한 제약 조건이 없으면 Karpenter가 가용성이 낮고 중단 빈도가 높은 인스턴스를 선택할 위험이 있습니다.
    Copy
    # 기존 nodepool 삭제
    kubectl delete nodepool,ec2nodeclass default
    
    # 모니터링
    kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
    eks-node-viewer --resources cpu,memory --node-selector "karpenter.sh/registered=true" --extra-labels eks-node-viewer/node-age
    watch -d "kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L karpenter.sh/capacity-type"
    
    # Create a Karpenter NodePool and EC2NodeClass
    cat <<EOF | envsubst | kubectl apply -f -
    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata:
      name: default
    spec:
      template:
        spec:
          nodeClassRef:
            group: karpenter.k8s.aws
            kind: EC2NodeClass
            name: default
          requirements:
            - key: kubernetes.io/os
              operator: In
              values: ["linux"]
            - key: karpenter.sh/capacity-type
              operator: In
              values: ["on-demand"]
            - key: karpenter.k8s.aws/instance-category
              operator: In
              values: ["c", "m", "r"]
            - key: karpenter.k8s.aws/instance-size
              operator: NotIn
              values: ["nano","micro","small","medium"]
            - key: karpenter.k8s.aws/instance-hypervisor
              operator: In
              values: ["nitro"]
          expireAfter: 1h # nodes are terminated automatically after 1 hour
      limits:
        cpu: "1000"
        memory: 1000Gi
      disruption:
        consolidationPolicy: WhenEmptyOrUnderutilized # policy enables Karpenter to replace nodes when they are either empty or underutilized
        consolidateAfter: 1m
    ---
    apiVersion: karpenter.k8s.aws/v1
    kind: EC2NodeClass
    metadata:
      name: default
    spec:
      role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
      amiSelectorTerms:
        - alias: "al2023@latest"
      subnetSelectorTerms:
        - tags:
            karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
      securityGroupSelectorTerms:
        - tags:
            karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
    EOF
    
    # 확인 
    kubectl get nodepool,ec2nodeclass
    
    # Deploy a sample workload
    cat <<EOF | kubectl apply -f -
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: inflate
    spec:
      replicas: 5
      selector:
        matchLabels:
          app: inflate
      template:
        metadata:
          labels:
            app: inflate
        spec:
          terminationGracePeriodSeconds: 0
          securityContext:
            runAsUser: 1000
            runAsGroup: 3000
            fsGroup: 2000
          containers:
          - name: inflate
            image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
            resources:
              requests:
                cpu: 1
                memory: 1.5Gi
            securityContext:
              allowPrivilegeEscalation: false
    EOF
    
    
    #
    kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L karpenter.sh/capacity-type
    kubectl get nodeclaims
    kubectl describe nodeclaims
    kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
    kubectl logs -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | grep 'launched nodeclaim' | jq '.'
    
    
    # Scale the inflate workload from 5 to 12 replicas, triggering Karpenter to provision additional capacity
    kubectl scale deployment/inflate --replicas 12
    
    # This changes the total memory request for this deployment to around 12Gi, 
    # which when adjusted to account for the roughly 600Mi reserved for the kubelet on each node means that this will fit on 2 instances of type m5.large:
    kubectl get nodeclaims
    
    
    # Scale down the workload back down to 5 replicas
    kubectl scale deployment/inflate --replicas 5
    kubectl get nodeclaims
    NAME            TYPE          CAPACITY    ZONE              NODE                                                READY     AGE
    default-dfc52   c6g.2xlarge   on-demand   ap-northeast-2a   ip-192-168-136-55.ap-northeast-2.compute.internal   Unknown   23s
    
    
    # We can check the Karpenter logs to get an idea of what actions it took in response to our scaling in the deployment. Wait about 5-10 seconds before running the following command:
    kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
    
      "level": "INFO",
      "time": "2025-03-08T13:12:30.534Z",
      "logger": "controller",
      "message": "computed new nodeclaim(s) to fit pod(s)",
      "commit": "ff59416",
      "controller": "provisioner",
      "namespace": "",
      "name": "",
      "reconcileID": "0dad53d9-6cb9-4b41-81e5-eaa2e19f966f",
      "nodeclaims": 1,
      "pods": 5
    }
    {
      "level": "INFO",
      "time": "2025-03-08T13:12:30.546Z",
      "logger": "controller",
      "message": "created nodeclaim",
      "commit": "ff59416",
      "controller": "provisioner",
      "namespace": "",
      "name": "",
      "reconcileID": "0dad53d9-6cb9-4b41-81e5-eaa2e19f966f",
      "NodePool": {
        "name": "default"
      },
      "NodeClaim": {
        "name": "default-dfc52"
      },
      "requests": {
        "cpu": "5150m",
        "memory": "7680Mi",
        "pods": "9"
      },
      "instance-types": "c5.2xlarge, c5.4xlarge, c5a.2xlarge, c5a.4xlarge, c5d.2xlarge and 55 other(s)"
    }
    {
      "level": "INFO",
      "time": "2025-03-08T13:12:33.023Z",
      "logger": "controller",
      "message": "launched nodeclaim",
      "commit": "ff59416",
      "controller": "nodeclaim.lifecycle",
      "controllerGroup": "karpenter.sh",
      "controllerKind": "NodeClaim",
      "NodeClaim": {
        "name": "default-dfc52"
      },
      "namespace": "",
      "name": "default-dfc52",
      "reconcileID": "08bca9fb-6196-4dc8-a68a-40789cb6c952",
      "provider-id": "aws:///ap-northeast-2a/i-06750c46c7705225d",
      "instance-type": "c6g.2xlarge",
      "zone": "ap-northeast-2a",
      "capacity-type": "on-demand",
      "allocatable": {
        "cpu": "7910m",
        "ephemeral-storage": "17Gi",
        "memory": "14103Mi",
        "pods": "58",
        "vpc.amazonaws.com/pod-eni": "38"
      }
    }
    {
      "level": "INFO",
      "time": "2025-03-08T13:12:48.551Z",
      "logger": "controller",
      "message": "registered nodeclaim",
      "commit": "ff59416",
      "controller": "nodeclaim.lifecycle",
      "controllerGroup": "karpenter.sh",
      "controllerKind": "NodeClaim",
      "NodeClaim": {
        "name": "default-dfc52"
      },
    
    # Karpenter can also further consolidate if a node can be replaced with a cheaper variant in response to workload changes. 
    # This can be demonstrated by scaling the inflate deployment replicas down to 1, with a total memory request of around 1Gi:
    kubectl scale deployment/inflate --replicas 1
    # underutilized된 node를 발견하고 replace disruption 발생. spec이 작은 node로 replace된다.
    {
      "level": "INFO",
      "time": "2025-03-08T13:16:30.981Z",
      "logger": "controller",
      "message": "disrupting node(s)",
      "commit": "ff59416",
      "controller": "disruption",
      "namespace": "",
      "name": "",
      "reconcileID": "cdfa2eab-05b5-4e52-8417-6fe2e4dd0a6b",
      "command-id": "7626fdd2-b1c1-4178-a0bf-cd476745f9e6",
      "reason": "underutilized",
      "decision": "replace",
      "disrupted-node-count": 1,
      "replacement-node-count": 1,
      "pod-count": 1,
      "disrupted-nodes": [
        {
          "Node": {
            "name": "ip-192-168-136-55.ap-northeast-2.compute.internal"
          },
          "NodeClaim": {
            "name": "default-dfc52"
          },
          "capacity-type": "on-demand",
          "instance-type": "c6g.2xlarge"
        }
      ],
      "replacement-nodes": [
        {
          "capacity-type": "on-demand",
          "instance-types": "c6g.large, c7g.large, c5a.large, c6gd.large, m6g.large and 55 other(s)"
        }
      ]
    }
    # replace될 nodeclaims를 새롭게 생성한다.
    {
      "level": "INFO",
      "time": "2025-03-08T13:16:31.063Z",
      "logger": "controller",
      "message": "created nodeclaim",
      "commit": "ff59416",
      "controller": "disruption",
      "namespace": "",
      "name": "",
      "reconcileID": "cdfa2eab-05b5-4e52-8417-6fe2e4dd0a6b",
      "NodePool": {
        "name": "default"
      },
      "NodeClaim": {
        "name": "default-zll8t"
      },
      "requests": {
        "cpu": "1150m",
        "memory": "1536Mi",
        "pods": "5"
      },
      "instance-types": "c5.large, c5.xlarge, c5a.large, c5a.xlarge, c5d.large and 55 other(s)"
    }
    
    kubectl get nodeclaims
    NAME            TYPE          CAPACITY    ZONE              NODE                                                READY   AGE
    default-dfc52   c6g.2xlarge   on-demand   ap-northeast-2a   ip-192-168-136-55.ap-northeast-2.compute.internal   True    3m16s
    
    kubectl get nodeclaims
    NAME            TYPE        CAPACITY    ZONE              NODE                                                READY   AGE
    default-zll8t   c6g.large   on-demand   ap-northeast-2c   ip-192-168-185-33.ap-northeast-2.compute.internal   True    2m34s
    
    
    # 삭제
    kubectl delete deployment inflate
    kubectl delete nodepool,ec2nodeclass default

    6.4. Spot-to-Spot Consolidation 실습 해보기

    Copy
    # 모니터링
    kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
    eks-node-viewer --resources cpu,memory --node-selector "karpenter.sh/registered=true"
    
    # Create a Karpenter NodePool and EC2NodeClass
    cat <<EOF | envsubst | kubectl apply -f -
    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata:
      name: default
    spec:
      template:
        spec:
          nodeClassRef:
            group: karpenter.k8s.aws
            kind: EC2NodeClass
            name: default
          requirements:
            - key: kubernetes.io/os
              operator: In
              values: ["linux"]
            - key: karpenter.sh/capacity-type
              operator: In
              values: ["spot"]
            - key: karpenter.k8s.aws/instance-category
              operator: In
              values: ["c", "m", "r"]
            - key: karpenter.k8s.aws/instance-size
              operator: NotIn
              values: ["nano","micro","small","medium"]
            - key: karpenter.k8s.aws/instance-hypervisor
              operator: In
              values: ["nitro"]
          expireAfter: 1h # nodes are terminated automatically after 1 hour
      limits:
        cpu: "1000"
        memory: 1000Gi
      disruption:
        consolidationPolicy: WhenEmptyOrUnderutilized # policy enables Karpenter to replace nodes when they are either empty or underutilized
        consolidateAfter: 1m
    ---
    apiVersion: karpenter.k8s.aws/v1
    kind: EC2NodeClass
    metadata:
      name: default
    spec:
      role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
      amiSelectorTerms:
        - alias: "bottlerocket@latest"
      subnetSelectorTerms:
        - tags:
            karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
      securityGroupSelectorTerms:
        - tags:
            karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
    EOF
    
    # 확인 
    kubectl get nodepool,ec2nodeclass
    
    # Deploy a sample workload
    cat <<EOF | kubectl apply -f -
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: inflate
    spec:
      replicas: 5
      selector:
        matchLabels:
          app: inflate
      template:
        metadata:
          labels:
            app: inflate
        spec:
          terminationGracePeriodSeconds: 0
          securityContext:
            runAsUser: 1000
            runAsGroup: 3000
            fsGroup: 2000
          containers:
          - name: inflate
            image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
            resources:
              requests:
                cpu: 1
                memory: 1.5Gi
            securityContext:
              allowPrivilegeEscalation: false
    EOF
    
    #
    kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L karpenter.sh/capacity-type
    kubectl get nodeclaims
    kubectl describe nodeclaims
    kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
    kubectl logs -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | grep 'launched nodeclaim' | jq '.'
    
    # Scale the inflate workload from 5 to 12 replicas, triggering Karpenter to provision additional capacity
    kubectl scale deployment/inflate --replicas 12
    
    # This changes the total memory request for this deployment to around 12Gi, 
    # which when adjusted to account for the roughly 600Mi reserved for the kubelet on each node means that this will fit on 2 instances of type m5.large:
    kubectl get nodeclaims
    
    # Scale down the workload back down to 5 replicas
    kubectl scale deployment/inflate --replicas 5
    kubectl get nodeclaims
    
    # We can check the Karpenter logs to get an idea of what actions it took in response to our scaling in the deployment. Wait about 5-10 seconds before running the following command:
    kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
    
    # Karpenter can also further consolidate if a node can be replaced with a cheaper variant in response to workload changes. 
    # This can be demonstrated by scaling the inflate deployment replicas down to 1, with a total memory request of around 1Gi:
    kubectl scale deployment/inflate --replicas 1
    kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'
    kubectl get nodeclaims
    
    # 삭제
    kubectl delete deployment inflate
    kubectl delete nodepool,ec2nodeclass default

    6.5. 실습 리소스 삭제 - Docs

    Copy
    # Karpenter helm 삭제 
    helm uninstall karpenter --namespace "${KARPENTER_NAMESPACE}"
    
    # Karpenter IAM Role 등 생성한 CloudFormation 삭제
    aws cloudformation delete-stack --stack-name "Karpenter-${CLUSTER_NAME}"
    
    # EC2 Launch Template 삭제
    aws ec2 describe-launch-templates --filters "Name=tag:karpenter.k8s.aws/cluster,Values=${CLUSTER_NAME}" |
        jq -r ".LaunchTemplates[].LaunchTemplateName" |
        xargs -I{} aws ec2 delete-launch-template --launch-template-name {}
    
    # 클러스터 삭제
    eksctl delete cluster --name "${CLUSTER_NAME}"
    • 클러스터 삭제 이후에도, Karpenter IAM Role 생성한 CloudFormation 삭제가 잘 안될 경우 AWS CloudFormation 관리 콘솔에서 직접 삭제!

    6.6. 참고

    [도전과제] (추천) 카펜터 심화 워크숍 따라해보기 - Workshop

    • Limit Resources - Link
    • Drift - Link
    • Weighting NodePool - Link
    • Using Graviton - Link
    • On-Demand & Spot Ratio Split - Link
    • Persistence Volume Topology - Link
    • Disruption Control - Link
    • Multi-Arch - Link
    • Dynamic Pod Density - Link
    • Observability - Link

    [도전과제] (추천) 비용 최적화 카펜터 워크숍 따라해보기 - Workshop

    [도전과제] Karpenter Drift를 사용하여 Amazon EKS 워커 노드를 업그레이드하기 - Link

    [도전과제] KWOK 를 활용하여 karpenter 시뮬레이션 해보기 - KWOK , Blog , Karpenter

    [도전과제] 아래 영상에 나온 Simkube 를 이용하여 CAS/Karpenter 를 가상의 노드로 시뮬레이션 해보기!

     
Designed by Tistory.