5주차 - 오토스케일링

개요

스케줄링 알고리즘
어떤 설정들을 할 수 있는지.

https://medium.com/@simardeep.oberoi/kubernetes-dynamic-resource-allocation-a-leap-in-resource-management-c39fdca6b99e
이거 알아보자.
inplace 수평확장도

카펜터는 필요사양도 알면서 스케줄링 조건도 이해한다..?
빈패킹 - 쿠버쪽도 확인

스케일링 종류

파드, 노드

HPA

HPA는 결국 스케일링의 기본이 되는 방식이라 잘 알아두는 것이 좋다.

환경

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

이걸 기본적인 스케일링 대상으로 삼는다.
이미지 이름만 봐도 알겠지만, 쿠버네티스에서 제공해주는 hpa 샘플용 이미지이다.

<?php
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
			$x += sqrt($x);
}
echo "OK!";
?>

http 요청을 보내면 자체적으로 연산을 하면서 cpu를 사용하게 돼있다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        averageUtilization: 50
        type: Utilization

hpa는 이렇게 만들었다.
잠시 해석하자면, cpu 사용률이 50퍼가 아닐 때 스케일링을 하겠다는 것이다.
위의 샘플은 cpu 요청량이 200m이며, 이것이 사용률을 계산할 때 분모로 들어가게 된다.
image.png
가만히 두면 이 상태가 될 것이다.
그라파나 대시보드는 22128을 사용했다.

스케일링 테스트 - 단일 파드

while true; do curl -s 파드; sleep 0.5; done

호스트로 들어가서 파드 ip에 지속적으로 요청을 날려본다.
image.png
조금 기다리다보면 이렇게 파드가 하나 늘어나게 되는 것을 확인할 수 있다.
image.png
대시보드로도 한번 사용률이 50퍼를 넘겼기 때문에 스케일업이 진행된 것이 확인된다.
image.png
그러나 여기에서 유의 깊게 볼 점은 더 이상 스케일링이 되지 않는다는 것이다.
현재 부하를 준 파드에선 요청 리소스(request) 200m의 절반을 넘기는 값으로 cpu 자원을 활용하고 있는 것이 보인다.
그렇지만 hpa는 현재 대상이 된 파드들의 전체 평균을 가지고 계산을 진행하기에, 한 파드의 지표가 아무리 높아도 전체적인 관점에서 사용률이 50퍼를 넘지 않기에 더 이상 스케일링이 되지 않는 것이다.
image.png
실제로도 hpa 기준에서는 현재 메트릭값이 35퍼로 잡히고 있다.

간단하게는 이렇게 계산해서 나오는 값이라는 것을 알 수 있다.

스케일링 테스트 - 전체 파드

	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"

이번에는 스케일링이 진행되도 모든 파드가 요청을 수행할 수 있도록 서비스로 요청을 날려본다.
image.png
잠시 기다리자 파드가 6개까지 늘어나게 됐다.
image.png
순간 사용률이 폭증하고, 이에 대해 파드를 순식간에 여러 개 늘린 것이 확인된다.
image.png
behavior 기본값에 따르면 15초 이내에 한번에 최대로 늘어나는 개수는 현재 개수의 2배이므로, 일단 2배가 늘어난 뒤에 최종적으로 계산된 값인 6개로 증가한 것이다.
image.png
부하를 중단하자, 스케일다운이 진행된 것이 보인다.
이때 실제 메트릭이 줄어든 시점은 11:50인 반면 레플리카가 줄어든 시점은 그 이후인 것을 볼 수 있는데, 이것은 안정화 윈도우에 의해 최대한의 유예가 주어진 것으로 볼 수 있다.

스케일링 테스트 - 안정화 윈도우값에 의한 점진적 스케일 다운

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: stable-window
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 12
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        averageUtilization: 50
        type: Utilization
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 180
      policies:
      - type: Pods
        value: 4
        periodSeconds: 30
      - type: Percent
        value: 10
        periodSeconds: 30
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

조금 더 안정화 윈도우와 동작을 면밀히 살피고자 세부 스펙을 더 정의했다.
image.png
생각보다 부하가 덜 발생하는 것 같아서, 다른 터미널을 띄우고 똑같은 명령어를 또 쳐서 부하가 두번 발생하게 만들었더니 최종적으로는 10개까지 레플리카가 늘어났다.
image.png
3분을 텀으로 안정화 윈도우를 적용했더니 일단 최대 레플리카였던 10개가 유지됐다.
image.png
그 이상을 넘어가자, 한번에 최대로 줄어들 수 있는 개수인 4개씩 줄어들기 시작한 것이 확인된다.

커스텀 메트릭 사용하기

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm show values >> values.yaml
# values.yaml에서 프메 서버 url 설정
helm install my-release prometheus-community/prometheus-adapter

이번에는 Prom-Adapter를 이용해 커스텀 메트릭으로 HPA를 해본다.
image.png
values 파일을 받아오는 이유는, 여기에서 프로메테우스 서버 주소를 명시해줘야 하기 때문이다.
image.png
배포가 되면 디플로이먼트를 통해 어댑터 파드가 하나 생긴다.

k get apiservices.apiregistration.k8s.io

image.png
또한 apiservice 오브젝트가 하나 생긴 것도 확인할 수 있다.
버전이 앞에 등장하는데, 실제 api를 쓸 때 이 값은 하위 경로로 들어가게 된다.

k get cm prom-adaptor-prometheus-adapter -oyaml

image.png
각종 룰들은 컨피그맵으로 생긴다.
이것을 직접 수정해서 가져오는 메트릭들을 조정하는 것도 가능하다.

    - seriesQuery: '{__name__=~"container_network_(receive|transmit)_packets_total",namespace!="",pod!=""}'
      resources:
        overrides:
          namespace:
            resource: namespace
          pod:
            resource: pod
      name:
        matches: ^container_network_(.*)_packets_total$
        as: "packets_per_second"
      metricsQuery: sum(rate(//.Series//{//.LabelMatchers//}[1m])) by (//.GroupBy//)

파드 단위로 오고가는 패킷을 메트릭으로 삼고자 이렇게 규칙을 작성했다.

 k get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/packets_per_second" | jq

내 커스텀 메트릭이 잘 만들어졌다면 이게 잘 돼야 한다.
image.png
석세스!

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: custom-metric
spec:
  minReplicas: 1
  maxReplicas: 10
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  metrics:
  - type: Pods
    pods:
      metric:
        name: packets_per_second
      target:
        type: AverageValue
        averageValue: 10000m

해당 값을 바로 이용해서 다시 HPA를 만들어본다.
image.png
다시 같은 방식으로 부하를 줄 때, 어마무시하게 빠르게 스케일링이 이뤄진다.
기준 값을 작게 설정한 것도 있지만, 스케일링된다고 해서 요청 수가 줄어드는 것도 아니라 평소 값이 크게 작아지지도 않는다.

replicaCount: 3
autoscaling:
  enabled: false

service:
  type: ClusterIP

ingress:
  enabled: true
  hostname: nginx.zerotay.com
  path: /
  pathType: Prefix
  annotations: 
    alb.ingress.kubernetes.io/certificate-arn: arn
    alb.ingress.kubernetes.io/group.name: aews
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-name: terraform-eks-ingress-alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/success-codes: 200-399
  ingressClassName: "alb"

metrics:
  enabled: true
  serviceMonitor:
    enabled: true

이번에는 웹 어플리케이션을 켜고, 이걸 토대로 커스텀 메트릭을 지정해보자.
헬름 values는 이렇게 세팅했다.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install nginx bitnami/nginx --version 19.0.0 -f values.yaml
k get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/nginx_http_requests" 
k get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/services/*/nginx_http_requests" 

위 헬름에서 서비스 모니터를 설정했기 때문에, 바로 관련 메트릭을 조회해보는 것이 가능하다.
파드를 대상으로 메트릭을 지정할 수도 있으나, 아예 서비스를 대상으로 조회를 하는 편이 조금 더 편리할 것이다.
(근데 사실 메트릭의 근원지가 같아서 결국 다를 건 없다.)

  - type: Object
    object:
      metric:
        name: nginx_http_requests
      describedObject:
        apiVersion: v1
        kind: Service
        name: nginx
      target:
        type: Value
        value: 10000m

hpa의 메트릭 필드는 이렇게 서비스를 대상으로 지정한다.
image.png
해당 값은 counter로, 그냥 total 값이라 줄어들지는 않는다.
아무튼 이렇게 다른 오브젝트의 메트릭을 활용하는 것도 가능하다!

KEDA

KEDA도 활용해보자.
이 녀석은 메트릭 기반이 아니라 이벤트 기반이라는 점에서 다르다고 할 수 있다.
하지만 내부적으로는 이벤트를 메트릭화시켜서 HPA를 구동한다.

나는 테라폼으로 세팅했다.

테스트

kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq

image.png
케다가 설치되면 알아서 externalmetric에 데이터를 노출할 준비를 해준다.

https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json
케다에서 제공하는 그라파나 대시보드를 활용하여 모니터링을 진행한다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron
spec:
  minReplicaCount: 0
  maxReplicaCount: 5
  pollingInterval: 30
  cooldownPeriod: 60
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers: 
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: "*/10 * * * *"  
      end: "5,15,25,35,45,55 * * * *"
      desiredReplicas: "1"

5분 단위로 1개를 만들었다 말았다 하는 크론 스케일러를 만들었다.
참고로 cooldownPeriod의 기본값이 5분이기 때문에 여기에서 명시적으로 더 작은 값으로 지정했다.
image.png
5분을 주기로 스케일링이 동작하는 것이 확인된다.

image.png
hpa의 스펙을 보면 이렇게 메트릭에 external이 들어간 것을 확인할 수 있다.

k get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/{네임스페이스}/{메트릭이름}?labelSelector=scaledobject.keda.sh%2Fname%3D{스케일오브젝트}"

이런 식으로 직접적으로 메트릭을 관찰하는 것도 가능한데, 기본적으로 케다는 이벤트 기반으로 메트릭을 내주다보니 직접적으로 메트릭을 확인한다는 것이 조금 이상한 일이기도하다.
다만 케다에서는
특히 크론 스케일러는 확인하기가 정말 어려웠다.
image.png
만들어진 HPA에서도 이런 이벤트가 계속 발생하나, 실제로 동작은 잘 되고 있다.

vpa

이건 인플레이스를 쓰고 싶어서, 온프렘에서 하려고 하는데 최근에 노드간 통신 이슈가 있어서 조금 수정하고 해보려 한다.

Cluster Autoscaler

Cluster Autoscaler를 사용해보자.

세팅

관리되고자 하는 노드에는 다음의 태그가 들어가있어야 한다.

k8s.io/cluster-autoscaler/enabled : true
k8s.io/cluster-autoscaler/myeks : owned

image.png
이것은 관리대상이 되는 노드를 자동으로 탐색하기 위한 태그인데, 커스텀할 수 있기는 하다.

curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml

image.png
양식 파일을 다운 받아보면 맨 아랫줄에 오토스케일러 디플로이먼트 인자 설정을 할 수 있다.
auto discovery 인자에서 태그를 커스텀하면 된다.

sed -i -e "s|<YOUR CLUSTER NAME>|$CLUSTER_NAME|g" cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict="false"

오토스케일러가 배포된 노드는 삭제되지 않도록 축출을 막는 어노테이션을 미리 달아준다.
image.png
그런데 나는 이러한 에러를 겪었다.
비슷한 에러를 겪는 사람들이 있었는데, 특정 ami에서 발생하는 에러로 보인다.[1]

helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler -n kube-system -f cluster-autoscaler-helm-values.yaml

이건 그냥 정말 개발 로직 상 버그인 것 같기도 한데, 헬름에서는 관련한 설정을 넣어줄 수 있는 것으로 보여 이쪽으로 방향을 선회했다.

autoDiscovery:
  clusterName: terraform-eks
  tags:
    - k8s.io/cluster-autoscaler/enabled
    - k8s.io/cluster-autoscaler/terraform-eks
awsRegion: ap-northeast-1
cloudProvider: aws
deployment:
  annotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
extraArgs:
  logtostderr: true
  stderrthreshold: info
  v: 4
  skip-nodes-with-local-storage: false
  expander: least-waste
  skip-nodes-with-system-pods: false

rbac:
  create: true
  serviceAccount:
    name: cluster-autoscaler
    annotations: 
      eks.amazonaws.com/role-arn: {irasa role!}

service:
  create: true

serviceMonitor:
  enabled: true
  namespace: monitoring

values 파일에서는 이 정도 커스텀을 해줬다.
프로메테우스 지표 수집을 하는 것도 가능해보이길래 해당 설정도 넣었는데, 추가적인 설정을 넣지 않은 건지 실제로 메트릭이 수집되지는 않았다.
image.png
만들어진 서비스모니터는 이 경로로 데이터를 긁어오는데..
image.png
실제 파드에는 관련 포트 정보가 없다.
아무래도 추가 설정을 해줘야 하는 모양인데 현재의 주제에서 조금 벗어나는 것 같아 생략했다.
image.png
제대로 설정이 됐다면 이런 식으로 노드와 파드 상태를 계속 확인하는 로그가 남아야 한다.
image.png
또한 컨피그맵으로 현재 상태 볼 수 있게 된다.

테스트 실패

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

한 파드당 코어의 절반을 잡아먹는 괴물 디플로이먼트를 만들어본다.
이 친구를 스케일 아웃하면..

kubectl scale --replicas=15 deployment/nginx-to-scaleout && date

image.png
당장 스케줄링될 수 없는 다수의 파드가 pending 상태로 머물러 버린다.
image.png
그러나 막상 로그를 확인해보니 또 이런 이슈가 있다.
인스턴스 id를 잘못 참조하는 것이 아닐까 싶다.
관련한 이슈가 지속적으로 보이는데, 문제가 많은 것 같다.[2]
image.png
로그를 더 파다보니 파드에서는 이렇게 이벤트가 발생하는 것을 확인했다.
이것으로 인해 스케일업이 트리거되지 않는다.
그러나 이런 이슈가 있는 경우는 asg를 디스커버리하지 못할 때 발생한다고 나오던데, asg에도 태그가 잘 붙어있고 컨트롤러 파드의 인자에도 제대로 값이 들어가 있는 것을 확인했다.
image.png
혹시 몰라 공백문자가 있는지도 확인했고..
image.png
버전을 낮춰보기도 했고..
image.png
IRSA 설정이 잘못됐나도 확인해보고..

간단하게 확인하고 행복하게 카펜터로 넘어가는 그림을 기대했으나, 생각보다 녹록치가 않다.
지금으로서 마음에 걸리는 한 가지는, ami 이슈.
단순히 양식파일로 설치할 때 다른 사람들이 al2023 standard가 아니라 al2023을 사용해야 예제 양식 파일이 적용된다는 이슈를 달았던 것을 토대로 생각해볼 때, ami도 영향을 주는 요소가 아닌가 한다.
난감한 게 어디가 정확하게 문제인지 짚기가 힘들다는 것.
image.png
image.png
인스턴스를 못 찾는 게 문제냐?
image.png
파드에서 not scale up을 거는 게 문제냐?
궁극적으로는 같은 원인에서 다른 방향으로 이런 결과를 내는 것 같지만, 그 원인을 트러블슈팅하는 게 생각보다 쉽지 않다.

카펜터를 파보고, 조금 시간이 남을 때 재도전을 해볼까 한다.
image.png
프로메테우스로 메트릭 수집하는 것도 성공했는데, 에러가 하나도 없다고 합니다 하하..

cpa

비례.

카펜터

Karpenter에 정말 열심히 문서를 정리했다..
이제 실습을 하면서 어떤 식으로 동작하는지, 어떻게 쓰는 게 좋은지 확인할 시간이다.

테라폼 설치

https://github.com/terraform-aws-modules/terraform-aws-eks/tree/v20.33.1/modules/karpenter
이 모듈을 최대한 활용해보자.
물론 문제가 많이 생길 수 있기에, 가능한 참조만 한다.
image.png
그냥 하니까 이런 에러가 발생한다.
image.png
아니 이제 보니까 create 변수도 true 해줘야 제대로 생성되게 설정돼있네..
create 변수는 왜 있는 걸까?
어차피 개별 리소스에 대해서도 create인자가 있는데.. 모든 create 리소스 변수들을 활성화하기 위한 변수로서 두는 건가..

확인

image.png
기본적으로 이벤트브릿지로 인스턴스에 관련한 이벤트를 받을 수 있도록 설정된다.
image.png
image.png
각 룰은 각 이벤트를 받도록 돼있고, 이 이벤트를 SQS로 보낸다.
image.png
그에 맞는 SQS 또한 만들어지는데, 이 큐에서 카펜터가 이벤트를 받아서 보게 될 것이다.

https://artifacthub.io/packages/helm/aws-karpenter/karpenter
헬름 설치를 할 때, ECR을 활용한다.
image.png

단순 설치를 하니 클러스터 네임이 없다고 화를 낸다.
image.png
기본 프롬스택에서는 프로메테우스 쪽에 추가 설정을 넣었다.
이 값은 문서에 나와있다.[3]
그라파나 대시보드도 나와있으나[4], 현재 사용하고 있는 프로메테우스 스택의 values.yaml 파일에서는 설정할 수 있는 방법이 없다.
아예 프롬스택에서 그라파나를 빼고 따로 설치할까 하다가, 고작 대시보드 2개 하자고 분리하는 것도 좀 별로다 싶어서 그라파나 대시보드는 프로비저닝된 이후 직접 세팅하도록 한다.

https://karpenter.sh/v1.3/getting-started/getting-started-with-karpenter/karpenter-capacity-dashboard.json
https://karpenter.sh/v1.3/getting-started/getting-started-with-karpenter/karpenter-performance-dashboard.json

다음의 두 대시보드를 받으면 된다.
(깃헙 레포를 가보면 다른 json 파일도 있는데 하나는 컨트롤러 전체에 관한 거고, 다른 하나는 과거 버전을 기반으로 하는지 메트릭이 없으므로 주의하자)
image.png
설정이 제대로 된다면 알아서 디스커버리를 통해 메트릭을 수집하게 된다.
image.png
이미 한번 테스트한 상태라 메트릭이 찍힘 대시보드도 성공적으로 구축된 것이 확인된다.

테스트

현 실습에서는 카펜터를 쓰지 않고 있다가 적용하는 상황을 가정한다.
이에 따라 기존 인스턴스들은 유지한 채로 카펜터에게 관리를 맡기다가 점차 관리를 확장시키는 방식으로 실습해볼까 한다.

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "카펜터를 만들 때 만들어진 node iam role"
  amiSelectorTerms:
    - alias: "al2023@latest" # ex) al2023@latest
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "terraform-eks"
		Name: "*Public*"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "terraform-eks"
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - 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-generation
          operator: Gt
          values: ["2"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h # 30 * 24h = 720h
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m

노드풀과 노드 클래스를 만들어본다.
노드 클래스에는 퍼블릭 서브넷에만 배치되도록 세팅했고, 스팟 인스턴스를 활용해본다.
image.png
image.png
제대로 세팅이 됐다면, 이렇게 True가 돼야 한다.
image.png
준비 상태는 검증이 성공해야 하는데, 검증 상태 역시 위의 다른 상태들이 준비 상태가 돼야 통과되므로 준비 상태가 된 것만으로도 모든 것들이 제대로 추적이 되고 있다는 것을 확인할 수 있다.

프로비저닝

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  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
        securityContext:
          allowPrivilegeEscalation: false

이번에도 괴물 파드를 만들어본다.
image.png
cpu 요청량이 크므로 스케줄이 되지 않는 파드가 발생한다.
image.png
만약 한번도 스팟을 만들어본 적이 없다면 service linked role이 없어서이러한 에러가 발생하기에 위에서 세팅을 해준 것이다.
image.png
1분도 안 되어 한 노드가 새로 프로비저닝되기 시작했다.
image.png
파드가 배치되는 데에도 시간이 거의 걸리지 않았다.
image.png
컨트롤러의 로그를 보면, 도움이 필요한 파드를 찾은 후 노드 클레임을 작동시키고 등록하는 과정이 이뤄진 것이 보인다.

디프로비저닝

image.png
이번에는 파드를 하나 줄여보았다.
처음에는 c5a.xlarge의 노드를 배치해주었는데, 더 작은 스펙의 노드로 바꿔주면 성공이다.
image.png
시간이 지나자 새로운 노드가 또 프로비저닝됐다.
image.png
조금 더 시간이 지나니 이렇게 한 노드가 사라졌다.
image.png
로그로는 underutilzed 효율화를 위해 노드를 중단한다는 말이 뜬 이후, 새로운 노드클레임 생성, 이후 노후 노드 삭제 절차를 밟는 것이 보인다.
image.png
결국 c5a.xlarge에서 c5a.large로 스펙을 줄여주었다!
image.png
중간에 이런 이벤트가 관찰됐다.
이 이벤트가 의미하는 게 15개의 인스턴스 타입 선택지를 주지 않으면 Spot2Spot을 안 해준다는 것으로 이해했었는데, 지금 보면 결국 해주기는 한다.
다만 효율적인 비용최적화를 하지 못할 가능성이 높다는 의미인 듯하다.
(그럼 왜 Unconsolidatable이냐구)
image.png
클라우드트레일에 들어가서 이벤트를 보면, CreateFleet 이벤트가 많이 뜨는 것을 확인할 수 있다.
이때 카펜터는 비용 절감 방안을 찾기 위해 드라이런으로도 api를 요청하는 모양이다.

파드 스케줄링 제약 조건 체크

    nodeSelector:
      topology.kubernetes.io/zone: ap-northeast-2a
      karpenter.k8s.aws/instance-cpu: "8"

파드를 만들면서 well-known 라벨을 이용해 노드에 배치되도록 해본다.
image.png
곧바로 설정한 제약을 준수하는 새로운 노드가 프로비저닝됐다.
image.png
추가적으로, 바로 통합이 진행되어 처음 테스트할 때 만들어졌던 노드가 삭제되며 기존의 파드가 새로운 노드로 이전됐다.
노드 스케줄링 제약 조건이 존재하는 상황에서, 제약에서 자유로운 파드를 아예 옮기는 식으로 효율적으로 비용을 최적화한 것이다.
이정도 똑똑함이라면 충분히 사람이 직접 비용을 최적화하기 위해 머리 싸매고 운영하는 수고로움을 덜 수 있을 것으로 생각된다.

스터디

alb컨에서 포트에 이름을 쓸 때 이슈가 있다.

ksm에 커스텀 메트릭.
이건 컨맵으로 들어간다.

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

# 변수 지정
CLUSTER_NAME=myeks
SSHKEYNAME=zero
MYACCESSKEY=AKIAR6VA7X35OEY5IJZY
MYSECRETKEY=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

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

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

# kubeconfig 생성
aws sts get-caller-identity --query Arn
aws eks update-kubeconfig --name myeks --user-alias arn:aws:iam::134555352826:user/aews-admin
aws eks update-kubeconfig --name myeks --user-alias admin

# 
kubectl ns default
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -A
kubectl get pdb -n kube-system
# EC2 공인 IP 변수 지정
export N1=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2a" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N2=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2b" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N3=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2c" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
echo $N1, $N2, $N3

# *remoteAccess* 포함된 보안그룹 ID
aws ec2 describe-security-groups --filters "Name=group-name,Values=*remoteAccess*" | jq
export MNSGID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=*remoteAccess*" --query 'SecurityGroups[*].GroupId' --output text)

# 해당 보안그룹 inbound 에 자신의 집 공인 IP 룰 추가
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr $(curl -s ipinfo.io/ip)/32

# 해당 보안그룹 inbound 에 운영서버 내부 IP 룰 추가
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr 172.20.1.100/32

# 워커 노드 SSH 접속
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh -o StrictHostKeyChecking=no ec2-user@$i hostname; echo; done

이건 ssh해서

# default 네임스페이스 적용
kubectl ns default

# 환경변수 정보 확인
export | egrep 'ACCOUNT|AWS_|CLUSTER|KUBERNETES|VPC|Subnet'
export | egrep 'ACCOUNT|AWS_|CLUSTER|KUBERNETES|VPC|Subnet' | egrep -v 'KEY'

# krew 플러그인 확인
kubectl krew list

# 인스턴스 정보 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table

# 노드 IP 확인 및 PrivateIP 변수 지정
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3

# 노드 IP 로 ping 테스트
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ping -c 1 $i ; echo; done

이건 내컴

cat << EOF >> ~/.zshrc
# eksworkshop
export CLUSTER_NAME=myeks
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
export N1=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node" "Name=availability-zone,Values=ap-northeast-2a" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N2=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node" "Name=availability-zone,Values=ap-northeast-2b" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N3=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node" "Name=availability-zone,Values=ap-northeast-2c" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
MyDomain=zerotay.com
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "$MyDomain." --query "HostedZones[0].Id" --output text)
EOF

# [신규 터미널] 확인
echo $CLUSTER_NAME $VPCID $PubSubnet1 $PubSubnet2 $PubSubnet3
echo $N1 $N2 $N3 $MyDomain $MyDnzHostedZoneId
tail -n 15 ~/.zshrc
# AWS LoadBalancerController
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller

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

# gp3 스토리지 클래스 생성
cat <<EOF | kubectl apply -f -
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gp3
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
  fsType: xfs # 기본값이 ext4
EOF
kubectl get sc


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

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

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

# 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하니, 4주차 실습과 다르게 PV/PVC 미사용
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    scrapeInterval: "15s"
    evaluationInterval: "15s"
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"
  
  # Enable vertical pod autoscaler support for prometheus-operator
  verticalPodAutoscaler:
    enabled: true

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

grafana:
  defaultDashboardsTimezone: Asia/Seoul
  adminPassword: 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


#
kubectl get targetgroupbindings.elbv2.k8s.aws -A


# 
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

관련 문서

이름 noteType created

참고


  1. https://github.com/kubernetes/autoscaler/issues/7389 ↩︎

  2. https://github.com/kubernetes/autoscaler/issues/6096 ↩︎

  3. https://github.com/aws/karpenter-provider-aws/blob/main/website/content/en/docs/getting-started/getting-started-with-karpenter/prometheus-values.yaml ↩︎

  4. https://github.com/aws/karpenter-provider-aws/blob/main/website/content/en/docs/getting-started/getting-started-with-karpenter/grafana-values.yaml ↩︎