본문 바로가기

AWS EKS 실습/EKS Beginner

Spot Instance로 EKS 구성

이번장에서는 Spot Instance를 통해 비용과 Sacle을 최적화 하는 EKS 환경을 구성해 본다.

ㅁ Spot EC2 Worker Node 추가

 

기존 구성되어 있는 node를 OnDemand Node로 변경하여 신규 생성할 Spot Node와 구분할 수 있도록 함

$ kubectl label nodes --all 'lifecycle=OnDemand'
node/ip-192-168-21-20.ap-northeast-2.compute.internal labeled
node/ip-192-168-40-208.ap-northeast-2.compute.internal labeled
node/ip-192-168-71-49.ap-northeast-2.compute.internal labeled

새로운 Spot worker node 추가

 

cat << EoF > ~/environment/eks-workshop-ng-spot.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: eks-newelite-eksctl 
  region: ${AWS_REGION}
nodeGroups:
  - name: ng-spot
    labels:
      lifecycle: Ec2Spot
    taints:
      spotInstance: true:PreferNoSchedule
    minSize: 2
    maxSize: 5
    instancesDistribution: 
      instanceTypes:
        - m5.large
        - m4.large
        - m5d.large
        - m5a.large
        - m5ad.large
        - m5n.large
        - m5dn.large
      onDemandBaseCapacity: 0
      onDemandPercentageAboveBaseCapacity: 0 # all the instances will be Spot Instances
      spotAllocationStrategy: capacity-optimized # launch Spot Instances from the most availably Spot Instance pools
EoF

eksctl create nodegroup -f ~/environment/eks-workshop-ng-spot.yaml

 

Node Group을 생성하는 동안 kubernetes가 지금 프로비저닝한 Node 유형을 알 수 있도록 Node Label을 구성했다. Node의 lifecycle을 EC2Spot으로 설정한 후 Spot Instance에 스케줄되지 않은 Pod를 선호하는 PreferNoSchedule로 tainting 한다. 이것은 NoSchedule의 기본 설정 또는 soft 버전이다. 시스템은 노드에 taint를 허용하지 않는 Pod를 배치하지 않도록 시도하지만 필수는 아니다.

 

또한 클러스터에서 Spot 중단 수를 줄이기 위해 가장 많은 가용 용량으로 Spot Instance Pool에서 Instance를 시작하는 spotAllocationStrategy로 capacity-optimized를 지정한다.

 

생성 순으로 전체 node를 조회해보면 AGE에 4m5s가 된 node를 볼 수 있다.

$ kubectl get nodes --sort-by=.metadata.creationTimestamp
NAME                                                STATUS   ROLES    AGE    VERSION
ip-192-168-71-49.ap-northeast-2.compute.internal    Ready    <none>   10d    v1.17.12-eks-7684af
ip-192-168-21-20.ap-northeast-2.compute.internal    Ready    <none>   10d    v1.17.12-eks-7684af
ip-192-168-40-208.ap-northeast-2.compute.internal   Ready    <none>   10d    v1.17.12-eks-7684af
ip-192-168-29-95.ap-northeast-2.compute.internal    Ready    <none>   4m5s   v1.17.12-eks-7684af
ip-192-168-83-171.ap-northeast-2.compute.internal   Ready    <none>   4m5s   v1.17.12-eks-7684af

 

selector로 lifecycle=Ec2Spot을 조회해 보면 동일한 아래 2개의 Node 확인할 수 있다. 

$ kubectl get nodes --label-columns=lifecycle --selector=lifecycle=Ec2Spot
NAME                                                STATUS   ROLES    AGE     VERSION               LIFECYCLE
ip-192-168-29-95.ap-northeast-2.compute.internal    Ready    <none>   5m33s   v1.17.12-eks-7684af   Ec2Spot
ip-192-168-83-171.ap-northeast-2.compute.internal   Ready    <none>   5m33s   v1.17.12-eks-7684af   Ec2Spot

 

selector를 lifecycle=OnDemand로 조회하면 처음에 설정한 전체 노드를 볼 수 있다.

$ kubectl get nodes --label-columns=lifecycle --selector=lifecycle=OnDemand
NAME                                                STATUS   ROLES    AGE   VERSION               LIFECYCLE
ip-192-168-21-20.ap-northeast-2.compute.internal    Ready    <none>   10d   v1.17.12-eks-7684af   OnDemand
ip-192-168-40-208.ap-northeast-2.compute.internal   Ready    <none>   10d   v1.17.12-eks-7684af   OnDemand
ip-192-168-71-49.ap-northeast-2.compute.internal    Ready    <none>   10d   v1.17.12-eks-7684af   OnDemand

 

kubectl describe nodes 를 해보면 taints에 두 node만 적용되어 있는 것을 볼 수 있다.

이제 Spot interruptions을 위한 클러스터를 구성한다.

 

Spot Instance에 대한 수요는 크게 다를 수 있으며, 그 결과 Spot Intance의 가용성은 사용 가능한 미사용 EC2 Intance의 수에 따라 달라진다. 항상 Spot Instance가 중단될 수 있으며 Spot Instance는 일을 정상적으로 마무리 하기 위해 2분전에 중단 알림을 받는다. 각 Spot Instance에 Pod를 배포하여 클러스터의 다른 곳에서 애플리케이션을 감지하고 재 배포 한다.

 

가장 먼저해야 할 일은 AWS Node Termination Handler를 각 Spot Instance에 배포해야 한다. 이것은 Interruption 알림을 위해 인스턴스의 EC2 메타데이터 서비스를 모니터링한다.  이 termination handler는 ServiceAccount, ClusterRole, ClusterRoleBinding, 그리고 DaemonSet으로 구성되어 있으며 워크플로우는 다음과 같이 요약할 수 있다.

- Spot Instance가 회수되고 있는지 확인

- 2분 알림 창을 사용하여 종료할 노드를 정상적으로 준비

- Taint node에 새 Pod가 배치되지 않도록 차단한다.

- 실행중인 Pods의 연결을 Drain(배수) 한다.

- 원하는 용량을 유지하면서 나머지 노드에 Pod를 교체한다.

 

기본적으로 aws-node-termination-handler는 모든 노드(On-Demand 및 Spot)에서 실행된다. Spot Instance에 Label이 지정된 경우 aws-node-termination-handler Label이 지정된 Spot Node에서만 실행되도록 구성할 수 있다. Tag lifecycle=Ec2Spot이 사용 된 경우 다음을 실행하여 spot-node-selector overlay를 적용할 수 있다.

 

helm repo add eks https://aws.github.io/eks-charts

helm upgrade --install aws-node-termination-handler \
             --namespace kube-system \
             --set nodeSelector.lifecycle=Ec2Spot \
              eks/aws-node-termination-handler

Pod가  lifecycle=Ec2Spot로 label이 되어 있는 node에서만 작동하는지 확인한다.

kubectl --namespace=kube-system get daemonsets

수행해보면 아래와 같은 결과가 나온다. Node Selector 부분을 확인

$ kubectl --namespace=kube-system get daemonsets
NAME                           DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                              AGE
aws-node                       5         5         5       5            5           <none>                                     11d
aws-node-termination-handler   2         2         2       2            2           kubernetes.io/os=linux,lifecycle=Ec2Spot   108s
kube-proxy                     5         5         5       5            5           <none>                                     11d

 

ㅁ Configure Node Affinity and Tolerations(허용 오차)

 

Cloud9 편집기에서 Deployment manifest를 오픈하여 Spot Instance를 선택하도록 NodeAffinity를 구성하도록 spec을 편집한다. 이렇게 하면 Spot Instance를 사용할 수 없거나 올바르게 label이 지정되지 않으면 pods가 on-demand nodes에 스케줄 될 수 있다.

 

Node Affnity, 예) (kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity)

 

Assigning Pods to Nodes

You can constrain a Pod to only be able to run on particular Node(s), or to prefer to run on particular nodes. There are several ways to do this, and the recommended approaches all use label selectors to make the selection. Generally such constraints are u

kubernetes.io

Taints 및 Tolerations, 예)  (kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)

 

Taints and Tolerations

Node affinity, is a property of Pods that attracts them to a set of nodes (either as a preference or a hard requirement). Taints are the opposite -- they allow a node to repel a set of pods. Tolerations are applied to pods, and allow (but do not require) t

kubernetes.io

~/environment/ecsdemo-frontend/kubernetes/deployment.yaml을 열어 spec.template.spec에 아래 항목을 추가한다.

      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
              - key: lifecycle
                operator: In
                values:
                - Ec2Spot
      tolerations:
      - key: "spotInstance"
        operator: "Equal"
        value: "true"
        effect: "PreferNoSchedule"

 

최종 적용된 deployment.yaml 은 아래와 같다.

더보기
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ecsdemo-frontend
  labels:
    app: ecsdemo-frontend
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ecsdemo-frontend
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: ecsdemo-frontend
    spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
              - key: lifecycle
                operator: In
                values:
                - Ec2Spot
      tolerations:
      - key: "spotInstance"
        operator: "Equal"
        value: "true"
        effect: "PreferNoSchedule"
      containers:
      - image: brentley/ecsdemo-frontend:latest
        imagePullPolicy: Always
        name: ecsdemo-frontend
        ports:
        - containerPort: 3000
          protocol: TCP
        env:
        - name: CRYSTAL_URL
          value: "http://ecsdemo-crystal.default.svc.cluster.local/crystal"
        - name: NODEJS_URL
          value: "http://ecsdemo-nodejs.default.svc.cluster.local/"

 

 

Spot Instance로 Frontend 재배포

 

첫번째로 모든 Pod를 Spot Instance에 배포가 되어 있는지 확인한다.

 for n in $(kubectl get nodes -l lifecycle=Ec2Spot --no-headers | cut -d " " -f1); do echo "Pods on instance ${n}:";kubectl get pods --all-namespaces  --no-headers --field-selector spec.nodeName=${n} ; echo ; done

 

편집된 Frontend Manifest로 다시 배포한다.

cd ~/environment/ecsdemo-frontend
kubectl apply -f kubernetes/service.yaml
kubectl apply -f kubernetes/deployment.yaml

cd ~/environment/ecsdemo-crystal
kubectl apply -f kubernetes/service.yaml
kubectl apply -f kubernetes/deployment.yaml

cd ~/environment/ecsdemo-nodejs
kubectl apply -f kubernetes/service.yaml
kubectl apply -f kubernetes/deployment.yaml

 

다시 결과를 확인한다.

 for n in $(kubectl get nodes -l lifecycle=Ec2Spot --no-headers | cut -d " " -f1); do echo "Pods on instance ${n}:";kubectl get pods --all-namespaces  --no-headers --field-selector spec.nodeName=${n} ; echo ; done

 

ㅁ Cleanup Script

cd ~/environment/ecsdemo-frontend
kubectl delete -f kubernetes/service.yaml
kubectl delete -f kubernetes/deployment.yaml

cd ~/environment/ecsdemo-crystal
kubectl delete -f kubernetes/service.yaml
kubectl delete -f kubernetes/deployment.yaml

cd ~/environment/ecsdemo-nodejs
kubectl delete -f kubernetes/service.yaml
kubectl delete -f kubernetes/deployment.yaml
helm uninstall aws-node-termination-handler --namespace=kube-system
kubectl label nodes --all lifecycle-

eksctl delete nodegroup -f  ~/environment/eks-workshop-ng-spot.yaml --approve