이번장에서는 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)
Taints 및 Tolerations, 예) (kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
~/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
'AWS EKS 실습 > EKS Beginner' 카테고리의 다른 글
EKS Cluster에서 사용자 권한 등록 (0) | 2022.04.20 |
---|---|
Statefulsets을 사용하는 STATEFUL CONTAINER (w/ Persistent Volume) (0) | 2021.03.01 |
Assigning Pods to Nodes (0) | 2021.02.27 |
Exposing a Service (0) | 2021.02.26 |
EKS Network 정책 실습 (w/Calico) (0) | 2021.02.26 |