개발놀이터

AWS EKS HPA로 확장성 높이기 본문

배포/kubernetes

AWS EKS HPA로 확장성 높이기

마늘냄새폴폴 2024. 10. 8. 23:17

저번 포스팅에서 이어지는 내용입니다! 

 

https://coding-review.tistory.com/558

 

AWS EKS 스프링 + Nginx + SSL

저번 포스팅에서 이어지는 내용입니다!  https://coding-review.tistory.com/557 AWS EKS 스프링 프로젝트에 NGINX 붙이기이번 포스팅은 이전 포스팅과 어느정도 이어집니다. 이전에 스프링 프로젝트를 간단

coding-review.tistory.com

 

저번 포스팅에선 Nginx Ingress Controller 에 letsencrypt로 발급한 키로 secret을 만들어서 SSL을 연결했습니다. 이번 포스팅에선 HPA와 CA를 이용해서 파드에 무리가 가면 파드를 늘리고 파드를 늘리는 과정에서 노드의 자원이 부족하면 노드까지 늘리는 것을 실습해보고 정리해보겠습니다. 

 

저번 포스팅에서 추가되는 것은 HPA와 CA를 만드는 것 뿐입니다. 

 

HPA만들기

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spring-hpa
  namespace: ingress-nginx
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: ReplicaSet
    name: spring-rs
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

 

이렇게 HPA를 만들면 레플리카를 우리가 설정한 개수내에서 늘렸다 줄였다할 수 있습니다. 

 

HPA에서 주목해야 하는 점은 scaleTargetRef에서 name을 우리가 생성한 Deployment (혹은 ReplicaSet, StatefulSet) 의 이름과 연결해줘야한다는 것입니다. 

 

또한, metrics에서 어떤 자원을 기반으로 스케일링을 할 것인지 정해줄 수 있습니다. 쿠버네티스 v2 (1.18버전 이후) 에는 Scale Out, Scale In 을 어떤 전략을 가지고 할 것인지도 정해줄 수 있습니다. 

 

하지만 저는 그런 전략까지는 필요없어서 우선 간단하게 만들었습니다. 

 

만약 스케일 전략을 가지고 있다면 100퍼센트로 스케일한다고 가정했을 경우 레플리카가 2 -> 4 - >8 이렇게 늘어나고 줄어들 때도 8 -> 4 -> 2 이렇게 줄어듭니다. 스케일 전략의 퍼센트는 커스텀하게 설정할 수 있습니다. 

 

실전에서는 이렇게 스케일 전략을 가져가는 것이 더 나은 선택이 될 수 있을 것 같습니다. 

 

우선 저는 CPU의 평균을 50퍼센트로 유지하도록 설정했습니다. 

 

Metric Server 배포

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

 

이 명령어를 통해 메트릭 서버를 배포할 수 있고 배포된 메트릭 서버는 파드의 정보를 읽어와서 모니터링할 수 있게 해줍니다. 

 

메트릭 서버는 추후에 파드의 상태를 확인할 때 사용하도록 하겠습니다. 

 

CA 배포

CA란 Cluster Autoscaler의 약자로 HPA만 존재하는 경우 파드가 늘어날 뿐 노드의 자원이 부족하다면 Pending상태가 됩니다. 이 때 클라우드 서비스들은 Auto Scaling 서비스를 가지고 있기 때문에 이를 연결해서 자동으로 노드를 늘려주는 역할을 합니다. 

 

---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
  name: cluster-autoscaler
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
  - apiGroups: [""]
    resources: ["events", "endpoints"]
    verbs: ["create", "patch"]
  - apiGroups: [""]
    resources: ["pods/eviction"]
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["pods/status"]
    verbs: ["update"]
  - apiGroups: [""]
    resources: ["endpoints"]
    resourceNames: ["cluster-autoscaler"]
    verbs: ["get", "update"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["watch", "list", "get", "update"]
  - apiGroups: [""]
    resources:
      - "namespaces"
      - "pods"
      - "services"
      - "replicationcontrollers"
      - "persistentvolumeclaims"
      - "persistentvolumes"
    verbs: ["watch", "list", "get"]
  - apiGroups: ["extensions"]
    resources: ["replicasets", "daemonsets"]
    verbs: ["watch", "list", "get"]
  - apiGroups: ["policy"]
    resources: ["poddisruptionbudgets"]
    verbs: ["watch", "list"]
  - apiGroups: ["apps"]
    resources: ["statefulsets", "replicasets", "daemonsets"]
    verbs: ["watch", "list", "get"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses", "csinodes", "csidrivers", "csistoragecapacities"]
    verbs: ["watch", "list", "get"]
  - apiGroups: ["batch", "extensions"]
    resources: ["jobs"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: ["coordination.k8s.io"]
    resources: ["leases"]
    verbs: ["create"]
  - apiGroups: ["coordination.k8s.io"]
    resourceNames: ["cluster-autoscaler"]
    resources: ["leases"]
    verbs: ["get", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["create", "list", "watch"]
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"]
    verbs: ["delete", "get", "update", "watch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    app: cluster-autoscaler
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cluster-autoscaler
  template:
    metadata:
      labels:
        app: cluster-autoscaler
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '8085'
    spec:
      priorityClassName: system-cluster-critical
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      serviceAccountName: cluster-autoscaler
      containers:
        - image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.26.2
          name: cluster-autoscaler
          resources:
            limits:
              cpu: 100m
              memory: 600Mi
            requests:
              cpu: 100m
              memory: 600Mi
          command:
            - ./cluster-autoscaler
            - --v=4
            - --stderrthreshold=info
            - --cloud-provider=aws
            - --skip-nodes-with-local-storage=false
            - --expander=least-waste
            - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/{클러스터 이름}
          volumeMounts:
            - name: ssl-certs
              mountPath: /etc/ssl/certs/ca-certificates.crt # /etc/ssl/certs/ca-bundle.crt for Amazon Linux Worker Nodes
              readOnly: true
          imagePullPolicy: "Always"
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
            readOnlyRootFilesystem: true
      volumes:
        - name: ssl-certs
          hostPath:
            path: "/etc/ssl/certs/ca-bundle.crt"

 

이건 깃허브에 돌아다니는 CA 예시를 그대로 가져와서 사용한 것입니다. 맨 아래가 CA이고 자세히 보시면 클러스터 이름을 적어야 하는 곳이 있습니다. 이곳에 AWS EKS의 클러스터 이름을 적어주면 됩니다. 

 

저는 EKS의 노드그룹의 설정으로 최대 노드 수를 3으로 설정해 실습을 위한 최소 비용만을 사용하도록 했습니다. 

 

HPA, CA 실습

우선 파드 상태를 확인합니다. 

 

 

Nginx 파드 두개, 스프링 파드 두개, Nginx Ingress Controller 한개가 생성되어있습니다. 

 

이 상태에서 exec 명령어로 스프링 파드 내부로 들어갑니다. 

 

 

그리고 CPU에 부하를 줘보겠습니다. 위의 명령어를 입력하면 됩니다. 

 

while true; do :; done

 

 

저는 스프링 파드의 limit을 0.5 코어로 주었기 때문에 더이상 올라가지 않는 것을 확인할 수 있었습니다. 

 

 

CPU가 100퍼센트에 도달하자 파드를 두개 늘리는 모습입니다. 

 

 

노드도 하나 생성됐네요. AGE를 보면 지금 막 생성된 것을 볼 수 있습니다. 

 

EC2 인스턴스 상황을 확인해보겠습니다. 

 

 

이렇게 하나가 늘어난 것을 확인할 수 있습니다. 

 

이제 부하를 멈춰보겠습니다. 

 

 

부하가 줄어들었고 파드가 줄어드는지 확인해봐야합니다. 

 

 

파드가 네개였다가 두개로 줄어들었습니다. 

 

그리고 노드도 확인해보면... 줄어들겠지만 EKS의 CA를 사용하는 경우 AWS ASG가 노드를 늘리는건 금방 늘리는데 죽일때는 정말 느리게 죽입니다. 

 

상식적으로도 트래픽이 몰려 서버가 간당간당한 상태에선 빠르게 노드를 추가해서 무리가 되는 파드의 부담을 줄여야하지만 부하가 점점 빠지는 경우에는 언제 또 증가할지 모르는 부하를 대비해 정말 이 노드가 사용되지 않는구나 판단될 때까지 노드를 살려놓습니다. 

 

체감상 노드가 추가되는 것은 1~2분이면 추가가 되는데 사라지는건 10분 넘게 기다려도 사라지지 않는 경우가 있더군요. 

 

CA사용시 주의사항!

CA를 사용하는 경우, 특히 AWS와 같은 클라우드 서비스를 사용하는 경우 문제되는 것이 바로 PV와 PVC바운딩이 안돼서 파드가 Pending 상태에 무한히 빠지는 경우입니다. 

 

AWS를 예시로 들자면 EBS를 PV로 사용하는 경우 그에 맞는 PVC를 생성하고 volumeMount를 하는 경우 자동으로 PV를 선택해서 바운드 해준다는 특징이 있습니다. 

 

하지만 이런 경우 문제가 되는 것이 EBS는 하나의 리전에서만 동작하기 때문에 노드가 추가되면 PV가 연결되지 않아 CA에 의해 생성되는 노드가 PV를 찾을 수 없어 무한히 Pending 상태로 이어지고 파드가 생성되지 않습니다. 

 

즉, EBS를 PV로 사용한다면, 한국의 경우 ap-northeast-2a 부터 ap-northeast-2d까지 생성되는데 만약 EBS가 2a에 생성되고 CA에 의해 생성되는 노드가 2b라면 이 2b에 생성된 노드는 2a에 생긴 EBS와 연결될 수 없다는 이야기입니다. 

 

때문에, CA를 사용한다면 EBS를 PV로 사용하는 것 보다는 EFS를 PV로 사용하는 것이 바람직합니다. EFS는 EBS와는 다르게 모든 리전에서 사용가능하기 때문이죠. 

 

 

이렇게 Pending 상태에서 무한히 돌고있는 파드를 describe 명령어를 이용해서 확인해보면 volume node affinity conflict 라는 에러 메세지를 볼 수 있습니다. 

 

 

PVC와 PV에 대한 describe도 확인해보면 해당 PV는 ap-northeast-2c에 생성되어있고 새롭게 생성된 노드가 만약 2c가 아닌 다른 리전에 생성되었다면 해당 PV가 노드와 연결되지 않아 파드 자체가 생성되지 않는 것이죠. 

 

 

아무튼 결국 노드도 하나 줄어들어서 최소 노드 개수인 2개에 맞춰진 모습입니다. 

 

EC2 인스턴스도 확인해보면 두개로 줄어든 것을 확인할 수 있습니다. 

 

 

 

마치며

이렇게 HPA와 CA를 이용해서 파드를 부하에 따라 유동적으로 생성되도록 설계하였습니다. 이렇게 설계하게 되면 노드 그룹으로 설정한 최대 노드 개수에 도달하기 전까진 파드가 안정적으로 동작할 수 있습니다. 

 

이제 다음 포스팅에선 데이터베이스 서버를 쿠버네티스와 분리하는 작업을 진행할 예정입니다. 

 

제가 이전 포스팅에서도 종종 언급했지만 데이터베이스는 stateful 애플리케이션이고 이 정도가 매우 심해 쿠버네티스와 어울리지 않는 애플리케이션 중 하나입니다. 

 

때문에 쿠버네티스 파드로 데이터베이스를 생성하는 것은 안전한 방법이 아니라고 볼 수 있죠. 

 

쿠버네티스에선 이런 stateful 애플리케이션을 관리하기 위한 StatefulSet 이라는 오브젝트를 제공해주긴 하지만 stateful의 정도가 너무 심한 것들은 쿠버네티스에서 분리할 것은 권장하고 있죠. 

 

그럼 다음 포스팅에서 뵙도록 하겠습니다! 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!