5주차 - 오토스케일링
개요
스케줄링 알고리즘
어떤 설정들을 할 수 있는지.
https://medium.com/@simardeep.oberoi/kubernetes-dynamic-resource-allocation-a-leap-in-resource-management-c39fdca6b99e
이거 알아보자.
inplace 수평확장도
카펜터는 필요사양도 알면서 스케줄링 조건도 이해한다..?
빈패킹 - 쿠버쪽도 확인
스케일링 종류
파드, 노드
HPA
HPA는 결국 스케일링의 기본이 되는 방식이라 잘 알아두는 것이 좋다.
환경
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-apache
spec:
selector:
matchLabels:
run: php-apache
template:
metadata:
labels:
run: php-apache
spec:
containers:
- name: php-apache
image: registry.k8s.io/hpa-example
ports:
- containerPort: 80
resources:
limits:
cpu: 500m
requests:
cpu: 200m
---
apiVersion: v1
kind: Service
metadata:
name: php-apache
labels:
run: php-apache
spec:
ports:
- port: 80
selector:
run: php-apache
이걸 기본적인 스케일링 대상으로 삼는다.
이미지 이름만 봐도 알겠지만, 쿠버네티스에서 제공해주는 hpa 샘플용 이미지이다.
<?php
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
$x += sqrt($x);
}
echo "OK!";
?>
http 요청을 보내면 자체적으로 연산을 하면서 cpu를 사용하게 돼있다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
averageUtilization: 50
type: Utilization
hpa는 이렇게 만들었다.
잠시 해석하자면, cpu 사용률이 50퍼가 아닐 때 스케일링을 하겠다는 것이다.
위의 샘플은 cpu 요청량이 200m이며, 이것이 사용률을 계산할 때 분모로 들어가게 된다.
가만히 두면 이 상태가 될 것이다.
그라파나 대시보드는 22128을 사용했다.
스케일링 테스트 - 단일 파드
while true; do curl -s 파드; sleep 0.5; done
호스트로 들어가서 파드 ip에 지속적으로 요청을 날려본다.
조금 기다리다보면 이렇게 파드가 하나 늘어나게 되는 것을 확인할 수 있다.
대시보드로도 한번 사용률이 50퍼를 넘겼기 때문에 스케일업이 진행된 것이 확인된다.
그러나 여기에서 유의 깊게 볼 점은 더 이상 스케일링이 되지 않는다는 것이다.
현재 부하를 준 파드에선 요청 리소스(request) 200m의 절반을 넘기는 값으로 cpu 자원을 활용하고 있는 것이 보인다.
그렇지만 hpa는 현재 대상이 된 파드들의 전체 평균을 가지고 계산을 진행하기에, 한 파드의 지표가 아무리 높아도 전체적인 관점에서 사용률이 50퍼를 넘지 않기에 더 이상 스케일링이 되지 않는 것이다.
실제로도 hpa 기준에서는 현재 메트릭값이 35퍼로 잡히고 있다.
간단하게는 이렇게 계산해서 나오는 값이라는 것을 알 수 있다.
스케일링 테스트 - 전체 파드
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"
이번에는 스케일링이 진행되도 모든 파드가 요청을 수행할 수 있도록 서비스로 요청을 날려본다.
잠시 기다리자 파드가 6개까지 늘어나게 됐다.
순간 사용률이 폭증하고, 이에 대해 파드를 순식간에 여러 개 늘린 것이 확인된다.
behavior 기본값에 따르면 15초 이내에 한번에 최대로 늘어나는 개수는 현재 개수의 2배이므로, 일단 2배가 늘어난 뒤에 최종적으로 계산된 값인 6개로 증가한 것이다.
부하를 중단하자, 스케일다운이 진행된 것이 보인다.
이때 실제 메트릭이 줄어든 시점은 11:50인 반면 레플리카가 줄어든 시점은 그 이후인 것을 볼 수 있는데, 이것은 안정화 윈도우에 의해 최대한의 유예가 주어진 것으로 볼 수 있다.
스케일링 테스트 - 안정화 윈도우값에 의한 점진적 스케일 다운
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: stable-window
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
minReplicas: 1
maxReplicas: 12
metrics:
- type: Resource
resource:
name: cpu
target:
averageUtilization: 50
type: Utilization
behavior:
scaleDown:
stabilizationWindowSeconds: 180
policies:
- type: Pods
value: 4
periodSeconds: 30
- type: Percent
value: 10
periodSeconds: 30
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
조금 더 안정화 윈도우와 동작을 면밀히 살피고자 세부 스펙을 더 정의했다.
생각보다 부하가 덜 발생하는 것 같아서, 다른 터미널을 띄우고 똑같은 명령어를 또 쳐서 부하가 두번 발생하게 만들었더니 최종적으로는 10개까지 레플리카가 늘어났다.
3분을 텀으로 안정화 윈도우를 적용했더니 일단 최대 레플리카였던 10개가 유지됐다.
그 이상을 넘어가자, 한번에 최대로 줄어들 수 있는 개수인 4개씩 줄어들기 시작한 것이 확인된다.
커스텀 메트릭 사용하기
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm show values >> values.yaml
# values.yaml에서 프메 서버 url 설정
helm install my-release prometheus-community/prometheus-adapter
이번에는 Prom-Adapter를 이용해 커스텀 메트릭으로 HPA를 해본다.
values 파일을 받아오는 이유는, 여기에서 프로메테우스 서버 주소를 명시해줘야 하기 때문이다.
배포가 되면 디플로이먼트를 통해 어댑터 파드가 하나 생긴다.
k get apiservices.apiregistration.k8s.io
또한 apiservice 오브젝트가 하나 생긴 것도 확인할 수 있다.
버전이 앞에 등장하는데, 실제 api를 쓸 때 이 값은 하위 경로로 들어가게 된다.
k get cm prom-adaptor-prometheus-adapter -oyaml
각종 룰들은 컨피그맵으로 생긴다.
이것을 직접 수정해서 가져오는 메트릭들을 조정하는 것도 가능하다.
- seriesQuery: '{__name__=~"container_network_(receive|transmit)_packets_total",namespace!="",pod!=""}'
resources:
overrides:
namespace:
resource: namespace
pod:
resource: pod
name:
matches: ^container_network_(.*)_packets_total$
as: "packets_per_second"
metricsQuery: sum(rate(//.Series//{//.LabelMatchers//}[1m])) by (//.GroupBy//)
파드 단위로 오고가는 패킷을 메트릭으로 삼고자 이렇게 규칙을 작성했다.
k get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/packets_per_second" | jq
내 커스텀 메트릭이 잘 만들어졌다면 이게 잘 돼야 한다.
석세스!
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: custom-metric
spec:
minReplicas: 1
maxReplicas: 10
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
metrics:
- type: Pods
pods:
metric:
name: packets_per_second
target:
type: AverageValue
averageValue: 10000m
해당 값을 바로 이용해서 다시 HPA를 만들어본다.
다시 같은 방식으로 부하를 줄 때, 어마무시하게 빠르게 스케일링이 이뤄진다.
기준 값을 작게 설정한 것도 있지만, 스케일링된다고 해서 요청 수가 줄어드는 것도 아니라 평소 값이 크게 작아지지도 않는다.
replicaCount: 3
autoscaling:
enabled: false
service:
type: ClusterIP
ingress:
enabled: true
hostname: nginx.zerotay.com
path: /
pathType: Prefix
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn
alb.ingress.kubernetes.io/group.name: aews
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/load-balancer-name: terraform-eks-ingress-alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/success-codes: 200-399
ingressClassName: "alb"
metrics:
enabled: true
serviceMonitor:
enabled: true
이번에는 웹 어플리케이션을 켜고, 이걸 토대로 커스텀 메트릭을 지정해보자.
헬름 values는 이렇게 세팅했다.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install nginx bitnami/nginx --version 19.0.0 -f values.yaml
k get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/nginx_http_requests"
k get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/services/*/nginx_http_requests"
위 헬름에서 서비스 모니터를 설정했기 때문에, 바로 관련 메트릭을 조회해보는 것이 가능하다.
파드를 대상으로 메트릭을 지정할 수도 있으나, 아예 서비스를 대상으로 조회를 하는 편이 조금 더 편리할 것이다.
(근데 사실 메트릭의 근원지가 같아서 결국 다를 건 없다.)
- type: Object
object:
metric:
name: nginx_http_requests
describedObject:
apiVersion: v1
kind: Service
name: nginx
target:
type: Value
value: 10000m
hpa의 메트릭 필드는 이렇게 서비스를 대상으로 지정한다.
해당 값은 counter로, 그냥 total 값이라 줄어들지는 않는다.
아무튼 이렇게 다른 오브젝트의 메트릭을 활용하는 것도 가능하다!
KEDA
KEDA도 활용해보자.
이 녀석은 메트릭 기반이 아니라 이벤트 기반이라는 점에서 다르다고 할 수 있다.
하지만 내부적으로는 이벤트를 메트릭화시켜서 HPA를 구동한다.
나는 테라폼으로 세팅했다.
테스트
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq
케다가 설치되면 알아서 externalmetric에 데이터를 노출할 준비를 해준다.
https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json
케다에서 제공하는 그라파나 대시보드를 활용하여 모니터링을 진행한다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: php-apache-cron
spec:
minReplicaCount: 0
maxReplicaCount: 5
pollingInterval: 30
cooldownPeriod: 60
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
triggers:
- type: cron
metadata:
timezone: Asia/Seoul
start: "*/10 * * * *"
end: "5,15,25,35,45,55 * * * *"
desiredReplicas: "1"
5분 단위로 1개를 만들었다 말았다 하는 크론 스케일러를 만들었다.
참고로 cooldownPeriod의 기본값이 5분이기 때문에 여기에서 명시적으로 더 작은 값으로 지정했다.
5분을 주기로 스케일링이 동작하는 것이 확인된다.
hpa의 스펙을 보면 이렇게 메트릭에 external이 들어간 것을 확인할 수 있다.
k get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/{네임스페이스}/{메트릭이름}?labelSelector=scaledobject.keda.sh%2Fname%3D{스케일오브젝트}"
이런 식으로 직접적으로 메트릭을 관찰하는 것도 가능한데, 기본적으로 케다는 이벤트 기반으로 메트릭을 내주다보니 직접적으로 메트릭을 확인한다는 것이 조금 이상한 일이기도하다.
다만 케다에서는
특히 크론 스케일러는 확인하기가 정말 어려웠다.
만들어진 HPA에서도 이런 이벤트가 계속 발생하나, 실제로 동작은 잘 되고 있다.
vpa
이건 인플레이스를 쓰고 싶어서, 온프렘에서 하려고 하는데 최근에 노드간 통신 이슈가 있어서 조금 수정하고 해보려 한다.
Cluster Autoscaler
Cluster Autoscaler를 사용해보자.
세팅
관리되고자 하는 노드에는 다음의 태그가 들어가있어야 한다.
k8s.io/cluster-autoscaler/enabled : true
k8s.io/cluster-autoscaler/myeks : owned
이것은 관리대상이 되는 노드를 자동으로 탐색하기 위한 태그인데, 커스텀할 수 있기는 하다.
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
양식 파일을 다운 받아보면 맨 아랫줄에 오토스케일러 디플로이먼트 인자 설정을 할 수 있다.
auto discovery 인자에서 태그를 커스텀하면 된다.
sed -i -e "s|<YOUR CLUSTER NAME>|$CLUSTER_NAME|g" cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict="false"
오토스케일러가 배포된 노드는 삭제되지 않도록 축출을 막는 어노테이션을 미리 달아준다.
그런데 나는 이러한 에러를 겪었다.
비슷한 에러를 겪는 사람들이 있었는데, 특정 ami에서 발생하는 에러로 보인다.[1]
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler -n kube-system -f cluster-autoscaler-helm-values.yaml
이건 그냥 정말 개발 로직 상 버그인 것 같기도 한데, 헬름에서는 관련한 설정을 넣어줄 수 있는 것으로 보여 이쪽으로 방향을 선회했다.
autoDiscovery:
clusterName: terraform-eks
tags:
- k8s.io/cluster-autoscaler/enabled
- k8s.io/cluster-autoscaler/terraform-eks
awsRegion: ap-northeast-1
cloudProvider: aws
deployment:
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
extraArgs:
logtostderr: true
stderrthreshold: info
v: 4
skip-nodes-with-local-storage: false
expander: least-waste
skip-nodes-with-system-pods: false
rbac:
create: true
serviceAccount:
name: cluster-autoscaler
annotations:
eks.amazonaws.com/role-arn: {irasa role!}
service:
create: true
serviceMonitor:
enabled: true
namespace: monitoring
values 파일에서는 이 정도 커스텀을 해줬다.
프로메테우스 지표 수집을 하는 것도 가능해보이길래 해당 설정도 넣었는데, 추가적인 설정을 넣지 않은 건지 실제로 메트릭이 수집되지는 않았다.
만들어진 서비스모니터는 이 경로로 데이터를 긁어오는데..
실제 파드에는 관련 포트 정보가 없다.
아무래도 추가 설정을 해줘야 하는 모양인데 현재의 주제에서 조금 벗어나는 것 같아 생략했다.
제대로 설정이 됐다면 이런 식으로 노드와 파드 상태를 계속 확인하는 로그가 남아야 한다.
또한 컨피그맵으로 현재 상태 볼 수 있게 된다.
테스트 실패
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-to-scaleout
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
service: nginx
app: nginx
spec:
containers:
- image: nginx
name: nginx-to-scaleout
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 500m
memory: 512Mi
한 파드당 코어의 절반을 잡아먹는 괴물 디플로이먼트를 만들어본다.
이 친구를 스케일 아웃하면..
kubectl scale --replicas=15 deployment/nginx-to-scaleout && date
당장 스케줄링될 수 없는 다수의 파드가 pending 상태로 머물러 버린다.
그러나 막상 로그를 확인해보니 또 이런 이슈가 있다.
인스턴스 id를 잘못 참조하는 것이 아닐까 싶다.
관련한 이슈가 지속적으로 보이는데, 문제가 많은 것 같다.[2]
로그를 더 파다보니 파드에서는 이렇게 이벤트가 발생하는 것을 확인했다.
이것으로 인해 스케일업이 트리거되지 않는다.
그러나 이런 이슈가 있는 경우는 asg를 디스커버리하지 못할 때 발생한다고 나오던데, asg에도 태그가 잘 붙어있고 컨트롤러 파드의 인자에도 제대로 값이 들어가 있는 것을 확인했다.
혹시 몰라 공백문자가 있는지도 확인했고..
버전을 낮춰보기도 했고..
IRSA 설정이 잘못됐나도 확인해보고..
간단하게 확인하고 행복하게 카펜터로 넘어가는 그림을 기대했으나, 생각보다 녹록치가 않다.
지금으로서 마음에 걸리는 한 가지는, ami 이슈.
단순히 양식파일로 설치할 때 다른 사람들이 al2023 standard가 아니라 al2023을 사용해야 예제 양식 파일이 적용된다는 이슈를 달았던 것을 토대로 생각해볼 때, ami도 영향을 주는 요소가 아닌가 한다.
난감한 게 어디가 정확하게 문제인지 짚기가 힘들다는 것.
인스턴스를 못 찾는 게 문제냐?
파드에서 not scale up을 거는 게 문제냐?
궁극적으로는 같은 원인에서 다른 방향으로 이런 결과를 내는 것 같지만, 그 원인을 트러블슈팅하는 게 생각보다 쉽지 않다.
카펜터를 파보고, 조금 시간이 남을 때 재도전을 해볼까 한다.
프로메테우스로 메트릭 수집하는 것도 성공했는데, 에러가 하나도 없다고 합니다 하하..
cpa
비례.
카펜터
Karpenter에 정말 열심히 문서를 정리했다..
이제 실습을 하면서 어떤 식으로 동작하는지, 어떻게 쓰는 게 좋은지 확인할 시간이다.
테라폼 설치
https://github.com/terraform-aws-modules/terraform-aws-eks/tree/v20.33.1/modules/karpenter
이 모듈을 최대한 활용해보자.
물론 문제가 많이 생길 수 있기에, 가능한 참조만 한다.
그냥 하니까 이런 에러가 발생한다.
아니 이제 보니까 create 변수도 true 해줘야 제대로 생성되게 설정돼있네..
create 변수는 왜 있는 걸까?
어차피 개별 리소스에 대해서도 create인자가 있는데.. 모든 create 리소스 변수들을 활성화하기 위한 변수로서 두는 건가..
확인
기본적으로 이벤트브릿지로 인스턴스에 관련한 이벤트를 받을 수 있도록 설정된다.
각 룰은 각 이벤트를 받도록 돼있고, 이 이벤트를 SQS로 보낸다.
그에 맞는 SQS 또한 만들어지는데, 이 큐에서 카펜터가 이벤트를 받아서 보게 될 것이다.
https://artifacthub.io/packages/helm/aws-karpenter/karpenter
헬름 설치를 할 때, ECR을 활용한다.
단순 설치를 하니 클러스터 네임이 없다고 화를 낸다.
기본 프롬스택에서는 프로메테우스 쪽에 추가 설정을 넣었다.
이 값은 문서에 나와있다.[3]
그라파나 대시보드도 나와있으나[4], 현재 사용하고 있는 프로메테우스 스택의 values.yaml 파일에서는 설정할 수 있는 방법이 없다.
아예 프롬스택에서 그라파나를 빼고 따로 설치할까 하다가, 고작 대시보드 2개 하자고 분리하는 것도 좀 별로다 싶어서 그라파나 대시보드는 프로비저닝된 이후 직접 세팅하도록 한다.
https://karpenter.sh/v1.3/getting-started/getting-started-with-karpenter/karpenter-capacity-dashboard.json
https://karpenter.sh/v1.3/getting-started/getting-started-with-karpenter/karpenter-performance-dashboard.json
다음의 두 대시보드를 받으면 된다.
(깃헙 레포를 가보면 다른 json 파일도 있는데 하나는 컨트롤러 전체에 관한 거고, 다른 하나는 과거 버전을 기반으로 하는지 메트릭이 없으므로 주의하자)
설정이 제대로 된다면 알아서 디스커버리를 통해 메트릭을 수집하게 된다.
이미 한번 테스트한 상태라 메트릭이 찍힘 대시보드도 성공적으로 구축된 것이 확인된다.
테스트
현 실습에서는 카펜터를 쓰지 않고 있다가 적용하는 상황을 가정한다.
이에 따라 기존 인스턴스들은 유지한 채로 카펜터에게 관리를 맡기다가 점차 관리를 확장시키는 방식으로 실습해볼까 한다.
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
role: "카펜터를 만들 때 만들어진 node iam role"
amiSelectorTerms:
- alias: "al2023@latest" # ex) al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "terraform-eks"
Name: "*Public*"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "terraform-eks"
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["2"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h # 30 * 24h = 720h
limits:
cpu: 1000
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
노드풀과 노드 클래스를 만들어본다.
노드 클래스에는 퍼블릭 서브넷에만 배치되도록 세팅했고, 스팟 인스턴스를 활용해본다.
제대로 세팅이 됐다면, 이렇게 True가 돼야 한다.
준비 상태는 검증이 성공해야 하는데, 검증 상태 역시 위의 다른 상태들이 준비 상태가 돼야 통과되므로 준비 상태가 된 것만으로도 모든 것들이 제대로 추적이 되고 있다는 것을 확인할 수 있다.
프로비저닝
apiVersion: apps/v1
kind: Deployment
metadata:
name: inflate
spec:
replicas: 0
selector:
matchLabels:
app: inflate
template:
metadata:
labels:
app: inflate
spec:
terminationGracePeriodSeconds: 0
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: inflate
image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
resources:
requests:
cpu: 1
securityContext:
allowPrivilegeEscalation: false
이번에도 괴물 파드를 만들어본다.
cpu 요청량이 크므로 스케줄이 되지 않는 파드가 발생한다.
만약 한번도 스팟을 만들어본 적이 없다면 service linked role이 없어서이러한 에러가 발생하기에 위에서 세팅을 해준 것이다.
1분도 안 되어 한 노드가 새로 프로비저닝되기 시작했다.
파드가 배치되는 데에도 시간이 거의 걸리지 않았다.
컨트롤러의 로그를 보면, 도움이 필요한 파드를 찾은 후 노드 클레임을 작동시키고 등록하는 과정이 이뤄진 것이 보인다.
디프로비저닝
이번에는 파드를 하나 줄여보았다.
처음에는 c5a.xlarge의 노드를 배치해주었는데, 더 작은 스펙의 노드로 바꿔주면 성공이다.
시간이 지나자 새로운 노드가 또 프로비저닝됐다.
조금 더 시간이 지나니 이렇게 한 노드가 사라졌다.
로그로는 underutilzed 효율화를 위해 노드를 중단한다는 말이 뜬 이후, 새로운 노드클레임 생성, 이후 노후 노드 삭제 절차를 밟는 것이 보인다.
결국 c5a.xlarge에서 c5a.large로 스펙을 줄여주었다!
중간에 이런 이벤트가 관찰됐다.
이 이벤트가 의미하는 게 15개의 인스턴스 타입 선택지를 주지 않으면 Spot2Spot을 안 해준다는 것으로 이해했었는데, 지금 보면 결국 해주기는 한다.
다만 효율적인 비용최적화를 하지 못할 가능성이 높다는 의미인 듯하다.
(그럼 왜 Unconsolidatable이냐구)
클라우드트레일에 들어가서 이벤트를 보면, CreateFleet 이벤트가 많이 뜨는 것을 확인할 수 있다.
이때 카펜터는 비용 절감 방안을 찾기 위해 드라이런으로도 api를 요청하는 모양이다.
파드 스케줄링 제약 조건 체크
nodeSelector:
topology.kubernetes.io/zone: ap-northeast-2a
karpenter.k8s.aws/instance-cpu: "8"
파드를 만들면서 well-known 라벨을 이용해 노드에 배치되도록 해본다.
곧바로 설정한 제약을 준수하는 새로운 노드가 프로비저닝됐다.
추가적으로, 바로 통합이 진행되어 처음 테스트할 때 만들어졌던 노드가 삭제되며 기존의 파드가 새로운 노드로 이전됐다.
노드 스케줄링 제약 조건이 존재하는 상황에서, 제약에서 자유로운 파드를 아예 옮기는 식으로 효율적으로 비용을 최적화한 것이다.
이정도 똑똑함이라면 충분히 사람이 직접 비용을 최적화하기 위해 머리 싸매고 운영하는 수고로움을 덜 수 있을 것으로 생각된다.
스터디
alb컨에서 포트에 이름을 쓸 때 이슈가 있다.
ksm에 커스텀 메트릭.
이건 컨맵으로 들어간다.
# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-5week.yaml
# 변수 지정
CLUSTER_NAME=myeks
SSHKEYNAME=zero
MYACCESSKEY=AKIAR6VA7X35OEY5IJZY
MYSECRETKEY=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
# CloudFormation 스택 배포
aws cloudformation deploy --template-file myeks-5week.yaml --stack-name $CLUSTER_NAME --parameter-overrides KeyName=$SSHKEYNAME SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 MyIamUserAccessKeyID=$MYACCESSKEY MyIamUserSecretAccessKey=$MYSECRETKEY ClusterBaseName=$CLUSTER_NAME --region ap-northeast-2
# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text
eksctl get cluster
# kubeconfig 생성
aws sts get-caller-identity --query Arn
aws eks update-kubeconfig --name myeks --user-alias arn:aws:iam::134555352826:user/aews-admin
aws eks update-kubeconfig --name myeks --user-alias admin
#
kubectl ns default
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -A
kubectl get pdb -n kube-system
# EC2 공인 IP 변수 지정
export N1=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2a" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N2=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2b" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N3=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=myeks-ng1-Node" "Name=availability-zone,Values=ap-northeast-2c" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
echo $N1, $N2, $N3
# *remoteAccess* 포함된 보안그룹 ID
aws ec2 describe-security-groups --filters "Name=group-name,Values=*remoteAccess*" | jq
export MNSGID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=*remoteAccess*" --query 'SecurityGroups[*].GroupId' --output text)
# 해당 보안그룹 inbound 에 자신의 집 공인 IP 룰 추가
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr $(curl -s ipinfo.io/ip)/32
# 해당 보안그룹 inbound 에 운영서버 내부 IP 룰 추가
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr 172.20.1.100/32
# 워커 노드 SSH 접속
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh -o StrictHostKeyChecking=no ec2-user@$i hostname; echo; done
이건 ssh해서
# default 네임스페이스 적용
kubectl ns default
# 환경변수 정보 확인
export | egrep 'ACCOUNT|AWS_|CLUSTER|KUBERNETES|VPC|Subnet'
export | egrep 'ACCOUNT|AWS_|CLUSTER|KUBERNETES|VPC|Subnet' | egrep -v 'KEY'
# krew 플러그인 확인
kubectl krew list
# 인스턴스 정보 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# 노드 IP 확인 및 PrivateIP 변수 지정
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3
# 노드 IP 로 ping 테스트
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ping -c 1 $i ; echo; done
이건 내컴
cat << EOF >> ~/.zshrc
# eksworkshop
export CLUSTER_NAME=myeks
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
export N1=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node" "Name=availability-zone,Values=ap-northeast-2a" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N2=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node" "Name=availability-zone,Values=ap-northeast-2b" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export N3=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node" "Name=availability-zone,Values=ap-northeast-2c" --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
export CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
MyDomain=zerotay.com
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "$MyDomain." --query "HostedZones[0].Id" --output text)
EOF
# [신규 터미널] 확인
echo $CLUSTER_NAME $VPCID $PubSubnet1 $PubSubnet2 $PubSubnet3
echo $N1 $N2 $N3 $MyDomain $MyDnzHostedZoneId
tail -n 15 ~/.zshrc
# AWS LoadBalancerController
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
--set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
# ExternalDNS
echo $MyDomain
curl -s https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml | MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst | kubectl apply -f -
# gp3 스토리지 클래스 생성
cat <<EOF | kubectl apply -f -
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
allowAutoIOPSPerGBIncrease: 'true'
encrypted: 'true'
fsType: xfs # 기본값이 ext4
EOF
kubectl get sc
# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=ClusterIP --set env.TZ="Asia/Seoul" --namespace kube-system
# kubeopsview 용 Ingress 설정 : group 설정으로 1대의 ALB를 여러개의 ingress 에서 공용 사용
echo $CERT_ARN
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
alb.ingress.kubernetes.io/group.name: study
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/load-balancer-name: $CLUSTER_NAME-ingress-alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/success-codes: 200-399
alb.ingress.kubernetes.io/target-type: ip
labels:
app.kubernetes.io/name: kubeopsview
name: kubeopsview
namespace: kube-system
spec:
ingressClassName: alb
rules:
- host: kubeopsview.$MyDomain
http:
paths:
- backend:
service:
name: kube-ops-view
port:
number: 8080 # name: http
path: /
pathType: Prefix
EOF
# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하니, 4주차 실습과 다르게 PV/PVC 미사용
cat <<EOT > monitor-values.yaml
prometheus:
prometheusSpec:
scrapeInterval: "15s"
evaluationInterval: "15s"
podMonitorSelectorNilUsesHelmValues: false
serviceMonitorSelectorNilUsesHelmValues: false
retention: 5d
retentionSize: "10GiB"
# Enable vertical pod autoscaler support for prometheus-operator
verticalPodAutoscaler:
enabled: true
ingress:
enabled: true
ingressClassName: alb
hosts:
- prometheus.$MyDomain
paths:
- /*
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
alb.ingress.kubernetes.io/success-codes: 200-399
alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
alb.ingress.kubernetes.io/group.name: study
alb.ingress.kubernetes.io/ssl-redirect: '443'
grafana:
defaultDashboardsTimezone: Asia/Seoul
adminPassword: prom-operator
defaultDashboardsEnabled: false
ingress:
enabled: true
ingressClassName: alb
hosts:
- grafana.$MyDomain
paths:
- /*
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
alb.ingress.kubernetes.io/success-codes: 200-399
alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
alb.ingress.kubernetes.io/group.name: study
alb.ingress.kubernetes.io/ssl-redirect: '443'
kube-state-metrics:
rbac:
extraRules:
- apiGroups: ["autoscaling.k8s.io"]
resources: ["verticalpodautoscalers"]
verbs: ["list", "watch"]
customResourceState:
enabled: true
config:
kind: CustomResourceStateMetrics
spec:
resources:
- groupVersionKind:
group: autoscaling.k8s.io
kind: "VerticalPodAutoscaler"
version: "v1"
labelsFromPath:
verticalpodautoscaler: [metadata, name]
namespace: [metadata, namespace]
target_api_version: [apiVersion]
target_kind: [spec, targetRef, kind]
target_name: [spec, targetRef, name]
metrics:
- name: "vpa_containerrecommendations_target"
help: "VPA container recommendations for memory."
each:
type: Gauge
gauge:
path: [status, recommendation, containerRecommendations]
valueFrom: [target, memory]
labelsFromPath:
container: [containerName]
commonLabels:
resource: "memory"
unit: "byte"
- name: "vpa_containerrecommendations_target"
help: "VPA container recommendations for cpu."
each:
type: Gauge
gauge:
path: [status, recommendation, containerRecommendations]
valueFrom: [target, cpu]
labelsFromPath:
container: [containerName]
commonLabels:
resource: "cpu"
unit: "core"
selfMonitor:
enabled: true
alertmanager:
enabled: false
defaultRules:
create: false
kubeControllerManager:
enabled: false
kubeEtcd:
enabled: false
kubeScheduler:
enabled: false
prometheus-windows-exporter:
prometheus:
monitor:
enabled: false
EOT
cat monitor-values.yaml
# helm 배포
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 69.3.1 \
-f monitor-values.yaml --create-namespace --namespace monitoring
# helm 확인
helm get values -n monitoring kube-prometheus-stack
# PV 사용하지 않음
kubectl get pv,pvc -A
kubectl df-pv
#
kubectl get targetgroupbindings.elbv2.k8s.aws -A
#
kubectl get clusterrole kube-prometheus-stack-kube-state-metrics
kubectl describe clusterrole kube-prometheus-stack-kube-state-metrics
kubectl describe clusterrole kube-prometheus-stack-kube-state-metrics | grep verticalpodautoscalers
관련 문서
이름 | noteType | created |
---|
참고
https://github.com/aws/karpenter-provider-aws/blob/main/website/content/en/docs/getting-started/getting-started-with-karpenter/prometheus-values.yaml ↩︎
https://github.com/aws/karpenter-provider-aws/blob/main/website/content/en/docs/getting-started/getting-started-with-karpenter/grafana-values.yaml ↩︎