블로그 불러오는 중...
문의 보내기
남겨주면 블로그 주인에게 바로 전달돼요.
오늘을 EKS의 파드와 노드 스케일링에 대한 실습을 진행해 본다.
# 코드 다운로드, 작업 디렉터리 이동
git clone https://github.com/gasida/aews.git
cd aews/3w
# IAM Policy 파일 작성
curl -o aws_lb_controller_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json
cat << EOF > externaldns_controller_policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets",
"route53:ListTagsForResources"
],
"Resource": [
"arn:aws:route53:::hostedzone/*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones"
],
"Resource": [
"*"
]
}
]
}
EOF
cat << EOF > cas_autoscaling_policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeScalingActivities",
"ec2:DescribeImages",
"ec2:DescribeInstanceTypes",
"ec2:DescribeLaunchTemplateVersions",
"ec2:GetInstanceTypesFromInstanceRequirements",
"eks:DescribeNodegroup"
],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": [
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup"
],
"Resource": ["*"]
}
]
}
EOF
ls *.json
aws_lb_controller_policy.json cas_autoscaling_policy.json externaldns_controller_policy.json
# 배포 : 12분 소요
terraform init
terraform plan
nohup sh -c "terraform apply -auto-approve" > create.log 2>&1 &
tail -f create.log
# 배포 완료 후 상세 정보 확인
cat terraform.tfstate
terraform show
terraform state list
terraform state show 'module.eks.aws_eks_cluster.this[0]'
terraform state show 'module.eks.data.tls_certificate.this[0]'
terraform state show 'module.eks.aws_eks_access_entry.this["cluster_creator"]'
terraform state show 'module.eks.aws_iam_openid_connect_provider.oidc_provider[0]'
terraform state show 'module.eks.data.aws_partition.current[0]'
...
terraform state show 'module.eks.time_sleep.this[0]'
terraform state show 'module.vpc.aws_vpc.this[0]'
...
# EKS 자격증명 설정
$(terraform output -raw configure_kubectl)
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') myeks
# k8s 1.35 버전 확인
kubectl get node -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ip-192-168-14-130.ap-northeast-2.compute.internal Ready <none> 34m v1.35.2-eks-f69f56f 192.168.14.130 <none> Amazon Linux 2023.10.20260302 6.12.73-95.123.amzn2023.x86_64 containerd://2.2.1+unknown
ip-192-168-16-49.ap-northeast-2.compute.internal Ready <none> 34m v1.35.2-eks-f69f56f 192.168.16.49 <none> Amazon Linux 2023.10.20260302 6.12.73-95.123.amzn2023.x86_64 containerd://2.2.1+unknown이번 실습에서는 EKS 노드를 들어갈 때에 Systems Manager 를 이용해 들어간다. 기존에 pem 키를 이용하는 것이 아닌, 아마존의 System Manager를 통해 연결하게 된다.
AWS Loadbalancer, External DNS, CAS Autoscaler 에 필요한 정책들을 다운받는다.
그 이후 테라폼을 실행한다.
실행이 다 되면, ssm 을 이용해 접속을 시도해 본다.
세션 매니저 플러그인을 설치해야 하는데 설치는 여기 링크 를 확인한다.
나는 윈도우에서 wsl 을 이용해 우분투 기반으로 작업했기 때문에 아래와 같이 작업했다.
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
sudo dpkg -i session-manager-plugin.debaws ssm describe-instance-information \
--query "InstanceInformationList[*].{InstanceId:InstanceId, Status:PingStatus, OS:PlatformName}" \
--output text
aws ssm start-session --target i-09aedb02c42e5be7b위 명령어를 입력 시 해당 타겟에 ssh 접근을 할 수 있다.
aws eks list-addons --cluster-name myeks | jq
kubectl get deploy -n kube-system metrics-server
kubectl describe deploy -n kube-system metrics-server
kubectl get pod -n kube-system -l app.kubernetes.io/instance=metrics-server -owide
kubectl get pdb -n kube-system metrics-server
kubectl get svc,ep -n kube-system metrics-server
# metrics-server api 관련 정보 확인
kubectl api-resources | grep -i metrics
kubectl explain NodeMetrics
kubectl explain PodMetrics
kubectl api-versions | grep metrics
kubectl get apiservices |egrep '(AVAILABLE|metrics)'
# 노드/파드 cpu/mem 자원 시용 확인
kubectl top node
kubectl top pod -A
kubectl top pod -n kube-system --sort-by='cpu'
kubectl top pod -n kube-system --sort-by='memory'
# external-dns 관련 정보 확인
kubectl get deploy,pod,svc,ep,sa -n external-dns
kubectl get sa -n external-dns external-dns -o yaml
kubectl describe deploy -n external-dns external-dns애드온 정보들을 확인 해 보았다. external-dns 에서 policy가 upsert-only 이므로, 실습 끝나고 도메인을 전체 수동으로 삭제해야 한다.
# Helm Chart Repository 추가
helm repo add eks https://aws.github.io/eks-charts
helm repo update
# Helm Chart - AWS Load Balancer Controller 설치 : EC2 Instance Profile(IAM Role)을 파드가 IMDS 통해 획득 가능!
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --version 3.1.0 \
--set clusterName=myeks
# 확인
helm list -n kube-system
kubectl get pod -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller
kubectl logs -n kube-system deployment/aws-load-balancer-controller -fIMDS 홉을 2로 설정하는 것이 실습 테라폼 안에 내장되어 있어서, 이번엔 바로 설치 후 사용이 된다.
추가로 맨 처음에 정책을 json 으로 받아 테라폼에서 추가도 해주었기 때문에 role 도 바인딩이 된다.
# macOS 설치
brew tap aws/tap
brew install eks-node-viewer
# Windows 에 WSL2 (Ubuntu) 설치
sudo apt update
sudo apt install golang-go
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest # 설치 시 2~3분 정도 소요
echo 'export PATH="$PATH:/root/go/bin"' >> /etc/profile
---
# 아래처럼 바이너리파일 다운로드 후 사용 가능 : 버전 체크
wget -O eks-node-viewer https://github.com/awslabs/eks-node-viewer/releases/download/v0.7.4/eks-node-viewer_Linux_x86_64
chmod +x eks-node-viewer
sudo mv -v eks-node-viewer /usr/local/bin나는 리눅스 기반의 wsl 이라서, 위 중간 방식으로 설치했다.

사용 예시는 아래와 같다.
# [신규 터미널] 모니터링 : eks 자격증명 필요
# Standard usage
eks-node-viewer
# Display both CPU and Memory Usage
eks-node-viewer --resources cpu,memory
eks-node-viewer --resources cpu,memory --extra-labels eks-node-viewer/node-age
# Display extra labels, i.e. AZ : node 에 labels 사용 가능
eks-node-viewer --extra-labels topology.kubernetes.io/zone
eks-node-viewer --extra-labels kubernetes.io/arch
# Sort by CPU usage in descending order
eks-node-viewer --node-sort=eks-node-viewer/node-cpu-usage=dsc
# Karenter nodes only
eks-node-viewer --node-selector "karpenter.sh/provisioner-name"
# Specify a particular AWS profile and region
AWS_PROFILE=myprofile AWS_REGION=us-west-2
Computed Labels : --extra-labels
# eks-node-viewer/node-age - Age of the node
eks-node-viewer --extra-labels eks-node-viewer/node-age
eks-node-viewer --extra-labels topology.kubernetes.io/zone,eks-node-viewer/node-age
# eks-node-viewer/node-ephemeral-storage-usage - Ephemeral Storage usage (requests)
eks-node-viewer --extra-labels eks-node-viewer/node-ephemeral-storage-usage
# eks-node-viewer/node-cpu-usage - CPU usage (requests)
eks-node-viewer --extra-labels eks-node-viewer/node-cpu-usage
# eks-node-viewer/node-memory-usage - Memory usage (requests)
eks-node-viewer --extra-labels eks-node-viewer/node-memory-usage
# eks-node-viewer/node-pods-usage - Pod usage (requests)
eks-node-viewer --extra-labels eks-node-viewer/node-pods-usage# kube-ops-view : NodePort 나 LoadBalancer Type 필요 없음!
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
# 확인
kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view
# 사용 리전의 인증서 ARN 변수 지정 : 정상 상태 확인(만료 상태면 에러 발생!)
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
echo $CERT_ARN
# 자신의 공인 도메인 변수 지정
MyDomain=devchanki.com
echo $MyDomain
MyDomain=gasida.link
echo $MyDomain
# kubeopsview 용 Ingress 설정 : group 설정으로 1대의 ALB를 여러개의 ingress 에서 공용 사용
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: myeks-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
path: /
pathType: Prefix
EOF
# service, ep, ingress 확인
kubectl get ingress,svc,ep -n kube-system
# Kube Ops View 접속 정보 확인
echo -e "Kube Ops View URL = https://kubeopsview.$MyDomain/#scale=1.5"
open "https://kubeopsview.$MyDomain/#scale=1.5" # macOS
# (참고) 삭제 시
kubectl delete ingress -n kube-system kubeopsview나는 도메인이 클라우드플레어에 이전되어 있어, ACM 에서 asterisk 도메인을 만들어 주었다. 그리고 클라우드플레에어서 수동으로 도메인 추가했다.

acm 을 위한 도메인 추가, 그리고 kubeopsview 를 위해 추가했다 .
위와 같이 정상적으로 추가 된 것을 확인 가능하다.
# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# helm values 파일 생성 : additionalScrapeConfigs 는 아래 설명
cat <<EOT > monitor-values.yaml
prometheus:
prometheusSpec:
podMonitorSelectorNilUsesHelmValues: false
serviceMonitorSelectorNilUsesHelmValues: false
additionalScrapeConfigs:
# apiserver metrics
- job_name: apiserver-metrics
kubernetes_sd_configs:
- role: endpoints
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels:
[
__meta_kubernetes_namespace,
__meta_kubernetes_service_name,
__meta_kubernetes_endpoint_port_name,
]
action: keep
regex: default;kubernetes;https
# Scheduler metrics
- job_name: 'ksh-metrics'
kubernetes_sd_configs:
- role: endpoints
metrics_path: /apis/metrics.eks.amazonaws.com/v1/ksh/container/metrics
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels:
[
__meta_kubernetes_namespace,
__meta_kubernetes_service_name,
__meta_kubernetes_endpoint_port_name,
]
action: keep
regex: default;kubernetes;https
# Controller Manager metrics
- job_name: 'kcm-metrics'
kubernetes_sd_configs:
- role: endpoints
metrics_path: /apis/metrics.eks.amazonaws.com/v1/kcm/container/metrics
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels:
[
__meta_kubernetes_namespace,
__meta_kubernetes_service_name,
__meta_kubernetes_endpoint_port_name,
]
action: keep
regex: default;kubernetes;https
# 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
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'
kubeControllerManager:
enabled: false
kubeEtcd:
enabled: false
kubeScheduler:
enabled: false
prometheus-windows-exporter:
prometheus:
monitor:
enabled: false
EOT
cat monitor-values.yaml
# 배포
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 80.13.3 \
-f monitor-values.yaml --create-namespace --namespace monitoring
# 확인
helm list -n monitoring
kubectl get sts,ds,deploy,pod,svc,ep,ingress -n monitoring
kubectl get prometheus,servicemonitors -n monitoring
kubectl get crd | grep monitoring
kubectl get-all -n monitoring # kubectl krew install get-all
# 프로메테우스 버전 확인
kubectl exec -it sts/prometheus-kube-prometheus-stack-prometheus -n monitoring -c prometheus -- prometheus --version
prometheus, version 3.1.0 (branch: HEAD, revision: 7086161a93b262aa0949dbf2aba15a5a7b13e0a3)
...
# 프로메테우스 웹 접속
echo -e "https://prometheus.$MyDomain"
open "https://prometheus.$MyDomain" # macOS
# 그라파나 웹 접속 : admin / prom-operator
echo -e "https://grafana.$MyDomain"
open "https://grafana.$MyDomain" # macOS
다음과 같이 정상적으로 그라파나도 접근 되는 것을 볼 수 있다.
두개의 메트릭이 수집이 안되는 것이 있다.
기본 배포한 서비스 어카운트에 해당 두개의 메트릭을 수집할 권한이 없는 것이다.
해당 내용을 확인 해 보자.
krew 설치 후 rolesum 을 설치해야 한다. 일단은 role 만 추가해도 정상적으로 메트릭이 수집 되는 것을 볼 수 있다.
# helm values 파일 생성 : additionalScrapeConfigs 다시 살펴보기!
helm get values -n monitoring kube-prometheus-stack
# Metrics.eks.amazonaws.com의 컨트롤 플레인 지표 가져오기 : kube-scheduler , kube-controller-manager 지표
kubectl get --raw "/apis/metrics.eks.amazonaws.com/v1/ksh/container/metrics"
kubectl get --raw "/apis/metrics.eks.amazonaws.com/v1/kcm/container/metrics"
kubectl get svc,ep -n kube-system eks-extension-metrics-api
kubectl get apiservices |egrep '(AVAILABLE|metrics)'
# 프로메테우스 파드 정보 확인
kubectl describe pod -n monitoring prometheus-kube-prometheus-stack-prometheus-0 | grep 'Service Account'
Service Account: kube-prometheus-stack-prometheus
# 해당 SA에 권한이 없음!
kubectl rbac-tool lookup kube-prometheus-stack-prometheus # kubectl krew install rbac-tool
kubectl rolesum kube-prometheus-stack-prometheus -n monitoring # kubectl krew install rolesum
...
Policies:
• [CRB] */kube-prometheus-stack-prometheus ⟶ [CR] */kube-prometheus-stack-prometheus
Resource Name Exclude Verbs G L W C U P D DC
endpoints [*] [-] [-] ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖
endpointslices.discovery.k8s.io [*] [-] [-] ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖
ingresses.networking.k8s.io [*] [-] [-] ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖
nodes [*] [-] [-] ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖
nodes/metrics [*] [-] [-] ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖
pods [*] [-] [-] ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖
services [*] [-] [-] ✔ ✔ ✔ ✖ ✖ ✖ ✖ ✖
# 클러스터롤에 권한 추가
kubectl get clusterrole kube-prometheus-stack-prometheus
kubectl patch clusterrole kube-prometheus-stack-prometheus --type=json -p='[
{
"op": "add",
"path": "/rules/-",
"value": {
"verbs": ["get"],
"apiGroups": ["metrics.eks.amazonaws.com"],
"resources": ["kcm/metrics", "ksh/metrics"]
}
}
]'
kubectl rolesum kube-prometheus-stack-prometheus -n monitoring

curl -O https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/refs/heads/master/dashboards/k8s-system-api-server.json
# sed 명령어로 uid 일괄 변경 : 기본 데이터소스의 uid 'prometheus' 사용
sed -i -e 's/${DS_PROMETHEUS}/prometheus/g' k8s-system-api-server.json
# my-dashboard 컨피그맵 생성 : Grafana 포드 내의 사이드카 컨테이너가 grafana_dashboard="1" 라벨 탐지!
kubectl create configmap my-dashboard --from-file=k8s-system-api-server.json -n monitoring
kubectl label configmap my-dashboard grafana_dashboard="1" -n monitoring
# 대시보드 경로에 추가 확인
kubectl exec -it -n monitoring deploy/kube-prometheus-stack-grafana -- ls -l /tmp/dashboards
그라파나의 사이드카 컨테이너가 대시보드 설정이 config 에 추가되는지 동적으로 확인한다고 한다.
Kubernetes / System / API Server 대시보드를 확인한다.

추가가 잘 되고 잘 출력 되는 것을 볼 수 있다.
현재 실습에서는 2개의 관리 그룹이 있다. 온디맨드 관리형 노드그룹, 그리고 AWS Graviton 기반의 인스턴스 2개의 관리 그룹이 있다. 거기에 추가로 spot instance 를 통한 노드그룹을 만들어 볼 예정이다.
먼저 관리형 노드 그룹인 myeks-ng-1 을 확인해 본다
# 노드 정보 확인
kubectl get nodes --label-columns eks.amazonaws.com/nodegroup,kubernetes.io/arch,eks.amazonaws.com/capacityType
# 관리형 노드 그룹 확인
eksctl get nodegroup --cluster myeks
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-ng-1 | jq아래와 같이 온디맨드 관리형 노드를 볼 수 있다.

두번째 관리 그룹인 myeks-ng-2 를 생성하고 확인 해 본다.
처음에 받았던 eks.tf 파일에서 두번째 노드그룹이 주석되어 있는데 그 부분을 해제하고 terraform plan -> terraform apply 를 실행한다.
terraform plan
terraform apply -auto-approve
# The aws eks wait nodegroup-active command can be used to wait until a specific EKS node group is active and ready for use.
aws eks wait nodegroup-active --cluster-name myeks --nodegroup-name myeks-ng-2실행 하면 kube-ops-view 에 노드가 추가가 된다.
추가된 노드 그룹 정보들을 확인해 본다.
# 신규 노드 그룹 생성 확인
kubectl get nodes --label-columns eks.amazonaws.com/nodegroup,kubernetes.io/arch,eks.amazonaws.com/capacityType
eksctl get nodegroup --cluster myeks
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-ng-2 | jq
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-ng-2 | jq .nodegroup.taints
[
{
"key": "cpuarch",
"value": "arm64",
"effect": "NO_EXECUTE" # 스케줄링 하지 않음 - 노드상에서 조건이 일치하지 않는 파드는 동작X. Taint 설정 전 이미 스케줄링된 파드(Toleration 미설정된)도 Evict됨.
}
]
# k8s 노드 정보 확인
kubectl get node -l kubernetes.io/arch=arm64
kubectl get node -l tier=secondary -owide
kubectl describe node -l tier=secondary | grep -i taint
# SSM 관리 대상 인스턴스 목록 조회
aws ec2 describe-instances \
--instance-ids $(aws ssm describe-instance-information \
--query "InstanceInformationList[?PingStatus=='Online'].InstanceId" \
--output text) \
--query "Reservations[].Instances[].{
InstanceId:InstanceId,
Type:InstanceType,
Arch:Architecture,
AMI:ImageId,
State:State.Name
}" \
--output table
# 인스턴스 접속 후 arch 확인
aws ssm start-session --target i-08b07a8575315a3a7해당 노드그룹 확인 이후에 pod를 해당 부분에 배포해 본다
# sample-app 디플로이먼트 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
labels:
app: sample-app
spec:
replicas: 1
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
nodeSelector:
kubernetes.io/arch: arm64
containers:
- name: sample-app
image: nginx:alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
EOF
# 확인
kubectl describe pod -l app=sample-app
# 파드에 tolerations 설정으로 배치 실행!
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
labels:
app: sample-app
spec:
replicas: 1
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
nodeSelector:
kubernetes.io/arch: arm64
tolerations:
- key: "cpuarch"
operator: "Equal"
value: "arm64"
effect: "NoExecute"
containers:
- name: sample-app
image: nginx:alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
EOF
kubectl get events -w --sort-by '.lastTimestamp'
# 확인
kubectl get pod -l app=sample-app
kubectl describe pod -l app=sample-app
# 삭제
kubectl delete deploy sample-apptaint torelation 이 걸려있기 때문에, 파드가 올라가지 않는다. 해당 사항을 변경하고 다시 파드를 배포한다.
그리고 arm 형태의 노드이기 때문에, 멀티 아키텍쳐 이미지라던지 이런것만 배포할 수 있다. 이정도를 알아보고, 해당 노드는 제거한다.
주석을 푼 부분을 제거하고 terraform apply -auto-approve 를 실행한다.
kube-ops-view 에도 추가 된 것을 볼 수 있다.
스팟 인스턴스는 가용 자원이 있을때만 가능하다. 그래서 여러개 인스턴스 타입을 추가해서 실습한다.
스팟 인스턴스 추가를 위해 eks.tf 의 세번째 노드그룹의 주석을 풀어 준다.
그리고 terraform apply 하여 스팟인스턴스 노드를 추가해 본다.

서비스 링크 롤을 만들어야 한다. 나를 대신해서 AWS 가 리소스를 조작할 수 있게 해야 한다.
# EC2 Spot Fleet의 service-linked-role 생성 확인 : 만들어있는것을 확인하는 거라 아래 에러 출력이 정상!
# 목적 : AWS 서비스(여기서는 Spot)가 사용자를 대신하여 다른 AWS 리소스(EC2 등)를 조작할 수 있는 권한을 가진 전용 역할을 만듭
# 이유 : 스팟 인스턴스를 요청하면, AWS 내부 시스템이 알아서 인스턴스를 띄우고 회수해야 합니다. 이 역할을 수행하려면 AWSServiceRoleForEC2Spot이라는 이름의 역할이 계정에 반드시 존재해야 합니다.
# 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스팟 인스턴스를 확인 해 본다.
# 신규 노드 그룹 생성 확인
kubectl get nodes --label-columns eks.amazonaws.com/nodegroup,kubernetes.io/arch,eks.amazonaws.com/capacityType
kubectl get nodes -L eks.amazonaws.com/capacityType
NAME STATUS ROLES AGE VERSION CAPACITYTYPE
ip-192-168-15-173.ap-northeast-2.compute.internal Ready <none> 3m20s v1.35.2-eks-f69f56f SPOT
ip-192-168-16-236.ap-northeast-2.compute.internal Ready <none> 132m v1.35.2-eks-f69f56f ON_DEMAND
ip-192-168-23-145.ap-northeast-2.compute.internal Ready <none> 132m v1.35.2-eks-f69f56f ON_DEMAND
eksctl get nodegroup --cluster myeks
CLUSTER NODEGROUP STATUS CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID ASG NAME TYPE
myeks myeks-ng-1 ACTIVE 2026-03-25T12:11:31Z 1 4 2 t3.medium AL2023_x86_64_STANDARD eks-myeks-ng-1-c0ce9274-159c-bf55-e5f2-820078f71e89 managed
myeks myeks-ng-3 ACTIVE 2026-03-25T14:21:03Z 1 1 1 c5a.large,c6a.large,t3a.large,t3a.medium AL2023_x86_64_STANDARD eks-myeks-ng-3-fcce92af-5fca-7f66-2ad3-997d4920638b managed
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-ng-3 | jq
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-ng-3 | jq .nodegroup.instanceTypes
[
"c5a.large",
"c6a.large",
"t3a.large",
"t3a.medium"
]
kubectl get node -l tier=third
kubectl get node -l eks.amazonaws.com/capacityType=SPOT
kubectl describe node -l eks.amazonaws.com/capacityType=SPOT
eks-node-viewer --extra-labels eks-node-viewer/node-age
ip-192-168-21-223.ap-northeast-2.compute.internal cpu ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 18% (9 pods) t3.medium/$0.0520 On-Demand - Ready 110m
ip-192-168-18-42.ap-northeast-2.compute.internal cpu ███████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 19% (10 pods) t3.medium/$0.0520 On-Demand - Ready 110m
ip-192-168-14-46.ap-northeast-2.compute.internal cpu ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 8% (3 pods) t3a.medium/$0.0142 Spot - Ready 83s
# 스팟 EC2 인스턴스 확인
aws ec2 describe-instances \
--filters "Name=instance-lifecycle,Values=spot" \
--query "Reservations[].Instances[].{ID:InstanceId,Type:InstanceType,AZ:Placement.AvailabilityZone,State:State.Name}" \
--output table
# 스팟 요청 확인 : Spot Instance Request
aws ec2 describe-spot-instance-requests \
--query "SpotInstanceRequests[].{ID:SpotInstanceRequestId,State:State,Type:Type,InstanceId:InstanceId}" \
--output table
# Spot 가격 조회 : # t3a.large t3a.medium
aws ec2 describe-spot-price-history \
--instance-types c6a.large c5a.large \
--product-descriptions "Linux/UNIX" \
--max-items 100 \
--query "SpotPriceHistory[].{Type:InstanceType,Price:SpotPrice,AZ:AvailabilityZone,Time:Timestamp}" \
--output table
배포해 본다
# 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: busybox
spec:
terminationGracePeriodSeconds: 3
containers:
- name: busybox
image: busybox
command:
- "/bin/sh"
- "-c"
- "while true; do date >> /home/pod-out.txt; cd /home; sync; sync; sleep 10; done"
nodeSelector:
eks.amazonaws.com/capacityType: SPOT
EOF
# 파드가 배포된 노드 정보 확인
kubectl get pod -owide
# 삭제
kubectl delete pod busyboxnode-selector 에 SPOT 을 설정해 스팟 인스턴스에 배포되게 셋팅 된 것을 볼 수 있다.
스케일링 관련 4가지 테크닉이 있다.

1. Application Tuning (애플리케이션 튜닝) 가장 기본이 되는 단계로, 코드나 설정을 통해 개별 프로세스의 효율을 극대화하는 방식입니다.
핵심 동작: 프로세스 내의 스레드(Threads) 수 조절, 힙 메모리(Heap size) 최적화 등.
특징: 하드웨어를 늘리기 전에 소프트웨어가 자원을 최대한 잘 쓰도록 만듭니다. 가장 비용 효율적이지만 한계가 명확합니다.
2. VPA: Vertical Pod Autoscaler (수직적 포드 자동 확장) 포드(Pod) 하나가 사용할 수 있는 '그릇'의 크기를 키우는 방식입니다.
핵심 동작: 컨테이너에 할당된 CPU나 메모리 리소스(Requests/Limits)를 자동으로 늘리거나 줄입니다.
특징: 개별 작업의 부하가 클 때 유리합니다. 하지만 리소스를 변경할 때 포드를 재시작해야 할 수도 있다는 점을 유의해야 합니다.
3. HPA: Horizontal Pod Autoscaler (수평적 포드 자동 확장) 포드의 '개수'를 늘려 부하를 분산하는 가장 대중적인 방식입니다.
핵심 동작: CPU 사용률이나 트래픽이 설정치를 넘으면 동일한 포드를 추가로 생성(Scale-out)하고, 한가해지면 제거(Scale-in)합니다.
특징: 서비스 중단 없이 유연하게 대응할 수 있어 고가용성 확보에 필수적입니다.
4. CA: Cluster Autoscaler (클러스터 자동 확장) 포드를 담을 '그릇의 바닥(Node)' 자체가 부족할 때 사용하는 인프라 수준의 확장입니다.
핵심 동작: 더 이상 포드를 배치할 노드 공간이 없으면 가상 머신(Node)을 추가로 생성합니다. 반대로 노드가 놀고 있으면 노드를 삭제합니다.
특징: 물리적인 자원(서버 인스턴스) 자체를 조절하므로 클라우드 비용과 직결되는 단계입니다.
그라파나(22128 , 22251) 대시보드 Import 한다.
다음과 같이 추가하면 된다.
아래 명령어를 통해 HPA 테스트용 파드를 배포한다.
# 확인
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# curl 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: curl
spec:
containers:
- name: curl
image: curlimages/curl:latest
command: ["sleep", "3600"]
restartPolicy: Never
EOF
# 서비스명으로 호출 : 'kubectl exec -it deploy/php-apache -- top' 에 CPU 증가 확인!
kubectl exec -it curl -- curl php-apache
kubectl exec -it curl -- curl php-apache오토스케일링 정책을 추가한다
# Create the HorizontalPodAutoscaler : requests.cpu=200m - [알고리즘](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#algorithm-details)
# Since each pod requests 200 milli-cores by kubectl run, this means an average CPU usage of 100 milli-cores.
cat <<EOF | kubectl apply -f -
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
EOF
# 혹은
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
kubectl exec curl -- sh -c 'while true; do curl -s php-apache; sleep 0.01; done' 명령어로 부하를 계속 줘 본다.
다음과 같이 오토스케일링이 된 것을 볼 수 있다.
kubectl delete deploy,svc,hpa,pod --all 로 전체 삭제한다.
해당 방식은 파드의 한도를 올리는 방식이다.

해당 실습을 위해 위에처럼 grafana 대시보드에 14588 를 추가한다.

keda 는 이벤트 기반의 오토스케일러다. 표준 HPA는 CPU/메모리 기반으로만 동작하지만, KEDA는 특정 이벤트(큐에 쌓인 메시지, 특정 시간대 등) 에 반응하여 스케일링을 수행합니다.
링크 의 json 을 대시보드에 추가한다.
# 설치 전 기존 metrics-server 제공 Metris API 확인
kubectl get --raw "/apis/metrics.k8s.io" -v=6 | jq
kubectl get --raw "/apis/metrics.k8s.io" | jq
{
"kind": "APIGroup",
"apiVersion": "v1",
"name": "metrics.k8s.io",
...
# KEDA 설치 : serviceMonitor 만으로도 충분할듯..
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
operator:
enabled: true
port: 8080
serviceMonitor:
# Enables ServiceMonitor creation for the Prometheus Operator
enabled: true
webhooks:
enabled: true
port: 8020
serviceMonitor:
# Enables ServiceMonitor creation for the Prometheus webhooks
enabled: true
EOT
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --version 2.16.0 --namespace keda --create-namespace -f keda-values.yaml
# KEDA 설치 확인
kubectl get crd | grep keda
kubectl get all -n keda
kubectl get validatingwebhookconfigurations keda-admission -o yaml
kubectl get podmonitor,servicemonitors -n keda
kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml
# CPU/Mem은 기존 metrics-server 의존하여, KEDA metrics-server는 외부 이벤트 소스(Scaler) 메트릭을 노출
## https://keda.sh/docs/2.16/operate/metrics-server/
kubectl get pod -n keda -l app=keda-operator-metrics-apiserver
# Querying metrics exposed by KEDA Metrics Server
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq
{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "external.metrics.k8s.io/v1beta1",
"resources": [
{
"name": "externalmetrics",
"singularName": "",
"namespaced": true,
"kind": "ExternalMetricValueList",
"verbs": [
"get"
]
}
]
}
# keda 네임스페이스에 디플로이먼트 생성
kubectl apply -f https://k8s.io/examples/application/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 # Specifies the maximum number of replicas to scale up to (defaults to 100).
pollingInterval: 30 # Specifies how often KEDA should check for scaling events
cooldownPeriod: 300 # Specifies the cool-down period in seconds after a scaling event
scaleTargetRef: # Identifies the Kubernetes deployment or other resource that should be scaled.
apiVersion: apps/v1
kind: Deployment
name: php-apache
triggers: # Defines the specific configuration for your chosen scaler, including any required parameters or settings
- 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
# 그라파나 대시보드 추가 : 대시보드 상단에 namespace : keda 로 변경하기!
# KEDA 대시보드 Import : https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json
# 모니터링
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 ScaledObject -n keda php-apache-cron-scaled && kubectl delete deploy php-apache -n keda && helm uninstall keda -n keda
kubectl delete namespace keda
예제는 시간 기준으로 파드를 올리고 내리고 하는 실습이다.

"클러스터 덩치에 비례해서 늘어나는 방식"
노드 개수가 많아지면, 클러스터를 관리하는 핵심 파드(예: CoreDNS)의 부하도 커집니다. CPA는 노드 수 또는 전체 CPU 코어 수에 비례하여 파드 수를 조절합니다.
아래와 같이, 노드의 수에 따라 파드를 제어한다던지 이런 식으로 이해하면 좋을 것 같다.
nodesToReplicas:
- [1, 1]
- [2, 2]
- [3, 3]
- [4, 3]
- [5, 5]
"전통적인 노드 오토스케일러"
Karpenter가 나오기 전까지 표준이었던 방식입니다. AWS의 ASG(Auto Scaling Group) 와 긴밀하게 연동되어 동작합니다.


아래와 같은 태그가 추가 되어 있어야 한다.
k8s.io/cluster-autoscaler/enabled : true
k8s.io/cluster-autoscaler/myeks : owned

cli 를 통해서도 확인할 수 있다.
# EKS 노드에 이미 아래 tag가 들어가 있음
# https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md#auto-discovery-setup
aws ec2 describe-instances --filters Name=tag:Name,Values=myeks-ng-1 --query "Reservations[*].Instances[*].Tags[*]" --output json | jq
aws ec2 describe-instances --filters Name=tag:Name,Values=myeks-ng-1 --query "Reservations[*].Instances[*].Tags[*]" --output yaml
...
- Key: k8s.io/cluster-autoscaler/myeks
Value: owned
- Key: k8s.io/cluster-autoscaler/enabled
Value: 'true'
...아래 명령어를 통해, CAS 를 설치한다.
# 현재 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
# 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
# 배포 : Deploy the Cluster Autoscaler (CAS)
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
...
- ./cluster-autoscaler
- --v=4
- --stderrthreshold=info
- --cloud-provider=aws
- --skip-nodes-with-local-storage=false # 로컬 스토리지를 가진 노드를 autoscaler가 scale down할지 결정, false(가능!)
- --expander=least-waste # 노드를 확장할 때 어떤 노드 그룹을 선택할지를 결정, least-waste는 리소스 낭비를 최소화하는 방식으로 새로운 노드를 선택.
- --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/<YOUR CLUSTER NAME>
...
sed -i -e "s|<YOUR CLUSTER NAME>|myeks|g" cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml
# 확인
kubectl get pod -n kube-system | grep cluster-autoscaler
kubectl describe deployments.apps -n kube-system cluster-autoscaler
kubectl describe deployments.apps -n kube-system cluster-autoscaler | grep node-group-auto-discovery
--node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/myeks
# (옵션) cluster-autoscaler 파드가 동작하는 워커 노드가 퇴출(evict) 되지 않게 설정
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict="false"추가로 파드를 배포하여 노드가 증가하는지를 확인한다.
# 모니터링
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 --resources cpu,memory
혹은
eks-node-viewer
# [운영서버 EC2] 최근 1시간 Fleet API 호출 확인 - Link
# https://ap-northeast-2.console.aws.amazon.com/cloudtrailv2/home?region=ap-northeast-2#/events?EventName=CreateFleet
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateFleet \
--start-time "$(date -d '1 hour ago' --utc +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"
# (참고) Event name : UpdateAutoScalingGroup
# https://ap-northeast-2.console.aws.amazon.com/cloudtrailv2/home?region=ap-northeast-2#/events?EventName=UpdateAutoScalingGroup
# 디플로이먼트 삭제
kubectl delete -f nginx.yaml && date
# [scale-down] 노드 갯수 축소 : 기본은 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
아래와 같이, 조금 기다리면 신규로 노드가 생성되는 것을 볼 수 있다.
kubeopsview 에서도 확인이 가능하다.

리소스를 삭제한다.
kubectl delete -f nginx.yaml
# size 수정
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 3 --desired-capacity 3 --max-size 3
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
# Cluster Autoscaler 삭제
kubectl delete -f cluster-autoscaler-autodiscover.yaml# 프로메테우스-스택 삭제
helm uninstall -n monitoring kube-prometheus-stack
# kube-ops-view 삭제
kubectl delete ingress -n kube-system kubeopsview
helm uninstall kube-ops-view --namespace kube-system
# 테라폼으로 삭제
terraform destroy -auto-approve
생성된 도메인은 수동으로 삭제한다.