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 구성을 진행할 수 있을 것 같다는 생각을 했다.

AEWS Study #4 – EKS Observability

AEWS 4회차는 EKS Observability에 대해 다룬다. 이전에 kOps 때 다뤘던 모니터링 및 대쉬보드에 대한 내용을 주로 다룰 예정이다.

0. 환경 구성

환경 구성은 매번 실습 때 가시다님이 제공해주시는 One Click Yaml을 통해 배포를 진행한다.
이번 실습 때는 이전에 시도했다가 성공하지 못한 Cross-Account ExternalDNS을 통해 진행하려고 한다. Cross-Account ExternalDNS 설정은 아래와 같이 진행할 예정이다. 여기서 Route53 Account는 Route53 Record을 관리하는 Account이고 EKS Account는 실습에 사용하는 EKS을 배포하는 Account이다.

EKS Account에 Policy&Role을 생성한다.
Policy는 Route53 Account에서 만들 Role에 대해 AssumeRole하는 Action으로 만든다.
Role은 Trust relationships을 작성할 때 EKS에서 확인 가능한 OIDC Provider URL을 참고해서 입력하고 Permission Policy는 위에서 만든 Policy을 선택한다

#Policy 생성
#Policy Name : external-dns-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::ROUTE53Account:role/external-dns-cross-account"
        }
    ]
}

#Role 생성
#Role Name : external-dns
#Trust relationships
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": {
				"AWS": "arn:aws:iam::EKSAccount:root"
			},
			"Action": "sts:AssumeRole",
			"Condition": {}
		},
		{
			"Effect": "Allow",
			"Principal": {
				"Federated": "arn:aws:iam::EKSAccount:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/2FD..."
			},
			"Action": "sts:AssumeRoleWithWebIdentity",
			"Condition": {
				"StringEquals": {
					"oidc.eks.ap-northeast-2.amazonaws.com/id/2FD...:sub": "system:serviceaccount:kube-system:external-dns"
				}
			}
		}
	]
}

Route53 Account에도 Role을 생성해준다.
Permission Policy는 나는 AmazonRoute53FullAccess을 줬지만 필요에 따라 최소한의 정책을 설정해줘도 된다. 그리고 Trust relationships 작성 시에 EKSAccount의 위에서 만든 Role인 external-dns Role에 대해 sts:AssumeRole 허용해줬다.

#Role 생성
#Role Name : external-dns-cross-account
#Trust relationships
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::EKSAccount:role/external-dns"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

위와 같이 Role을 만들어줬다면 externaldns 배포에 사용되는 yaml 파일을 수정한 후 배포를 진행하면 된다.
ServiceAccount kind에서 annotations 부분에 EKSAccount의 Role ARN을 입력해준다.
Deployment kind에서는 Containers args 부분을 찾은 뒤 Route53 Account의 Role ARN을 입력해준다.

#externaldns.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
  #아래 내용 삽입
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::EKSAccount:role/external-dns
  #
  labels:
    app.kubernetes.io/name: external-dns
.......
apiVersion: apps/v1
kind: Deployment

          args:
            - --source=service
            - --source=ingress
            - --domain-filter=${MyDomain} # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
            - --provider=aws
            #아래 내용 삽입
            - --aws-assume-role=arn:aws:iam::Route53Account:role/external-dns-cross-account
            #
            #- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
            - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
            - --registry=txt
            - --txt-owner-id=${MyDnzHostedZoneId}
...........

이후 ExternalDNS 배포를 진행하면 Cross Account ExternalDNS을 배포할 수 있게 된다.
이때 Route53 Hosted Zone ID는 수동으로 입력해주었다.
ExternalDNS 및 kube-ops-view을 설치 후 ExternalDNS Log을 확인해보면 제대로 조회 및 등록이 된 것을 확인할 수 있다. 해당 URL로 접근하면 페이지도 제대로 나오는 것을 알 수 있다.

# ExternalDNS
MyDomain=bs-yang.com
echo "export MyDomain=bs-yang.com" >> /etc/profile
MyDnzHostedZoneId=Z030...
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"

# Log 확인
kubectl logs -n kube-system external-dns-
time="2023-05-16T06:56:38Z" level=info msg="Instantiating new Kubernetes client"
time="2023-05-16T06:56:38Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2023-05-16T06:56:38Z" level=info msg="Created Kubernetes client https://10.100.0.1:443"
time="2023-05-16T06:56:38Z" level=info msg="Assuming role: arn:aws:iam::Route53Account:role/external-dns-cross-account"
time="2023-05-16T06:56:39Z" level=info msg="Applying provider record filter for domains: [bs-yang.com. .bs-yang.com.]"
time="2023-05-16T06:56:40Z" level=info msg="All records are already up to date"
time="2023-05-16T06:57:39Z" level=info msg="Applying provider record filter for domains: [bs-yang.com. .bs-yang.com.]"
time="2023-05-16T06:57:39Z" level=info msg="Desired change: CREATE cname-kubeopsview.bs-yang.com TXT [Id: /hostedzone/Z030...]"
time="2023-05-16T06:57:39Z" level=info msg="Desired change: CREATE kubeopsview.bs-yang.com A [Id: /hostedzone/Z030...]"
time="2023-05-16T06:57:39Z" level=info msg="Desired change: CREATE kubeopsview.bs-yang.com TXT [Id: /hostedzone/Z030...]"
time="2023-05-16T06:57:40Z" level=info msg="3 record(s) in zone bs-yang.com. [Id: /hostedzone/Z030...] were successfully updated"

LB Controller와 EBS csi driver, gp3 sc, EFS sc 등을 설치 및 설정하면서 환경 설정을 마무리한다.

# AWS LB Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
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

# EBS csi driver 설치 확인
eksctl get addon --cluster ${CLUSTER_NAME}
kubectl get pod -n kube-system -l 'app in (ebs-csi-controller,ebs-csi-node)'
kubectl get csinodes

# gp3 스토리지 클래스 생성
kubectl get sc
cat <<EOT > gp3-sc.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gp3
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
EOT
kubectl apply -f gp3-sc.yaml
kubectl get sc

# EFS csi driver 설치
helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
    --namespace kube-system \
    --set image.repository=602401143452.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/eks/aws-efs-csi-driver \
    --set controller.serviceAccount.create=false \
    --set controller.serviceAccount.name=efs-csi-controller-sa

# EFS 스토리지클래스 생성 및 확인
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml
sed -i "s/fs-92107410/$EfsFsId/g" storageclass.yaml
kubectl apply -f storageclass.yaml
kubectl get sc efs-sc

1. EKS Console

EKS에 대한 옵저빌리티 중 EKS Console에서 확인하는 내용이다. EKS라고 이름은 되어있지만 EKS Console 상에서 확인하는 대부분의 내용은 k8s API을 통해 호출된다.
예전에 혼자 EKS을 배포하는 테스트를 할 때 EKS 배포는 했는데 EKS Console에서 상세한 내용을 확인할 수 없는 문제가 있었다. 분명 Admin User였어서 권한의 문제는 없어야하는데 아래와 같은 메시지가 나오면서 확인되지 않는 부분이 있었다.


이런 메시지가 나오는 이유는 아무리 AdminAccess 권한을 갖고 있더라도 eks cluster role을 부여받지 않으면 저런 메시지가 호출되게 된다.
물론 EKS 배포에 사용한 iam user의 경우 자신이 EKS Cluster Owner이기 때문에 해당 권한을 갖고있지 않더라도 EKS Console 및 kubectl 등 명령어를 통해 확인하는데는 문제가 없다.
해당 Admin User에 EKS Rolebinding을 해주면 문제 없이 EKS Console 등에서 확인이 가능하다.
EKS Console 에서 확인 가능한 내용은 다양하다. Pod 정보를 포함하여 k8s에서 조회가 가능한 대부분의 내용을 확인할 수 있다고 보면 된다.


2. Logging in EKS

EKS에서의 Logging의 경우 CP, node, App 등에 대한 로깅이 가능하다.
CP 로깅 종류는 API Server, Authenticator, Audit, Controller Manager, Scheduler가 있다. 기본적으로는 로깅 Off로 되어있고 이걸 aws cli을 통해 활성화 할 수 있다.
CP 로깅의 자세한 내용은 아래 링크를 참고 가능하다.
https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/control-plane-logs.html

# 모든 로깅 활성화
aws eks update-cluster-config --region $AWS_DEFAULT_REGION --name $CLUSTER_NAME \
    --logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'

# 로그 그룹 확인
aws logs describe-log-groups | jq

# 로그 tail 확인 : aws logs tail help
aws logs tail /aws/eks/$CLUSTER_NAME/cluster | more

# 신규 로그를 바로 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --follow

# 필터 패턴
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --filter-pattern <필터 패턴>

# 로그 스트림이름
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix <로그 스트림 prefix> --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix kube-controller-manager --follow
kubectl scale deployment -n kube-system coredns --replicas=1
kubectl scale deployment -n kube-system coredns --replicas=2

# 시간 지정: 1초(s) 1분(m) 1시간(h) 하루(d) 한주(w)
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m

# 짧게 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m --format short

Container Pod 로깅도 가능하다. Pod 로깅을 테스트하기 위해 Helm에서 nginx 서버를 배포한다.

# NGINX 웹서버 배포
helm repo add bitnami https://charts.bitnami.com/bitnami

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

# 도메인 확인
echo $MyDomain

# 파라미터 파일 생성
cat <<EOT > nginx-values.yaml
service:
    type: NodePort

ingress:
  enabled: true
  ingressClassName: alb
  hostname: nginx.$MyDomain
  path: /*
  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: $CLUSTER_NAME-ingress-alb
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/ssl-redirect: '443'
EOT
cat nginx-values.yaml | yh

# 배포
helm install nginx bitnami/nginx --version 14.1.0 -f nginx-values.yaml

# 확인
kubectl get ingress,deploy,svc,ep nginx
kubectl get targetgroupbindings # ALB TG 확인

# 접속 주소 확인 및 접속
echo -e "Nginx WebServer URL = https://nginx.$MyDomain"
curl -s https://nginx.$MyDomain
kubectl logs deploy/nginx -f

# (참고) 삭제 시
helm uninstall nginx

설치하고 브라우저에서 정상적으로 페이지가 열리는 것을 확인했다면 별도의 모니터링용 터미널을 하나 더 띄우고 반복접속과 모니터링 결과를 확인한다. 접속이 하나 들어올 때마다 로그에도 내용이 추가되는 것을 확인할 수 있었다. 이렇게 간략하게 Pod의 Application 로그를 확인하는 내용을 진행해보았다.

# 반복 접속
while true; do curl -s https://nginx.$MyDomain -I | head -n 1; date; sleep 1; done

# 로그 모니터링
kubectl logs deploy/nginx -f

# 컨테이너 로그 파일 위치 확인
kubectl exec -it deploy/nginx -- ls -l /opt/bitnami/nginx/logs/
total 0
lrwxrwxrwx 1 root root 11 Apr 24 10:13 access.log -> /dev/stdout
lrwxrwxrwx 1 root root 11 Apr 24 10:13 error.log -> /dev/stderr

3. Container Insights metrics in Amazon CloudWatch & Fluent Bit (Logs)

CCI(CloudWatch Container Insight)는 노드에 Cloudwatch Agent 파드와 Fluent Bit 파드가 데몬셋으로 배치되어 Metrics와 Log을 수집할 수 있다.
Fluent Bit는 Cloudwatch Logs에 로그를 전송하기 위한 데몬셋이다.
Fluent Bit 데몬셋으로 로그를 수집하고 시각화하는 과정은 3단계로 작동하게 되는데, 수집-저장-시각화의 단계로 이루어진다.
[수집] : Fluent Bit 파드를 데몬셋으로 동작시키고 아래 3가지 종류의 로그를 CW Logs에 전송
1) /aws/containerinsights/Cluster_Name/application : /var/log/containers에 포함 된 모든 로그 파일과 각 컨테이너/파드 로그
2) /aws/containerinsights/Cluster_Name/host : /var/log/dmesg, /var/log/secure, /var/log/message의 로그 파일들과 노드 로그
3) /aws/containerinsights/Cluster_Name/dataplane : /var/log/journal의 로그 파일과 k8s dataplane 로그
[저장] : CW Logs에 로그를 저장한다. 로그 그룹 별 로그 보존 기간 설정 가능하다.
[시각화] : CW의 LogsInsights를 사용하여 대상 로그를 분석하고 CW의 대쉬보드를 시각화한다.
간략하게 보기 위한 그림은 아래와 같다.

이후 로그들을 열어보기 위해 사전에 Node의 로그 위치를 확인해 본다.
Application, host, dataplane 위치를 각각 확인해본다.

# Application 로그 위치 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/containers; echo; done
>>>>> 192.168.3.182 <<<<<
/var/log/containers
├── aws-load-balancer-controller-6fb4f86d9d-r9tch_kube-system_aws-load-balancer-controller-8de8d0ceaa922575f336ded6a7b5e30f43656765829d268ddb01f36539e80cb3.log -> /var/log/pods/kube-system_aws-load-balancer-controller-6fb4f86d9d-r9tch_48c648fb-83c1-499e-9453-e0ddb65cf088/aws-load-balancer-controller/0.log
├── aws-node-955f2_kube-system_aws-node-c36d95a4abb2e33245350b1acf670b63208e7ad0353b87e39d10a6c32904f643.log -> /var/log/pods/kube-system_aws-node-955f2_fe7aaf5b-f4bd-425b-a099-cdc8821c6844/aws-node/1.log
├── aws-node-955f2_kube-system_aws-node-d05cfd440860db1074b84ead88b5dd79575ded2b078bcb3ca2cb5d2bd1094ecc.log -> /var/log/pods/kube-system_aws-node-955f2_fe7aaf5b-f4bd-425b-a099-cdc8821c6844/aws-node/0.log
...
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -al /var/log/containers; echo; done
>>>>> 192.168.3.182 <<<<<
total 16
drwxr-xr-x  2 root root 8192 May 20 03:54 .
drwxr-xr-x 10 root root 4096 May 17 00:02 ..
lrwxrwxrwx  1 root root  143 May 17 01:20 aws-load-balancer-controller-6fb4f86d9d-r9tch_kube-system_aws-load-balancer-controller-8de8d0ceaa922575f336ded6a7b5e30f43656765829d268ddb01f36539e80cb3.log -> /var/log/pods/kube-system_aws-load-balancer-controller-6fb4f86d9d-r9tch_48c648fb-83c1-499e-9453-e0ddb65cf088/aws-load-balancer-controller/0.log
lrwxrwxrwx  1 root root   92 May 17 00:02 aws-node-955f2_kube-system_aws-node-c36d95a4abb2e33245350b1acf670b63208e7ad0353b87e39d10a6c32904f643.log -> /var/log/pods/kube-system_aws-node-955f2_fe7aaf5b-f4bd-425b-a099-cdc8821c6844/aws-node/1.log
...

# host 로그 위치 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/ -L 1; echo; done
>>>>> 192.168.3.182 <<<<<
/var/log/
├── amazon
├── audit
├── aws-routed-eni
...
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -la /var/log/; echo; done
>>>>> 192.168.3.182 <<<<<
total 4132
drwxr-xr-x  10 root   root               4096 May 17 00:02 .
drwxr-xr-x  19 root   root                268 May 17 01:21 ..
drwxr-xr-x   3 root   root                 17 May 15 01:33 amazon
drwx------   2 root   root                 61 May 20 01:18 audit
drwxr-xr-x   2 root   root                 69 May 15 01:34 aws-routed-eni
...

# dataplane 로그 위치 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/journal -L 1; echo; done
>>>>> 192.168.3.182 <<<<<
/var/log/journal
├── ec2179c4f3e906eda92ce733733bd5d0
└── ec221453e78b0818ba3c9f00233580cb

Cloudwatch Container Insight를 설치해서 로그 수집을 하고 내용을 확인해볼 예정이다.

# 설치
FluentBitHttpServer='On'
FluentBitHttpPort='2020'
FluentBitReadFromHead='Off'
FluentBitReadFromTail='On'
curl -s https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed 's/{{cluster_name}}/'${CLUSTER_NAME}'/;s/{{region_name}}/'${AWS_DEFAULT_REGION}'/;s/{{http_server_toggle}}/"'${FluentBitHttpServer}'"/;s/{{http_server_port}}/"'${FluentBitHttpPort}'"/;s/{{read_from_head}}/"'${FluentBitReadFromHead}'"/;s/{{read_from_tail}}/"'${FluentBitReadFromTail}'"/' | kubectl apply -f -

# 설치 확인
kubectl get-all -n amazon-cloudwatch
kubectl get ds,pod,cm,sa -n amazon-cloudwatch
kubectl describe clusterrole cloudwatch-agent-role fluent-bit-role                          # 클러스터롤 확인
kubectl describe clusterrolebindings cloudwatch-agent-role-binding fluent-bit-role-binding  # 클러스터롤 바인딩 확인
kubectl -n amazon-cloudwatch logs -l name=cloudwatch-agent -f # 파드 로그 확인
kubectl -n amazon-cloudwatch logs -l k8s-app=fluent-bit -f    # 파드 로그 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ss -tnlp | grep fluent-bit; echo; done

# cloudwatch-agent 설정 확인
kubectl describe cm cwagentconfig -n amazon-cloudwatch
{
  "agent": {
    "region": "ap-northeast-2"
  },
  "logs": {
    "metrics_collected": {
      "kubernetes": {
        "cluster_name": "myeks",
        "metrics_collection_interval": 60
      }
    },
    "force_flush_interval": 5
  }
}

# Fluent Bit Cluster Info 확인
kubectl get cm -n amazon-cloudwatch fluent-bit-cluster-info -o yaml | yh
apiVersion: v1
data:
  cluster.name: myeks
  http.port: "2020"
  http.server: "On"
  logs.region: ap-northeast-2
  read.head: "Off"
  read.tail: "On"
kind: ConfigMap
...

# Fluent Bit 로그 INPUT/FILTER/OUTPUT 설정 확인
## 설정 부분 구성 : application-log.conf , dataplane-log.conf , fluent-bit.conf , host-log.conf , parsers.conf
kubectl describe cm fluent-bit-config -n amazon-cloudwatch
...
application-log.conf:
----
[INPUT]
    Name                tail
    Tag                 application.*
    Exclude_Path        /var/log/containers/cloudwatch-agent*, /var/log/containers/fluent-bit*, /var/log/containers/aws-node*, /var/log/containers/kube-proxy*
...

[FILTER]
    Name                kubernetes
    Match               application.*
    Kube_URL            https://kubernetes.default.svc:443
...

[OUTPUT]
    Name                cloudwatch_logs
    Match               application.*
    region              ${AWS_REGION}
    log_group_name      /aws/containerinsights/${CLUSTER_NAME}/application
...

# (참고) 삭제
curl -s https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed 's/{{cluster_name}}/'${CLUSTER_NAME}'/;s/{{region_name}}/'${AWS_DEFAULT_REGION}'/;s/{{http_server_toggle}}/"'${FluentBitHttpServer}'"/;s/{{http_server_port}}/"'${FluentBitHttpPort}'"/;s/{{read_from_head}}/"'${FluentBitReadFromHead}'"/;s/{{read_from_tail}}/"'${FluentBitReadFromTail}'"/' | kubectl delete -f -

Cloudwatch Container Insight을 설치하고 CW에서 로그와 매트릭 수집이 되고 있는지 확인 가능하다. 로그 그룹에 들어가보면 위에서 설정한 내용들의 로그그룹이 생성 된 것을 확인할 수 있다. 마찬가지로 Container Insight에 들어가면 메트릭도 확인할 수 있다.

로그가 제대로 나오는지 보기위해 부하를 발생시키고 로그에서 확인을 해본다.

# 부하 발생
curl -s https://nginx.$MyDomain
yum install -y httpd
ab -c 500 -n 30000 https://nginx.$MyDomain/

# 파드 직접 로그 모니터링
kubectl logs deploy/nginx -f

부하 발생을 시키고 동시에 직접 로그 모니터링을 실행한다. 로그 모니터링을 같이 하는 이유는 이후 로그 그룹에서 확인할 때 동일하게 내용이 나오는지 보기 위함이다.
다시 콘솔로 돌아가서 로그 그룹의 내용과 매트릭도 확인해보았다.매트릭은 CPU 사용률이나 기타 다양한 내용들을 확인할 수 있으니 Pod을 실행하고 부하 발생 테스트 등을 할 때 같이 모니터링을 하면 좋을 것 같다.

4. Metrics-server & kwatch & botkube

Metrics-Server : kubelet으로부터 수집한 리소스 메트릭을 수집 및 집계하는 클러스터 애드온 구성 요소로 cAdvisor(kubelet에 포함 된 커네티어 메트릭을 수집, 집계 및 노출하는 데몬)을 통해 데이터를 가져온다.

metrics-server을 배포하고 설치 내용을 확인해본다.
메트릭 값의 경우 15초 간격으로 cAdvisor을 통해 가져오게 되어있다. 그러니 최초 배포 후 일정 시간 후에 확인해보도록 하자.

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

# 메트릭 서버 확인 : 메트릭은 15초 간격으로 cAdvisor를 통하여 가져옴
kubectl get pod -n kube-system -l k8s-app=metrics-server
NAME                              READY   STATUS    RESTARTS   AGE
metrics-server-6bf466fbf5-zxjb2   1/1     Running   0          46s
kubectl api-resources | grep metrics
nodes                                          metrics.k8s.io/v1beta1                 false        NodeMetrics
pods                                           metrics.k8s.io/v1beta1                 true         PodMetrics
kubectl get apiservices |egrep '(AVAILABLE|metrics)'
NAME                                   SERVICE                      AVAILABLE   AGE
v1beta1.metrics.k8s.io                 kube-system/metrics-server   True        85s

# 노드 메트릭 확인
kubectl top node

# 파드 메트릭 확인
kubectl top pod -A
NAMESPACE           NAME                                            CPU(cores)   MEMORY(bytes)
amazon-cloudwatch   cloudwatch-agent-4k4mm                          7m           33Mi
amazon-cloudwatch   cloudwatch-agent-krmd4                          7m           34Mi
amazon-cloudwatch   cloudwatch-agent-x6bz4                          6m           30Mi
...
kubectl top pod -n kube-system --sort-by='cpu'
NAME                                            CPU(cores)   MEMORY(bytes)
kube-ops-view-558d87b798-6z5bt                  12m          38Mi
metrics-server-6bf466fbf5-zxjb2                 5m           19Mi
ebs-csi-controller-67658f895c-8zqph             5m           60Mi
...
kubectl top pod -n kube-system --sort-by='memory'
NAME                                            CPU(cores)   MEMORY(bytes)
ebs-csi-controller-67658f895c-8zqph             6m           60Mi
ebs-csi-controller-67658f895c-lm6wq             2m           52Mi
aws-node-n962c                                  5m           47Mi
...

kwatch : Kubernetes(K8s) 클러스터의 모든 변경 사항을 모니터링하고, 실행 중인 앱의 충돌을 실시간으로 감지하고, 채널(Slack, Discord 등)에 즉시 알림을 게시하도록 도와주는 서비스이다. 지난 kOps 스터디 때도 다뤘던 서비스이다.
나는 스터디에서 제공한 Slack 채널이 아닌 별도 관리하는 채널에 테스트를 진행할 예정이다.

일전에 만들어둔 WebHookURL을 입력해서 Configmap을 생성하고 kwatch 배포를 진행했다. 배포가 되면 내 슬랙 채널에 메시지가 뜨는 것을 확인할 수 있다.

# configmap 생성
cat <<EOT > ~/kwatch-config.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kwatch
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kwatch
  namespace: kwatch
data:
  config.yaml: |
    alert:
      slack:
        webhook: 'MyWebhook URL'
        title: $NICK-EKS
        #text:
    pvcMonitor:
      enabled: true
      interval: 5
      threshold: 70
EOT
kubectl apply -f kwatch-config.yaml

# 배포
kubectl apply -f https://raw.githubusercontent.com/abahmed/kwatch/v0.8.3/deploy/deploy.yaml

일부러 잘못 된 이미지의 파드를 배포해서 오류 메시지를 수신하는지 확인해볼 예정이다.
이미지 버전 정보가 잘못 된 이미지를 배포하면 바로 오류가 발생하고 슬랙에도 메시지가 오는 것을 볼 수 있다.

# 터미널1
watch kubectl get pod

# 잘못된 이미지 정보의 파드 배포
kubectl apply -f https://raw.githubusercontent.com/junghoon2/kube-books/main/ch05/nginx-error-pod.yml
kubectl get events -w

위 내용을 수정해보도록 한다.
kubectl set image pod nginx-19 nginx-pod=nginx:1.19
위 명령어로 잘못된 이미지 태그 정보를 수정했더니 바로 pod가 Running 상태로 변경된 것을 확인할 수 있다.

botkube : k8s 클러스터에서 알림 및 이벤트 관리를 위한 오픈소스 도구로 클러스터의 상태 변화, 오류 및 경고 등을 적시에 감지하고 조치를 취할 수 있는 서비스. 커스터마이즈가 가능하여 각각의 알림 룰을 정의하고 원하는 이벤트 유형에 대한 알림을 선택적으로 받을 수 있다. 이를 통해 클러스터의 상태 및 이벤트를 효과적으로 관리하고 모니터링할 수 있게 된다.

kwatch 때처럼 별도의 슬랙 채널에서 테스트할 예정이라 Slack App을 생성해야 한다. App 생성 방법은 공식 사이트를 참고하였다. https://docs.botkube.io/installation/slack/

# Slack API Token 설정
export SLACK_API_BOT_TOKEN='MySlackBotToken'
export SLACK_API_APP_TOKEN='MySlackAppToken'

# repo 추가
helm repo add botkube https://charts.botkube.io
helm repo update

# 변수 지정
export ALLOW_KUBECTL=true
export ALLOW_HELM=true
export SLACK_CHANNEL_NAME=study

#
cat <<EOT > botkube-values.yaml
actions:
  'describe-created-resource': # kubectl describe
    enabled: true
  'show-logs-on-error': # kubectl logs
    enabled: true

executors:
  k8s-default-tools:
    botkube/helm:
      enabled: true
    botkube/kubectl:
      enabled: true
EOT

# 설치
helm install --version v1.0.0 botkube --namespace botkube --create-namespace \
--set communications.default-group.socketSlack.enabled=true \
--set communications.default-group.socketSlack.channels.default.name=${SLACK_CHANNEL_NAME} \
--set communications.default-group.socketSlack.appToken=${SLACK_API_APP_TOKEN} \
--set communications.default-group.socketSlack.botToken=${SLACK_API_BOT_TOKEN} \
--set settings.clusterName=${CLUSTER_NAME} \
--set 'executors.k8s-default-tools.botkube/kubectl.enabled'=${ALLOW_KUBECTL} \
--set 'executors.k8s-default-tools.botkube/helm.enabled'=${ALLOW_HELM} \
-f botkube-values.yaml botkube/botkube

# 참고 : 삭제 시
helm uninstall botkube --namespace botkube

설치가 잘 됐다면 앱 표시에 Botkube가 생성된 것을 확인할 수 있고 채널에 들어가서 초대를 하면 (이상한 게 디폴트 채널을 설정해놔도 바로 입장하지 않았다.) 안내메시지를 호출해준다.

botkube가 제대로 설치되었으니 이제 테스트를 진행해보도록 한다.
슬랙 채널에서 아래 메시지들을 입력하면 botkube가 응답해주는 것을 볼 수 있다. 직접 터미널을 통해 접속하기 어려운 환경일 때 간단한 명령어 등으로 현황을 확인할 수 있는 좋은 방법이라고 생각이 든다. 물론 보안적인 문제는 있을 수 있으니 비공개채널에 만들어서 사용하는 게 좋을 것 같다.

# 연결 상태, notifications 상태 확인
@Botkube ping
@Botkube status notifications

# 파드 정보 조회
@Botkube k get pod
@Botkube kc get pod --namespace kube-system
@Botkube kubectl get pod --namespace kube-system -o wide

# Actionable notifications
@Botkube kubectl

kwatch에서의 테스트처럼 일부러 잘못 된 이미지를 배포해서 알림을 받아보도록 한다.
배포를 진행하면 바로 알림이 오는 것을 알 수 있다. 알림의 속도는 kwatch보다 빠른 것 같다.

5. Prometheus-stack

Prometheus : Soundcloud에서 제작한 오픈소스 시스템 Monitoring, Alert 툴킷이다. 

특징으로는 간단한 구조를 갖추고 있어 수월한 운영을 할 수 있고 강력한 쿼리 기능을 통해 다양한 결과값을 도출할 수 있다. 뒤에 실습할 Grafana와의 조합을 통해 시각화를 할 수 있고 ELK와 같은 로깅 방식이 아니라 시스템으로부터 모니터링 지표를 수집하여 저장하는 시스템이다. 자세한 구조는 아래 그림과 같다.

Prometheus-stack : kube-prometheus-stack은 Kubernetes 매니페스트, Grafana 대시보드, 문서 및 스크립트와 결합된 Prometheus 규칙을 수집하여 Prometheus Operator를 사용하여 Prometheus에서 엔드 투 엔드 Kubernetes 클러스터 모니터링을 쉽게 운영할 수 있다.
Prometheus-stack을 설치해보도록 한다.

# 모니터링 : 터미널 새로 띄워서 확인
kubectl create ns monitoring
watch kubectl get pod,pvc,svc,ingress -n monitoring

# 사용 리전의 인증서 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"

  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: myapssword

  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

# alertmanager:
#   ingress:
#     enabled: true
#     ingressClassName: alb
#     hosts: 
#       - alertmanager.$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'
EOT
cat monitor-values.yaml | yh

# 배포
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

# 확인
## alertmanager-0 : 사전에 정의한 정책 기반(예: 노드 다운, 파드 Pending 등)으로 시스템 경고 메시지를 생성 후 경보 채널(슬랙 등)로 전송
## grafana : 프로메테우스는 메트릭 정보를 저장하는 용도로 사용하며, 그라파나로 시각화 처리
## prometheus-0 : 모니터링 대상이 되는 파드는 ‘exporter’라는 별도의 사이드카 형식의 파드에서 모니터링 메트릭을 노출, pull 방식으로 가져와 내부의 시계열 데이터베이스에 저장
## node-exporter : 노드익스포터는 물리 노드에 대한 자원 사용량(네트워크, 스토리지 등 전체) 정보를 메트릭 형태로 변경하여 노출
## operator : 시스템 경고 메시지 정책(prometheus rule), 애플리케이션 모니터링 대상 추가 등의 작업을 편리하게 할수 있게 CRD 지원
## kube-state-metrics : 쿠버네티스의 클러스터의 상태(kube-state)를 메트릭으로 변환하는 파드
helm list -n monitoring
NAME                 	NAMESPACE 	REVISION	UPDATED                                	STATUS  	CHART                        	APP VERSION
kube-prometheus-stack	monitoring	1       	2023-05-20 14:40:34.552674361 +0900 KST	deployed	kube-prometheus-stack-45.27.2	v0.65.1

kubectl get pod,svc,ingress -n monitoring
NAME                                                            READY   STATUS    RESTARTS   AGE
pod/kube-prometheus-stack-grafana-665bb8df8f-547f2              3/3     Running   0          51s
pod/kube-prometheus-stack-kube-state-metrics-5d6578867c-p9g6b   1/1     Running   0          51s
pod/kube-prometheus-stack-operator-74d474b47b-br78c             1/1     Running   0          51s
...

kubectl get-all -n monitoring
NAME                                                                                 NAMESPACE   AGE
configmap/kube-prometheus-stack-alertmanager-overview                                monitoring  68s
configmap/kube-prometheus-stack-apiserver                                            monitoring  68s
configmap/kube-prometheus-stack-cluster-total                                        monitoring  68s
configmap/kube-prometheus-stack-grafana                                              monitoring  68s
...

kubectl get prometheus,servicemonitors -n monitoring
NAME                                                                VERSION   DESIRED   READY   RECONCILED   AVAILABLE   AGE
prometheus.monitoring.coreos.com/kube-prometheus-stack-prometheus   v2.42.0   1         1       True         True        80s

NAME                                                                                  AGE
servicemonitor.monitoring.coreos.com/kube-prometheus-stack-apiserver                  80s
servicemonitor.monitoring.coreos.com/kube-prometheus-stack-coredns                    80s
servicemonitor.monitoring.coreos.com/kube-prometheus-stack-grafana                    80s
...

kubectl get crd | grep monitoring
(k8sadmin@myeks:default) [root@myeks-bastion-EC2 ~]# kubectl get crd | grep monitoring
alertmanagerconfigs.monitoring.coreos.com    2023-05-20T05:40:32Z
alertmanagers.monitoring.coreos.com          2023-05-20T05:40:32Z
podmonitors.monitoring.coreos.com            2023-05-20T05:40:32Z
probes.monitoring.coreos.com                 2023-05-20T05:40:32Z
prometheuses.monitoring.coreos.com           2023-05-20T05:40:33Z
prometheusrules.monitoring.coreos.com        2023-05-20T05:40:33Z
servicemonitors.monitoring.coreos.com        2023-05-20T05:40:33Z
thanosrulers.monitoring.coreos.com           2023-05-20T05:40:33Z

설치하고 ALB을 확인하면 하나의 리스너(443)에 다수의 규칙이 생성 된 것을 확인할 수 있다. 이게 가능한 이유는 배포 yaml 파일의 anontation 부부분에 아래와 같이 group.name을 study로 다 통일시켰기 때문이다.

  alb.ingress.kubernetes.io/group.name: study

이렇게 되면 ALB 자원은 하나만 생성 및 관리해도 되기 때문에 편리하다. 동일한 목적과 수명주기를 갖고 관리하는 서비스들은 위와 같은 방법으로 생성하는 것이 좋을 것ㄱ ㅏㅌ다.

프로메테우스의 모니터링 대상이 되는 서비스는 일반적으로 자체 웹 서버의 /metrics 경로에 다양한 메트릭 정보를 노출한다. 이후 프로메테우스는 해당 경로에 HTTP GET 호출을 하게 되고 TSDB 형식으로 데이터를 저장한다.

ALB 리스너에서 도메인 주소도 확인했다면 프로메테우스에 접속해서 간단하게 확인해보도록 한다. 아래와 같이 데이터가 잘 보인다면 수집 및 출력이 잘 되는 것이다.

6. Grafana

그라파나는 위에서 설명한 프로메테우스가 수집하고 TSDB로 저장한 데이터를 시각화하는 서비스이다. 시각화 솔루션이기 때문에 자체적으로 데이터를 저장하지는 않고 Datasource라고 불리는 수집 데이터를 연결하여 시각화해주는 기능을 제공한다.

프로메테우스 스택으로 설치했기 때문에 그라파나에는 기본적으로 프로메테우스가 Datasource로 추가되어 있다. 별도로 추가하지 않아도 그라파나에서는 위에서 설치한 프로메테우스의 저장 데이터를 불러올 수 있다는 뜻이다.


그라파나에서 확인한 프로메테우스 서비스 주소를 통해 방금 생성한 Pod의 수집을 확인할 수 있다.

# 테스트용 파드 배포
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: netshoot-pod
spec:
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF
kubectl get pod netshoot-pod

# 접속 확인
kubectl exec -it netshoot-pod -- nslookup kube-prometheus-stack-prometheus.monitoring
Server:		10.100.0.10
Address:	10.100.0.10#53
Name:	kube-prometheus-stack-prometheus.monitoring.svc.cluster.local
Address: 10.100.32.51

kubectl exec -it netshoot-pod -- curl -s kube-prometheus-stack-prometheus.monitoring:9090/graph -v ; echo
*   Trying 10.100.32.51:9090...
* Connected to kube-prometheus-stack-prometheus.monitoring (10.100.32.51) port 9090 (#0)
> GET /graph HTTP/1.1
> Host: kube-prometheus-stack-prometheus.monitoring:9090
> User-Agent: curl/8.0.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2023 06:01:06 GMT
< Content-Length: 734
< Content-Type: text/html; charset=utf-8
<
* Connection #0 to host kube-prometheus-stack-prometheus.monitoring left intact
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><script>const GLOBAL_CONSOLES_LINK="",GLOBAL_AGENT_MODE="false",GLOBAL_READY="true"</script><link rel="manifest" href="./manifest.json" crossorigin="use-credentials"/><title>Prometheus Time Series Collection and Processing Server</title><script defer="defer" src="./static/js/main.c1286cb7.js"></script><link href="./static/css/main.cb2558a0.css" rel="stylesheet"></head><body class="bootstrap"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

# 삭제
kubectl delete pod netshoot-pod

우선 Dashobard을 생성한다. 추천 받은 Dashobard 목록 중 한국어 버전으로 나온 걸 추가해볼 생각이다.

Dashboard->Import을 선택한다.

Import via Grafana.com에 추천 받은 Dashboard의 번호를 입력한다. 여기서는 13770을 입력하고 Load을 클릭한다.

Name과 Data Source을 확인하고 Import을 클릭한다.

몇 번의 클릭만으로 훌륭한 대쉬보드를 가져올 수 있게 됐다. 여기서 불필요한 건 제거하고 필요하다고 생각되는 걸 추가하는 식으로 나만의 Dashboard을 꾸밀 수 있다.

Application 대쉬보드를 테스트해보기 위해 nginx을 배포해서 테스트해볼 예정이다.

# 모니터링
watch -d kubectl get pod

# 파라미터 파일 생성 : 서비스 모니터 방식으로 nginx 모니터링 대상을 등록하고, export 는 9113 포트 사용, nginx 웹서버 노출은 AWS CLB 기본 사용
cat <<EOT > ~/nginx_metric-values.yaml
metrics:
  enabled: true

  service:
    port: 9113

  serviceMonitor:
    enabled: true
    namespace: monitoring
    interval: 10s
EOT

# 배포
helm upgrade nginx bitnami/nginx --reuse-values -f nginx_metric-values.yaml

# 확인
kubectl get pod,svc,ep
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-85fc957979-lp8tk   2/2     Running   0          29s
NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                       AGE
service/kubernetes   ClusterIP   10.100.0.1       <none>        443/TCP                       5d4h
service/nginx        NodePort    10.100.212.109   <none>        80:31864/TCP,9113:31740/TCP   134m
NAME                   ENDPOINTS                               AGE
endpoints/kubernetes   192.168.1.42:443,192.168.2.209:443      5d4h
endpoints/nginx        192.168.2.105:9113,192.168.2.105:8080   134m

kubectl get servicemonitor -n monitoring nginx
NAME    AGE
nginx   44s

kubectl get servicemonitor -n monitoring nginx -o json | jq
{
  "apiVersion": "monitoring.coreos.com/v1",
  "kind": "ServiceMonitor",
  "metadata": {
    "annotations": {
      "meta.helm.sh/release-name": "nginx",
      "meta.helm.sh/release-namespace": "default"
...

# 메트릭 확인 >> 프로메테우스에서 Target 확인
NGINXIP=$(kubectl get pod -l app.kubernetes.io/instance=nginx -o jsonpath={.items[0].status.podIP})
curl -s http://$NGINXIP:9113/metrics # nginx_connections_active Y 값 확인해보기
curl -s http://$NGINXIP:9113/metrics | grep ^nginx_connections_active

# nginx 파드내에 컨테이너 갯수 확인
kubectl get pod -l app.kubernetes.io/instance=nginx
NAME                     READY   STATUS    RESTARTS   AGE
nginx-85fc957979-lp8tk   2/2     Running   0          2m20s

kubectl describe pod -l app.kubernetes.io/instance=nginx
Name:             nginx-85fc957979-lp8tk
Namespace:        default
Priority:         0
Service Account:  default
Node:             ip-192-168-2-82.ap-northeast-2.compute.internal/192.168.2.82
Start Time:       Sat, 20 May 2023 15:08:18 +0900
...

# 접속 주소 확인 및 접속
echo -e "Nginx WebServer URL = https://nginx.$MyDomain"
curl -s https://nginx.$MyDomain
kubectl logs deploy/nginx -f

# 반복 접속
while true; do curl -s https://nginx.$MyDomain -I | head -n 1; date; sleep 1; done

프로메테우스에서도 nginx에 대한 메트릭 정보를 확인할 수 있고 해당 데이터를 그래프로 표현해서 볼 수도 있다.

메트릭 수집이 정상적으로 되는 것을 확인했으니 이를 Grafana에서 확인해보도록 하겠다.
위에서 Dashboard을 Import할 때와 같은 방식으로 Import ID는 12708을 사용해서 생성한 대쉬보드이다. 요청 수나 Active Connection 수 등을 확인할 수 있다. 앞서 Prometheus에서 확인한 Connections_active와 동일한 모양임을 알 수 있다.


Prometheus에서 수집되는 데이터를 바탕으로 Grafana에서 시각화할 수 있는 것을 확인할 수 있었다.

7. kubecost

kubecost : k8s 클러스터의 비용 분석 및 관리를 위한 오픈 소스 도구로 Kubecost를 사용하면 Kubernetes 클러스터의 리소스 사용량, 비용, 예상 비용 등을 실시간으로 모니터링하고 분석할 수 있다.

https://docs.kubecost.com/install-and-configure/install/provider-installations/aws-eks-cost-monitoring

kubecost을 설치한다. 다만, 설치를 하더라도 bastion 환경에서 바로 대쉬보드를 볼 수는 없기 때문에 이를 Loadbalancer에 연결하는 작업을 이후에 진행해주었다.

# kubecost 설치 진행 : 버전은 https://gallery.ecr.aws/kubecost/cost-analyzer 에서 최신 버전 확인
export VERSION="1.103.3"
helm upgrade -i kubecost \
oci://public.ecr.aws/kubecost/cost-analyzer --version="$VERSION" \
--namespace kubecost --create-namespace \
-f https://raw.githubusercontent.com/kubecost/cost-analyzer-helm-chart/develop/cost-analyzer/values-eks-cost-monitoring.yaml \
--set prometheus.configmapReload.prometheus.enabled="false"

# 설치 상태 확인
kubectl get pod -n kubecost
NAME                                          READY   STATUS    RESTARTS   AGE
kubecost-cost-analyzer-9f75ffc6b-9dww8        2/2     Running   0          80s
kubecost-kube-state-metrics-d6d9b7594-hwzgd   1/1     Running   0          80s
kubecost-prometheus-server-7755c9b669-l8hs5   1/1     Running   0          80s

# LoadBalancer에 연결하여 업데이트
cat <<'EOF' |
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kubecost-alb-ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: kubecost-cost-analyzer
              port:
                number: 9090
EOF
(export NAMESPACE=kubecost && kubectl apply -n $NAMESPACE -f -)

# DNS 연결
export ENDPOINT=$(kubectl get ingress kubecost-alb-ingress -n kubecost --output jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo "Kubecost UI DNS name: ${ENDPOINT}"
Kubecost UI DNS name: k8s-kubecost-kubecost-7931dc7581-278425107.ap-northeast-2.elb.amazonaws.com

kubectl annotate ingress kubecost-alb-ingress -n kubecost "external-dns.alpha.kubernetes.io/hostname=kubecost.$MyDomain"
ingress.networking.k8s.io/kubecost-alb-ingress annotated
echo -e "kubecost URL = http://kubecost.$MyDomain"
kubecost URL = http://kubecost.bs-yang.com

kubecost 설치와 Loadbalancer 연결이 제대로 됐다면 아래와 같이 대쉬보드가 호출되는 것을 알 수 있다.

클러스터 배포와 kubecost 수집 자체가 얼마 되지 않아서 자세한 데이터들을 볼 수는 없지만 화면을 여기저기 클릭하면서 데이터를 확인할 수 있었다.

8. 정리

kOps에서 다뤘던 내용들이 있어서 이번에는 많이 어렵지 않게 실습을 진행할 수 있었다. 그라파나에서 이미지를 포함한 알림을 보내고 싶었지만 사용하고 있는 계정이 회사 계정이라 정책상 S3 Public Access을 허용할 수 없어서 테스트하지 못해 아쉽다. 추후 개인계정에서 테스트해볼 수 있으면 좋을 것 같다.

PKOS Study #4 – Monitoring

PKOS Study #4은 Prometheus, Grafana을 사용하여 k8s 환경 Monitoring, Alert을 진행해보려 한다.

0. 환경 구성

이번 실습은 지난 Study #3의 환경에 그대로 병행해서 진행할 예정이다. 리소스를 지우고 다시 만드는데 시간이 소요되고 kOps설치 시 레이턴시 이슈인지 한 번에 되지 않을 때가 있어 이번엔 이전 환경을 삭제하지 않고 그대로 진행한다.

1. Metric-server 확인

Metric-server : kubelet으로부터 리소스 매트릭을 수집 및 집계하는 클러스터 애드온 구성 요소이다. 실습에 사용 된 yaml에는 Metric-server가 올라오는 낸용이 포함되어 있기 때문에 실습 내용으로 진행하게 되면 Metric-server을 바로 확인 할 수 있다.

node(cp, worker)들의 CPU, Memory 사용량을 확인할 수 있고 각 pod들의 CPU, Memory 점유량도 확인할 수 있다. 나는 지난 실습 환경을 그대로 유지하고 있기 때문에 gitlab, argocd pod들의 내용도 확인할 수 있다.

2. Prometheus를 통한 Metric 수집

Prometheus : Soundcloud에서 제작한 오픈소스 시스템 Monitoring, Alert 툴킷이다. 

특징으로는 간단한 구조를 갖추고 있어 수월한 운영을 할 수 있고 강력한 쿼리 기능을 통해 다양한 결과값을 도출할 수 있다. 뒤에 실습할 Grafana와의 조합을 통해 시각화를 할 수 있고 ELK와 같은 로깅 방식이 아니라 시스템으로부터 모니터링 지표를 수집하여 저장하는 시스템이다. 자세한 구조는 아래 그림과 같다.

설치는 아래와 같이 진행한다. 이번엔 실습 때 제공되는 코드에서 인증서 ARN과 Cluster 주소를 한 번에 넣어주는 방식을 사용한다.

예제로 제공 받은 코드는 Grafana도 같이 helm Install을 사용하여 설치를 진행하게 된다. 아래 코드로 설치를 진행하면 Prometheus와 Grafana가 같이 설치되는 점 참고하여 진행한다.

<em># 모니터링</em>
kubectl create ns monitoring
watch kubectl get pod,pvc,svc,ingress -n monitoring

<em># 사용 리전의 인증서 ARN 확인</em>
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo "alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN"

<em># 설치</em>
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

<em># 파라미터 파일 생성</em>
cat <<EOT > ~/monitor-values.yaml
alertmanager:
  ingress:
    enabled: true
    ingressClassName: alb

    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/group.name: "monitoring"

    hosts:
      - alertmanager.$KOPS_CLUSTER_NAME

    paths:
      - /*

grafana:
  defaultDashboardsTimezone: Asia/Seoul
  adminPassword: prom-operator

  ingress:
    enabled: true
    ingressClassName: alb

    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/group.name: "monitoring"

    hosts:
      - grafana.$KOPS_CLUSTER_NAME

    paths:
      - /*

prometheus:
  ingress:
    enabled: true
    ingressClassName: alb

    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/group.name: "monitoring"

    hosts:
      - prometheus.$KOPS_CLUSTER_NAME

    paths:
      - /*

  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"
EOT

<em># 배포</em>
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 45.7.1 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring

위와 같이 정상적으로 배포 된 것을 확인할 수 있다.

프로메테우스가 각 서비스의 metric 경로에 HTTP GET 방식으로 메트릭 정보를 가져와 TSDB 형식으로 저장을 하게 된다.

<em># 아래 처럼 프로메테우스가 각 서비스의 9100 접속하여 메트릭 정보를 수집</em>
kubectl get node -owide
kubectl get svc,ep -n monitoring kube-prometheus-stack-prometheus-node-exporter

<em># 마스터노드에 lynx 설치</em>
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME hostname
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME sudo apt install lynx -y

<em># 노드의 9100번의 /metrics 접속 시 다양한 메트릭 정보를 확인할수 있음 : 마스터 이외에 워커노드도 확인 가능</em>
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME lynx -dump localhost:9100/metrics

Prometheus 주소(Ingress와 연결 된 Domain 주소)에 접속하면 아래와 같이 접속 화면을 확인할 수 있다.

2. Grafana을 통한 시각화

Grafana는 Grafana Labs에서 개발한 Metric/Log 시각화 Dashboard이다. 다양한 Datasource을 연결해서 시각화하여 제공할 수 있다.

실습 예제로 제공 된 코드에는 Grafana도 Prometheus와 같이 Helm install를 통해 설치하기 때문에 별도 설치하는 내용은 건너뛰고 Grafana에 접근하여 진행하도록 한다. 초기 제공 되는 계정을 사용하여 로그인하고 암호를 변경한다.

암호 변경하고 Data Source을 확인해보면, http://kube-prometheus-stack-prometheus.monitoring:9090/ 가 입력되어 있는 것을 알 수 있다.

kube-system 이외에 상태를 체크하기 위해 테스트용 pod을 배포한 다음 대쉬보드를 통해 확인해 볼 예정이다.

우선 Dashobard을 생성한다. 추천 받은 Dashobard 목록 중 한국어 버전으로 나온 걸 추가해볼 생각이다.

Dashboard->Import을 선택한다.

Import via Grafana.com에 추천 받은 Dashboard의 번호를 입력한다. 여기서는 13770을 입력하고 Load을 클릭한다.

Name과 Data Source을 확인하고 Import을 클릭한다.

몇 번의 클릭만으로 훌륭한 대쉬보드를 가져올 수 있게 됐다. 여기서 불필요한 건 제거하고 필요하다고 생각되는 걸 추가하는 식으로 나만의 Dashboard을 꾸밀 수 있다.

테스트 목적의 Application은 nginx 웹서버를 배포할 예정이다. 배포할 때 참고하는 yaml에는 monitoring 옵션을 활성화해서 바로 Prometheus에서 모니터링할 수 있도록 한다.

위 사진 처럼 Prometheus에 바로 해당 내용을 확인할 수 있다. 예제로 Connections_active을 Graph로 확인해봤다. 메트릭 수집이 정상적으로 되는 것을 확인했으니 이를 Grafana에서 확인해보도록 하겠다.

위에서 Dashboard을 Import할 때와 같은 방식으로 Import ID는 12708을 사용해서 생성한 대쉬보드이다. 요청 수나 Active Connection 수 등을 확인할 수 있다. 앞서 Prometheus에서 확인한 Connections_active와 동일한 모양임을 알 수 있다.

이와 같이 Prometheus에서 수집되는 데이터를 바탕으로 Grafana에서 시각화할 수 있는 것을 확인해보았다.

3. kwatch을 통한 Application 문제  전송

Grafana로 시각화하는 것까지 확인했으니 이제 kwatch을 사용해 Application 충돌 등 이슈를 확인해 Webhook 전송하는 것을 진행하려 한다.

kwatch : Kubernetes(K8s) 클러스터의 모든 변경 사항을 모니터링하고, 실행 중인 앱의 충돌을 실시간으로 감지하고, 채널(Slack, Discord 등)에 즉시 알림을 게시하도록 도와주는 서비스이다. 나는 Study에서 제공 된 webhook이 아닌 개인 스터디 목적으로 생성한 Workspace에 별도 webhook을 생성해서 진행할 예정이다.

우선 간단하게 웹훅을 생성하는 방법을 소개한다.

내가 속해있는 Workspace에서 “+ 앱 추가”을 선택한다.

검색창에 webhook을 검색하고 나온 결과 중 incoming-webhooks에 추가 버튼을 클릭한다.

브라우저로 URL이 연결되면서 위와 같은 페이지를 볼 수 있다. 여기서 Slack에 추가를 선택한다.

Webhook을 초대할 채널명을 선택한다. 여기서는 #스터디 를 선택하였다. 이후 “수신 웹후크 통합 앱 추가”를 선택한다.

Webhook 이름과 아이콘(선택사항)을 변경하고 웹후크URL을 따로 복사해놓고 설정 저장을 클릭한다. 복사한 웹후크 URL은 아래 kwatch 배포 코드에서 webhook 주소에 해당 URL을 입력해주면 된다.

Slack-bot이 정상적으로 추가 된 것을 확인할 수 있다. (위 추가 된 건 테스트 용도로 생성한 거니 무시해도 된다.)

이제 Webhook을 생성했으니 kwatch 배포를 진행한다.

<em># configmap 생성</em>
cat <<EOT > ~/kwatch-config.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kwatch
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kwatch
  namespace: kwatch
data:
  config.yaml: |
    alert:
      slack:
        webhook: 'https://mywebhookurl'
        <em>#title:</em>
        <em>#text:</em>
    pvcMonitor:
      enabled: true
      interval: 5
      threshold: 70
EOT
kubectl apply -f kwatch-config.yaml

<em># 배포</em>
kubectl apply -f https://raw.githubusercontent.com/abahmed/kwatch/v0.8.3/deploy/deploy.yaml

위 코드에서 webhook URL만 내 웹훅 URL로 변경하고 실행하면 된다.

배포를 하게 되면 내 채널에 kwatch started라는 메시지가 뜨게 된다.

이제 kwatch가 정상적으로 역할을 수행하는지 확인하기 위해서 잘못 된 이미지 정보를 갖고 있는 pod을 배포해보도록 한다.

<em># 터미널1</em>
watch kubectl get pod

<em># 잘못된 이미지 정보의 파드 배포</em>
kubectl apply -f https://raw.githubusercontent.com/junghoon2/kube-books/main/ch05/nginx-error-pod.yml
kubectl get events -w

Error가 나오면서 배포가 완료되지 않는 것을 알 수 있다.

마찬가지로 Slack에서도 위와 같은 경고를 보내준다. Logs까지 상세하게 잡아줘서 대략적으로 어떤 문제인지 확인할 수 있다.

이제 문제가 발생했으니 해결해야 할 차례다. 위에서 사용한 yaml을 불러와 본다.

image: nginx:1.19.19 는 존재하지 않는 이미지이기 때문에 배포가 제대로 되지 않았음을 알 수 있다.

nginx-19 pod의 이미지 정보를 1.19.19에서 1.19로 변경하는 작업을 진행해줬다.

ImagePullBackOff 상태였던 nginx-19가 제대로 Running 상태로 변경된 것을 확인할 수 있었다.

4. Grafana을 통한 경고 전송

kwatch에서 Pod의 문제를 확인하여 트러블슈팅하는 과정을 진행해보았다면 이번엔 Grafana에서 시각화 된 메트릭 중 장애를 확인하여 경고 전송해주는 내용을 진행해볼 예정이다. 스터디에서는 Prometheus Alert Manager을 사용했지만 나는 이번 실습에서 Grafana을 통해 진행해보려고 한다.

경고 전송을 위해 우선 Contact points을 생성한다.

Alerting->Net Contact Point을 선택한다.

Name은 임의로 입력하고 Webhook URL에 kwatch 때 사용했던 Webhook URL을 입력한 다음 Test을 실행한다. 문제없이 Slack 채널에 테스트 메시지가 전송 될 경우 Save Contact Point을 클릭해 설정을 저장한다.

Alert 창구는 만들었으니 이제 경고를 만들어 테스트를 진행해보려 한다. 여기서는 Node가 다운되었을 때 경고를 받는 것을 테스트해볼 예정이다.

위와 같은 조건으로 설정을 진행해보았다.

kube_node_status_condition Metric에서 Condition=Ready, Status=Unknown이 0보다 클 경우 경고를 발생하는 조건이다.

Node 1개에서 kubectl service을 stop 했더니 아래와 같이 경고를 받을 수 있었다.

경고를 받았으니 조치를 취해야 한다. Stop했던 node의 서비스를 다시 시작시켜줬다.

위 메시지가 온 것으로 해결됐음을 알 수 있다.

5. 마무리

모니터링은 서비스 운영에 있어 중요한 지점이나 항상 쉽지 않은 것 같아. Metric-server에서 데이터를 수집하고 Prometheus와 Grafana을 이용해 시각화, 경고를 전송하는 것을 테스트해보았다.

Alert 자체가 수집할 수 있는 Metric의 조건을 거의 모두 활용할 수 있기 때문에 내가 원하는 방식의 경고를 생성할 수 있을 것 같다.

꼭 k8s가 아니더라도 인프라 운영에 있어 Prometheus, Grafana 조합을 활용해봐야겠다.