AEWS Study #5 – EKS Autoscaling

그동안은 EKS 실습을 하면서 pod의 replica 수량을 수동으로 설정하고 경우에 따라 늘리거나 줄이거나 했었다.
그 부분을 EC2 기반 서비스를 운영할 때와 마찬가지로 Autoscaling을 할 수 있는 방법에 대해 학습하고 실습을 진행할 예정이다.

0. 환경 구성

환경 구성은 이번엔 특별하게 추가 진행한 부분은 없고 가시다님이 제공해주신 스크립트를 통해 환경 구성을 진행했다.
yaml 배포를 진행하고 프로메테우스&그라파나 설치를 진행했다.

# 사용 리전의 인증서 ARN 확인
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

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

# 파라미터 파일 생성
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  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

  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'

defaultRules:
  create: false
kubeControllerManager:
  enabled: false
kubeEtcd:
  enabled: false
kubeScheduler:
  enabled: false
alertmanager:
  enabled: false
EOT

# 배포
kubectl create ns monitoring
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 45.27.2 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring

# Metrics-server 배포
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

EKS Node Viewer도 설치를 진행한다. EKS Node Viewer은 예약된 Pod 리소스 요청과 노드의 할당 가능한 용량을 표시해줍니다. AutoScaling을 진행할 때 도움이 되는 Viewer이니 미리 설치를 진행한다.

# go 설치
yum install -y go

# EKS Node Viewer 설치 : 현재 ec2 spec에서는 설치에 다소 시간이 소요됨 = 2분 이상
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest

# bin 확인 및 사용 
tree ~/go/bin
cd ~/go/bin
./eks-node-viewer
3 nodes (875m/5790m) 15.1% cpu ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ $0.156/hour | $113.880/month
20 pods (0 pending 20 running 20 bound)

ip-192-168-3-82.ap-northeast-2.compute.internal  cpu ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (6 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-2-196.ap-northeast-2.compute.internal cpu ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (7 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-1-205.ap-northeast-2.compute.internal cpu ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  12% (7 pods) t3.medium/$0.0520 On-Demand - ReadyPress any key to quit

명령 샘플
# Standard usage
./eks-node-viewer

# Display both CPU and Memory Usage
./eks-node-viewer --resources cpu,memory

# Karenter nodes only
./eks-node-viewer --node-selector "karpenter.sh/provisioner-name"

# Display extra labels, i.e. AZ
./eks-node-viewer --extra-labels topology.kubernetes.io/zone
3 nodes (875m/5790m)     15.1% cpu    ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ $0.156/hour | $113.880/month
        390Mi/10165092Ki 3.9%  memory ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
20 pods (0 pending 20 running 20 bound)

ip-192-168-3-82.ap-northeast-2.compute.internal  cpu    ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (6 pods) t3.medium/$0.0520 On-Demand - Ready
                                                 memory █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   2%
ip-192-168-2-196.ap-northeast-2.compute.internal cpu    ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (7 pods) t3.medium/$0.0520 On-Demand - Ready
                                                 memory ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   8%
ip-192-168-1-205.ap-northeast-2.compute.internal cpu    ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  12% (7 pods) t3.medium/$0.0520 On-Demand - Ready
                                                 memory █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   2%

# Specify a particular AWS profile and region
AWS_PROFILE=myprofile AWS_REGION=us-west-2

# select only Karpenter managed nodes
node-selector=karpenter.sh/provisioner-name

# display both CPU and memory
resources=cpu,memory

1. Auto Scaling?

k8s에서 Auto Scaling은 크게 3가지 방식으로 작동하게 된다. HPA(Horizontal Pod Autoscaler), VPA(Vertical Pod Autoscaler), CA(Cluster Autoscaler)

https://www.oreilly.com/library/view/production-kubernetes/9781492092292/ch01.html

HPA는 Scale In/Out 방식으로 Resource API을 통해 15분 마다 메모리/CPU 사용량을 수집하여 정책에 맞게 Pod의 수를 증가/감소 시키는 Auto Scaling을 작동하게 된다.

VPA는 Scale Up/Down 방식으로 Resource 사용량을 수집하여 Pod을 Restart 하면서 Pod의 Resource을 증가/감소 시키는 Auto Scaling 방식이다.

CA는 노드 레벨에서의 감시가 이루어지며 워커 노드의 Resource을 확인하여 부족할 경우 Node을 추가 배포하여 이후 Pod을 새로운 Node에 배포해주는 방식이다.

2. HPA – Horizontal Pod Autoscaler

HPA 방식을 사용해서 Auto Scaling을 진행해본다. Pod의 수량 변동을 확인하기 위해 kube-ops-view와 그라파나 대쉬보드(대쉬보드 import id #17125)를 통해 모니터링 한다. 
우선 테스트에 사용할 Pod을 배포하고 모니터링을 진행한다.

# Run and expose php-apache server
curl -s -O https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/application/php-apache.yaml
cat php-apache.yaml | yh
kubectl apply -f php-apache.yaml

# 확인
kubectl exec -it deploy/php-apache -- cat /var/www/html/index.php
...

# 모니터링 : 터미널2개 사용
watch -d 'kubectl get hpa,pod;echo;kubectl top pod;echo;kubectl top node'
kubectl exec -it deploy/php-apache -- top

# 접속
PODIP=$(kubectl get pod -l run=php-apache -o jsonpath={.items[0].status.podIP})
curl -s $PODIP; echo

# Create the HorizontalPodAutoscaler : requests.cpu=200m - 알고리즘
# Since each pod requests 200 milli-cores by kubectl run, this means an average CPU usage of 100 milli-cores.
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
kubectl describe hpa
Warning: autoscaling/v2beta2 HorizontalPodAutoscaler is deprecated in v1.23+, unavailable in v1.26+; use autoscaling/v2 HorizontalPodAutoscaler
Name:                                                  php-apache
Namespace:                                             default
Labels:                                                <none>
Annotations:                                           <none>
CreationTimestamp:                                     Thu, 25 May 2023 21:37:49 +0900
Reference:                                             Deployment/php-apache
Metrics:                                               ( current / target )
  resource cpu on pods  (as a percentage of request):  0% (1m) / 50%
Min replicas:                                          1
Max replicas:                                          10
Deployment pods:                                       1 current / 1 desired
Conditions:
  Type            Status  Reason               Message
  ----            ------  ------               -------
  AbleToScale     True    ScaleDownStabilized  recent recommendations were higher than current one, applying the highest recent recommendation
  ScalingActive   True    ValidMetricFound     the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
  ScalingLimited  False   DesiredWithinRange   the desired count is within the acceptable range
Events:           <none>

# HPA 설정 확인
kubectl krew install neat
kubectl get hpa php-apache -o yaml
kubectl get hpa php-apache -o yaml | kubectl neat | yh
spec: 
  minReplicas: 1               # [4] 또는 최소 1개까지 줄어들 수도 있습니다
  maxReplicas: 10              # [3] 포드를 최대 5개까지 늘립니다
  scaleTargetRef: 
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache           # [1] php-apache 의 자원 사용량에서
  metrics: 
  - type: Resource
    resource: 
      name: cpu
      target: 
        type: Utilization
        averageUtilization: 50  # [2] CPU 활용률이 50% 이상인 경우

모니터링이 잘 되고 있는 것을 확인했으니 이제 부하를 발생시켜서 Pod가 증가하는지 확인해보도록 한다. POD IP을 직접 타겟해서 반복 접속을 실행했고 CPU 사용량이 증가하면서 Pod가 1개에서 2개로 늘어난 것을 확인할 수 있다. Pod의 CPU는 200m으로 할당이 되어있었고 50%이상 사용 시 증가하는 규칙이 있기 때문에 1번 Pod의 사용량이 100m이 넘으면서 2번 Pod가 배포 된 것을 알 수 있다.

# 반복 접속 1 (파드1 IP로 접속) >> 증가 확인 후 중지
while true;do curl -s $PODIP; sleep 0.5; done

2번째 테스트는 Pod IP가 아닌 서비스명 도메인으로 접속하는 테스트를 진행해보았다.
부하 생성을 진행했고 서비스 도메인으로 접속하기 때문에 Pod가 늘어날 때마다 Pod가 늘어났다. 다만, 10개를 MAX로 했지만 7개까지밖에 늘어나지 않는 것을 확인할 수 있었다.
이유는 아마도, 이 정도의 부하만으로는 7개의 Pod가 견딜 수 있고 그로인해 CPU 사용률이 50%을 넘지않아 Pod가 더 추가되지 않는 것으로 보인다.

# 반복 접속 2 (서비스명 도메인으로 접속) >> 증가 확인(몇개까지 증가되는가? 그 이유는?) 후 중지 >> 중지 5분 후 파드 갯수 감소 확인
# 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"

부하 발생을 중지하고 약 5분 정도의 시간이 지난 뒤에 Pod가 1개로 감소한 것을 확인할 수 있었다. 감소 규칙도 잘 적용된 것을 확인할 수 있었다.

3. KEDA – Kubernetes based Event Driven Autoscaler

위에서 실습한 HPA는 CPU, Memory와 같은 Resource Metic을 기반으로 스케일을 구성하게 되는데 KEDA는 특정 이벤트 기반으로 스케일을 구성할 수 있

https://keda.sh/docs/2.10/concepts/

테스트를 진행하기에 앞서 그라파나 대시보드를 구성하는데 아래 json 파일을 Import 해서 준비하였다.
https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json
이후 KEDA을 설치하고 테스트를 진행해보았다.
ScaledObject 정책은 minReplica 0, MaxReplica 2에 정시부터 15분 간격으로 Pod가 생성되고 다시 5분부터 15분 단위로 Pod가 줄어드는 규칙이 적용되어 있다.

# 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: 8080
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus webhooks
      enabled: true
EOT

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

# KEDA 설치 확인
kubectl get-all -n keda
kubectl get all -n keda
kubectl get crd | grep keda

# keda 네임스페이스에 디플로이먼트 생성
kubectl apply -f php-apache.yaml -n keda
kubectl get pod -n keda

# ScaledObject 정책 생성 : cron
cat <<EOT > keda-cron.yaml
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"
EOT
kubectl apply -f keda-cron.yaml -n keda

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

# 확인
kubectl get ScaledObject,hpa,pod -n keda
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 -f keda-cron.yaml -n keda && kubectl delete deploy php-apache -n keda && helm uninstall keda -n keda
kubectl delete namespace keda

테스트를 진행하게 되면 처음엔 0개의 Pod로 시작해서 정시부터 15분 단위로 1개의 Pod가 생성되고 다시 약 10분 뒤에 Pod가 줄어들어 0개의 Pod가 되는 것을 확인할 수 있다.
이렇게 간단하게 KEDA을 사용해서 이벤트 기반으로 Autoscale을 진행해보았다.

4. VPA – Vertical Pod Autoscaler

Pod의 수를 증가/감소시켰던 HPA에 이어 이번에는 Pod의 Resource 자체를 증가/감소시키는 VPA을 실습한다.
아래 코드를 사용하여 VPA 환경을 배포하고 그라파나 대시보드는 14588로 Import 하여 준비한다.

# 코드 다운로드
git clone https://github.com/kubernetes/autoscaler.git
cd ~/autoscaler/vertical-pod-autoscaler/
tree hack

# openssl 버전 확인
openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017

# 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

# 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
kubectl get crd | grep autoscaling

VPA Autoscaler을 작동시켜보았다. CPU와 Memory가 확장된 것을 볼 수 있다.
VPA을 통해 HPA의 수평 확장과 다르게 수직 확장을 하는 것을 확인할 수 있었다. 다만 VPA는 HPA와 동시에 사용할 수 없기 때문에 단일 Pod에서의 처리량을 높일지 아니면 Pod의 수량을 늘려서 처리량을 받아낼지 결정을 해서 HPA와 VPA 중에 선택하면 좋을 것 같다.

# 모니터링
watch -d kubectl top pod

# 공식 예제 배포
cd ~/autoscaler/vertical-pod-autoscaler/
cat examples/hamster.yaml | yh
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
111s        Normal    EvictedByVPA             pod/hamster-5bccbb88c6-dq4td         Pod was evicted by VPA Updater to apply resource recommendation.
51s         Normal    EvictedByVPA             pod/hamster-5bccbb88c6-bpfwj         Pod was evicted by VPA Updater to apply resource recommendation.

5. CA – Cluster Autoscaler

CA는 위의 HPA, VPA와 다르게 Pod의 수나 리소스를 증가/감소시키는 게 아닌 Node을 증가/감소시키는 Scaler 이다.
Pending 상태인 Pod가 있을 경우 Node의 리소스가 부족해 Pod가 배포되지 않는 것으로 인지하기 때문에 Node을 증가시킨다. EKS는 AWS 환경 내에서 작동하기 때문에 ASG(Auto Scaling Group)과 병행하여 사용할 수 있다.

https://catalog.us-east-1.prod.workshops.aws/workshops/9c0aa9ab-90a9-44a6-abe1-8dff360ae428/ko-KR/100-scaling/200-cluster-scaling

CA 설정을 하기 전에 기존 배포되어 있는 EKS의 CA 설정 확인을 통해 CA 설정이 활성화 되어있는 것을 확인할 수 있었다.

# 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 yaml | yh
...
    - Key: k8s.io/cluster-autoscaler/enabled
      Value: 'true'
    - Key: k8s.io/cluster-autoscaler/myeks
      Value: owned
...

CA는 앞서 위에서 설명한 것과 같이 ASG와 통합해서 구성되기 때문에 ASG의 Launch Template을 기반으로 Auto Scaling을 진행한다.
ASG 정보를 확인하고 기본 설정으로 되어있는 min, max값을 확인한 다음 max 값을 6으로 수정한다.

# 현재 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-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  3 |  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-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  3 |  6 |  3 |
+------------------------------------------------+----+----+----+

Max값이 변경 된 것을 확인했다면 CA 서비스를 배포하고 CA Pod가 동작하는 Node가 추후에 Auto Scaling 정책으로 인해 evict 되지 않도록 설정도 진행한다. 해당 설정을 진행하지 않으면 사용량 증가로 인해 Node가 증설됐다가 추후 감소될 때 CA Pod가 배포되어 있는 Node가 사라질 경우 CA 서비스에 영향이 갈 수 있으니 설정을 진행하도록 한다.

# 배포 : Deploy the Cluster Autoscaler (CA)
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
sed -i "s/<YOUR CLUSTER NAME>/$CLUSTER_NAME/g" cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml

# 확인
kubectl get pod -n kube-system | grep cluster-autoscaler
cluster-autoscaler-74785c8d45-mqkd4             1/1     Running   0             26s

kubectl describe deployments.apps -n kube-system cluster-autoscaler
Name:                   cluster-autoscaler
Namespace:              kube-system
CreationTimestamp:      Fri, 26 May 2023 10:02:21 +0900
Labels:                 app=cluster-autoscaler
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=cluster-autoscaler
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable

# (옵션) cluster-autoscaler 파드가 동작하는 워커 노드가 퇴출(evict) 되지 않게 설정
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict="false"
deployment.apps/cluster-autoscaler annotated

설정 확인 및 변경을 진행했으니 Test App을 배포하고 CA가 잘 작동하는지 테스트 해보도록 한다.
Test App의 Replicas를 15로 변경하면 Node가 2개 추가되면서 Pending 상태의 Pod들을 배포하는 것을 확인할 수 있었다.
CA가 잘 작동한 것을 확인했다면 test app을 삭

# 모니터링 
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

# 확인
kubectl get pods -l app=nginx -o wide --watch
kubectl -n kube-system logs -f deployment/cluster-autoscaler

# 노드 자동 증가 확인
kubectl get nodes
aws autoscaling describe-auto-scaling-groups \
    --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
    --output table

./eks-node-viewer
5 nodes (8725m/9650m) 90.4% cpu ████████████████████████████████████░░░░ $0.260/hour | $189.800/month
42 pods (0 pending 42 running 42 bound)

ip-192-168-3-82.ap-northeast-2.compute.internal  cpu █████████████████████████████████░░  95% (9 pods)  t3.medium/$0.0520 On-Demand - Ready
ip-192-168-2-196.ap-northeast-2.compute.internal cpu ███████████████████████████████████ 100% (11 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-1-205.ap-northeast-2.compute.internal cpu ███████████████████████████████░░░░  89% (10 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-1-60.ap-northeast-2.compute.internal  cpu █████████████████████████████░░░░░░  84% (6 pods)  t3.medium/$0.0520 On-Demand - Ready
ip-192-168-2-125.ap-northeast-2.compute.internal cpu █████████████████████████████░░░░░░  84% (6 pods)  t3.medium/$0.0520 On-Demand - Ready

CA 증가가 잘 되는 것을 확인했다면 감소도 잘 작동하는지 확인해보도록 한다.
노드 갯수 축소를 강제로 진행할 수 있지만 test app을 삭제하고 10분 정도 대기 후에 node가 감소하는지를 확인해보도록 한다.
약 10분 정도 시간이 지나면 Node가 감소하는 것을 알 수 있다. 증가 된 Node가 제거되고 기존 운영 중이던 Node는 유지되는 것까지 확인하였다. (AGE를 통해)

# 디플로이먼트 삭제
kubectl delete -f nginx.yaml && date

# 노드 갯수 축소 : 기본은 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

CA 실습이 마무리 되었다면 다시 Node MAX을 기본값이었던 3으로 수정하도록 한다.

CA는 단순 Pod을 늘리거나 Pod의 리소스를 증가시켜주는 방식이 아닌 Node의 수를 늘리는 방식으로 순간 많은 Pod가 필요할 때 유용한 방식처럼 보인다. 하지만 CA에는 몇가지 문제점이 있다.
우선, 실습을 하면서 느낀 부분이지만 HPA, VPA에 비해 Scaling 속도가 매우 느리다. Node가 새로 배포되어야 하니 Pod만 새로 배포하는 거에 비해 시간이 오래 소요됐다.
그리고 ASG와 EKS의 결합으로 구성되는 Scaler이기 때문에 각각 서로의 정보 동기화가 부족하다. 서로 다른 정보를 갖고 있기 때문에 EKS에서 노드를 삭제해도 ASG에는 인스턴스가 남아있는 상황이 발생하게 된다. 그리고 노드=인스턴스가 성립되지 않기 때문에 Pod가 적게 배포 된 Node 먼저 없애는 게 사실상 어려운 상황이다.
그리고 Scaling 조건이 Pending 상태의 Pod가 생겼을 때이기 때문에 Req 양이 많을 때 무조건적인 ScaleOut이 발생하는 게 아니어서 서비스 장애가 발생할 수 있다.
이런 부분을 잘 참고해서 CA 사용을 고려해봐야 할 것 같다.

6. CPA – Cluster Proportional Autoscaler

CPA는 CA 등으로 인해 Node의 Scaling 이 발생했을 때 노드 수가 증가함에 따라 성능 이슈가 발생할 수 있는 어플리케이션 Pod의 수를 수평적으로 증가시켜주는 서비스이다.
예를 들면, Node가 증가함에 따라 CoreDNS의 부하가 증가할 수 있는데 그 부분을 해소시키기 위해 Node의 증가폭에 따라 CoreDNS Pod을 수평 증가시켜준다.

CPA 테스트를 하기 위해 test app과 CPA을 설치한다.
CPA 규칙을 설정하지 않은 상태에서 CPA을 릴리즈 하려고 하면 실패하게 된다.
test app과 cpa 규칙 설정을 할 cpa-values.yaml을 생성한 다음 CPA 릴리즈를 다시 진행한다.
CPA 설정은 Node 수에 따라 Pod 수가 동일하게 증가하도록 설정을 했다. 따라서 CPA 릴리즈가 된다면 현재 Node가 3대이기 때문에 Test App의 Pod도 3개로 증가해야 하는데 릴리즈 되자마자 Pod가 3개가 되는 것을 확인할 수 있었다.

#
helm repo add cluster-proportional-autoscaler https://kubernetes-sigs.github.io/cluster-proportional-autoscaler

# CPA규칙을 설정하고 helm차트를 릴리즈 필요
# CPA 규칙을 설정하지 않은 상태에서는 helm 차트 릴리즈가 실패한다.
helm upgrade --install cluster-proportional-autoscaler cluster-proportional-autoscaler/cluster-proportional-autoscaler
Release "cluster-proportional-autoscaler" does not exist. Installing it now.
Error: execution error at (cluster-proportional-autoscaler/templates/deployment.yaml:3:3): options.target must be one of deployment, replicationcontroller, or replicaset

# nginx 디플로이먼트 배포
cat <<EOT > cpa-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        resources:
          limits:
            cpu: "100m"
            memory: "64Mi"
          requests:
            cpu: "100m"
            memory: "64Mi"
        ports:
        - containerPort: 80
EOT
kubectl apply -f cpa-nginx.yaml

# CPA 규칙 설정
cat <<EOF > cpa-values.yaml
config:
  ladder:
    nodesToReplicas:
      - [1, 1]
      - [2, 2]
      - [3, 3]
      - [4, 3]
      - [5, 5]
options:
  namespace: default
  target: "deployment/nginx-deployment"
EOF

# 모니터링
watch -d kubectl get pod

# helm 업그레이드
helm upgrade --install cluster-proportional-autoscaler -f cpa-values.yaml cluster-proportional-autoscaler/cluster-proportional-autoscaler

이제는 Node을 5개로 증가시켜보도록 하겠다.
min, max 그리고 desired을 모두 5로 변경하면 시간이 2~3분 정도 흐른 뒤 Node 2개가 추가로 생성 된다. 이후 Node가 등록되면 바로 Pod도 5개로 증가하는 것을 확인할 수 있었다.

# 노드 5개로 증가
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 5 --desired-capacity 5 --max-size 5
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  5 |  5 |  5 |
+------------------------------------------------+----+----+----+

Node 감소 테스트도 진행해보도록 한다.
증가 때와 마찬가지로 max, min 그리고 desired을 모두 4로 변경하면 잠시 후 Node 1개가 제외되고 Pod도 3개로 변경되는 것을 확인할 수 있다. (node 4:pod 3규칙이기 때문에)


# 노드 4개로 축소
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 4 --desired-capacity 4 --max-size 4
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  4 |  4 |  4 |
+------------------------------------------------+----+----+----+


CPA는 CA와 병행해서 사용하면 Node가 증가할 때 자연스럽게 Pod을 증가시킬 수 있어서 CA의 아쉬운 부분을 조금 채울 수 있는 서비스라고 생각한다.

7. Karpenter : K8S Native AutoScaler & Fargate

Karpenter는 k8s를 위한 Auto Scaling 솔루션 중 하나로 Cluster 내의 워크로드를 효율적으로 관리하고 리소스를 효과적으로 활용할 수 있게 해준다. CSP나 다른 스케줄러와 독립적으로 동작하며 k8s object와 native하게 통합되는 특징이 있다.

Karpenter 실습을 진행하기 전에 앞서 진행했던 실습 내용들을 정리하기 위해 작업을 진행한다.

# helm 삭제
helm uninstall -n kube-system kube-ops-view
helm uninstall -n monitoring kube-prometheus-stack

# cloudformation 삭제
eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

이전 실습 내용이 삭제됐다면 카펜터 실습을 위한 환경을 다시 배포한다.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file karpenter-preconfig.yaml --stack-name myeks2 --parameter-overrides KeyName=aewspair SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks2 --region ap-northeast-2

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

# 작업용 EC2 SSH 접속
ssh -i aewspair.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks2 --query 'Stacks[*].Outputs[0].OutputValue' --output text)

실습 환경을 배포 후 EKS 배포 전 사전 확인 및 EKS Node Viewer을 설치한다.

# IP 주소 확인 : 172.30.0.0/16 VPC 대역에서 172.30.100.0/24 대역을 사용 중
ip -br -c addr
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             172.30.1.100/24 fe80::ac:c5ff:fec2:77b8/64
docker0          DOWN           172.17.0.1/16

# EKS Node Viewer 설치 : 현재 ec2 spec에서는 설치에 다소 시간이 소요됨 = 2분 이상
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest

# [터미널1] bin 확인 및 사용
tree ~/go/bin
cd ~/go/bin
./eks-node-viewer -h

사전 확인 및 EKS Node Viewer 설치를 완료했다면 이제 EKS 배포를 진행한다.
아래 진행할 EKS 배포에는 다음과 같은 내용이 포함되어 있다.
1) IAM Policy, Role, EC2 Instance Profile을 생성
2) Karpenter가 인스턴스를 시작할 수 있도록 IRSA 사용
3) Karpenter Node Role을 kube-auth configmap에 추가하여 연결 허용
4-1) kube-system 및 karpenter Namespace에 EKS Managed Node Group 사용
4-2) Managed Node Group 대신 Fargate을 사용하고 싶다면 Fargate 주석을 제거하고 managedNodeGroups에 주석 처리를 진행
5) Spot Instance 허용 역할 생성
6) helm 통한 Karpenter 설치

# 환경변수 정보 확인
export | egrep 'ACCOUNT|AWS_|CLUSTER' | egrep -v 'SECRET|KEY'
declare -x ACCOUNT_ID="..."
declare -x AWS_ACCOUNT_ID="..."
declare -x AWS_DEFAULT_REGION="ap-northeast-2"
declare -x AWS_PAGER=""
declare -x AWS_REGION="ap-northeast-2"
declare -x CLUSTER_NAME="myeks"

# 환경변수 설정
export KARPENTER_VERSION=v0.27.5
export TEMPOUT=$(mktemp)
echo $KARPENTER_VERSION $CLUSTER_NAME $AWS_DEFAULT_REGION $AWS_ACCOUNT_ID $TEMPOUT

# CloudFormation 스택으로 IAM Policy, Role, EC2 Instance Profile 생성 : 3분 정도 소요
curl -fsSL https://karpenter.sh/"${KARPENTER_VERSION}"/getting-started/getting-started-with-karpenter/cloudformation.yaml  > $TEMPOUT \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"

# 클러스터 생성 : myeks2 EKS 클러스터 생성 19분 정도 소요
eksctl create cluster -f - <<EOF
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: "1.24"
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}

iam:
  withOIDC: true
  serviceAccounts:
  - metadata:
      name: karpenter
      namespace: karpenter
    roleName: ${CLUSTER_NAME}-karpenter
    attachPolicyARNs:
    - arn:aws:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}
    roleOnly: true

iamIdentityMappings:
- arn: "arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes

managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10
  iam:
    withAddonPolicies:
      externalDNS: true

## Optionally run on fargate
# fargateProfiles:
# - name: karpenter
#  selectors:
#  - namespace: karpenter
EOF

# eks 배포 확인
eksctl get cluster
eksctl get nodegroup --cluster $CLUSTER_NAME
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
eksctl get addon --cluster $CLUSTER_NAME

# [터미널1] eks-node-viewer
cd ~/go/bin && ./eks-node-viewer

# k8s 확인
kubectl cluster-info
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -n kube-system -owide
kubectl describe cm -n kube-system aws-auth
...
mapRoles:
----
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:aws:iam::...:role/KarpenterNodeRole-myeks
  username: system:node:{{EC2PrivateDNSName}}
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:aws:iam::...:role/eksctl-myeks-nodegroup-myeks-ng-NodeInstanceRole-8QULZTOJ5C5D  username: system:node:{{EC2PrivateDNSName}}
...

# 카펜터 설치를 위한 환경 변수 설정 및 확인
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"
echo $CLUSTER_ENDPOINT $KARPENTER_IAM_ROLE_ARN

# EC2 Spot Fleet 사용을 위한 service-linked-role 생성 확인 : 만들어있는것을 확인하는 거라 아래 에러 출력이 정상!
# If the role has already been successfully created, you will see:
# An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation: Service role name AWSServiceRoleForEC2Spot has been taken in this account, please try a different suffix.
aws iam create-service-linked-role --aws-service-name spot.amazonaws.com || true

# docker logout : Logout of docker to perform an unauthenticated pull against the public ECR
docker logout public.ecr.aws

# karpenter 설치
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION} --namespace karpenter --create-namespace \
  --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${KARPENTER_IAM_ROLE_ARN} \
  --set settings.aws.clusterName=${CLUSTER_NAME} \
  --set settings.aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-${CLUSTER_NAME} \
  --set settings.aws.interruptionQueueName=${CLUSTER_NAME} \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait

# 확인
kubectl get-all -n karpenter
kubectl get all -n karpenter
kubectl get cm -n karpenter karpenter-global-settings -o jsonpath={.data} | jq
kubectl get crd | grep karpenter

EKS와 karpenter 설치를 완료하고 pod가 생성되는 걸 확인하기 위해 kubeopsview와 external-dns 설치도 진행했다.

# ExternalDNS
MyDomain=bs-yang.com
echo "export MyDomain=bs-yang.com" >> /etc/profile
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | 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 env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"

provisioner 생성을 진행하도록 한다. provisioner는 Karpenter의 핵심 구성 요소로, 워크로드의 요구 사항에 따라 클러스터에 노드를 프로비저닝하고, 확장 및 축소를 관리하게 된다. Provisioner는 노드 유형, 가용성 영역, 리소스 제한 등과 같은 정책을 사용하여 프로비저닝을 수행하는 구성 요소이다. provisioner의 정책에 따라 scale out 시 프로비저닝이 진행된다고 보면 된다.

#
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["spot"]
  limits:
    resources:
      cpu: 1000
  providerRef:
    name: default
  ttlSecondsAfterEmpty: 30
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: ${CLUSTER_NAME}
  securityGroupSelector:
    karpenter.sh/discovery: ${CLUSTER_NAME}
EOF

# 확인
kubectl get awsnodetemplates,provisioners
NAME                                        AGE
awsnodetemplate.karpenter.k8s.aws/default   2m35s

NAME                               AGE
provisioner.karpenter.sh/default   2m35s

Pod가 늘어나는 속도를 체감해보기 위해 프로메테우스와 그라파나를 설치하였다.

#
helm repo add grafana-charts https://grafana.github.io/helm-charts
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

kubectl create namespace monitoring

# 프로메테우스 설치
curl -fsSL https://karpenter.sh/"${KARPENTER_VERSION}"/getting-started/getting-started-with-karpenter/prometheus-values.yaml | tee prometheus-values.yaml
helm install --namespace monitoring prometheus prometheus-community/prometheus --values prometheus-values.yaml --set alertmanager.enabled=false

# 그라파나 설치
curl -fsSL https://karpenter.sh/"${KARPENTER_VERSION}"/getting-started/getting-started-with-karpenter/grafana-values.yaml | tee grafana-values.yaml
helm install --namespace monitoring grafana grafana-charts/grafana --values grafana-values.yaml --set service.type=LoadBalancer

# 그라파나 접속
kubectl annotate service grafana -n monitoring "external-dns.alpha.kubernetes.io/hostname=grafana.$MyDomain"
echo -e "grafana URL = http://grafana.$MyDomain"

Test App의 Replica을 0개로 배포하고 이후 Scale Out과 In을 테스트하면서 Scaling 속도가 얼마나 빠른지 지켜보았다.
우선은 Scale Out부터 진행하였는데 Replicas를 15개로 지정해보았다.
회사 랩탑 보안 상 영상으로 담지 못한 게 아쉬울 정도로 빠른 속도로 Pod 배포가 완료되었다. 순식간에 Spot Instance가 올라오고 Pod가 15개가 배포되는데 아주 짧은 시간밖에 걸리지 않았다.

# pause 파드 1개에 CPU 1개 최소 보장 할당
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1
EOF
kubectl scale deployment inflate --replicas 15
kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller

# 스팟 인스턴스 확인!
aws ec2 describe-spot-instance-requests --filters "Name=state,Values=active" --output table
kubectl get node -l karpenter.sh/capacity-type=spot -o jsonpath='{.items[0].metadata.labels}' | jq
kubectl get node --label-columns=eks.amazonaws.com/capacityType,karpenter.sh/capacity-type,node.kubernetes.io/instance-type
NAME                                                 STATUS   ROLES    AGE   VERSION                CAPACITYTYPE   CAPACITY-TYPE   INSTANCE-TYPE
ip-192-168-129-195.ap-northeast-2.compute.internal   Ready    <none>   51s   v1.24.13-eks-0a21954                  spot            c5d.4xlarge
ip-192-168-31-133.ap-northeast-2.compute.internal    Ready    <none>   38m   v1.24.13-eks-0a21954   ON_DEMAND                      m5.large
ip-192-168-40-55.ap-northeast-2.compute.internal     Ready    <none>   38m   v1.24.13-eks-0a21954   ON_DEMAND                      m5.large

반대로 Scale Down을 진행해보았다.
Deployment delete을 했고 바로 Spot Instance가 삭제되는 것을 확인할 수 있었다.

kubectl delete deployment inflate
kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller

아래는 그라파나를 통해 Scaling 상황을 캡쳐한 화면이다. 실시간으로 영상 촬영은 하지 못했지만 엄청 빠른 속도로 배포가 되고 또 제거가 된 것을 알 수 있었다.

8. 정리

VPA, HPA 그리고 CA와 Karpenter을 통해 k8s Scaling을 경험해볼 수 있었다.
최근에 Karpenter에 대한 얘기들이 많이 나오고 있어 궁금했던 차에 경험해 볼 수 있어서 좋았던 것 같다.
다만, spot instance을 사용하는 건 조금 운영상 불안한 측면이 있기 때문에 이 부분만 고려한다면 좋은 Scaling 구성을 진행할 수 있을 것 같다는 생각을 했다.