[CloudNeta] EKS 워크샵 스터디 (1) - EKS 소개 #1

이번 게시글에서는 EKS 워크샵 스터디 제 1주차 내용을 작성합니다.

이 글은 2부로 나누어집니다.

  1. 1주차 - EKS 소개 #1 (현재 보고계신 글)
  2. 1주차 - EKS 소개 #2

EKS 에 대한 소개와 간략한 배포, 그리고 클러스터 접근모드에 대한 내용을 살펴봅니다.

이번 장에서 배울 것

이번 장에서는 아래 내용을 다룹니다:

  • EKS 아키텍처 살펴보기
  • EKS 배포를 테라폼으로 해보기
  • EKS Cluster Endpoint Access 모드 별 동작 살펴보기
  • EKS Fully Private Cluster 동작 구성과 고려사항

EKS란?

쿠버네티스 클러스터의 운영은 AWS에게 맡기고, 실제 쿠버네티스 운영을 AWS의 환경 하에 작업할 수 있도록 하는 서비스입니다. 다시말해 클러스터의 운영, 구축, 보안, 유지/관리를 AWS와 통합시켜서 관리하고, 운영에 집중하게 하는 것이죠.

EKS 생성방법에 따라 아래 두 가지 방안으로 쓸 수 있습니다.

그림으로 보시면 아래와 같습니다:

EKS comparison between EKS standard vs EKS Auto Mode

EKS가 관리해주는 부분

  1. 관리 인터페이스
    • eksctl 로 하거나
    • kubectl을 그대로 사용하되, AWS 권한을 부여받은 후 주소를 등록하여 사용할 수도 있습니다.
  2. 액세스 제어
    • EKS는 AWS IAM과 Kubernetes의 인증/인가 체계를 양방향으로 통합합니다.
    • 클러스터 접근 제어: IAM 사용자/역할을 Kubernetes RBAC 권한에 매핑하여, 누가 kubectl이나 AWS Console로 클러스터 리소스를 조회/수정할 수 있는지 제어합니다(Access Entry, aws-auth ConfigMap).
    • 워크로드 권한 제어: Kubernetes ServiceAccount를 IAM Role에 연결하여, Pod 내 프로세스가 S3, DynamoDB 등 AWS 리소스에 접근할 수 있게 합니다(Pod Identity, IRSA)[1].
  3. 컴퓨팅 리소스
    • 자체 관리노드(EC2 직접 관리)
    • 관리형 노드 그룹(EC2 프로비저닝, 업데이트)
    • EKS Auto Mode(EC2 기반인데 자동업데이트까지 AWS가 처리)
    • Fargate(노드 개념없이 파드단위로 수행)
    • Hybrid Nodes(온프렘 물리/가상머신을 EKS 클러스터와 함께 관리)
  4. 스토리지
    • EBS 볼륨으로 자동화
    • S3, EFS, FSX, 등등을 관리
  5. 보안
    • 관리해주는 부분이 있지만 기본적으로 공동책임입니다.
    • 노드, 컨테이너 및 운영체제, 애플리케이션, 네트워크 구성, 데이터 민감도 등에 대해서는 관리자가 책임을 두어야 합니다.
  6. Observability
    • controlplane 로깅을 필요에 따라 수집할 수 있습니다.
    • 노드의 메트릭과 워크로드의 메트릭을 필요에 따라 수집할 수 있습니다.
    • 수집된 데이터들을 관리하여 살펴볼 수 있습니다.
  7. k8s 호환성
    • k8s 호환 애플리케이션 및 커뮤니티 도구나 플러그인을 활용할 수 있습니다.

Pricing

EKS 배포

EKS 배포이후, 컨트롤플레인과 워커노드가 어떻게 되어있나 살펴보겠습니다.

배포를 위한 인프라 코드는 아래 링크에서 보실 수 있습니다.

코드 살펴보기 - Terraform을 이용한 가장 기본적인 클러스터 배포 코드

EKS 컨트롤플레인 살펴보기

위 게시글을 통해 kubectl 구성까지 끝낸 후, 클러스터 정보를 직접 살펴봅시다.

# 권한이 없으면 이런식으로 나오고요,
 kubectl cluster-info

aws: [ERROR]: An error occurred (NoCredentials): Unable to locate credentials. You can configure credentials by running "aws login".
E0316 14:29:22.276247   64044 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com/api?timeout=32s\": getting credentials: exec: executable aws failed with exit code 253"


# 권한이 있다면 이렇게 잘 나옵니다.
 kubectl cluster-info
Kubernetes control plane is running at https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

엔드포인트 살펴보기

그럼 이 클러스터의 기본 엔드포인트를 살펴봅시다.

이 엔드포인트는 뭐를 의미하나요?

EKS 컨트롤플레인의 API 서버 주소를 의미합니다.
EKS는 컨트롤플레인을 알아서 운영해주는데, 이 엔드포인트 주소를 가지고 붙을 수 있게 하는 거죠.

$ CLUSTER_NAME=myeks
$ aws eks describe-cluster --name $CLUSTER_NAME | jq

결과는 이렇게 나옵니다.

클러스터 주소에 dig 해봅시다.

 dig +short $APIDNS
52.79.150.59
54.116.106.230

 curl -s ipinfo.io/52.79.150.59
{
  "ip": "52.79.150.59",
  "hostname": "ec2-52-79-150-59.ap-northeast-2.compute.amazonaws.com",
  "city": "Incheon",
  "region": "Incheon",
  "country": "KR",
  "loc": "37.4565,126.7052",
  "org": "AS16509 Amazon.com, Inc.",
  "postal": "21505",
  "timezone": "Asia/Seoul",
  "readme": "https://ipinfo.io/missingauth"
}

EKS 노드 그룹 살펴보기

그렇다면 노드 그룹의 정보도 살펴보죠.

 aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name $CLUSTER_NAME-node-group | jq

그럼 노드정보도 한번 볼까요?

쉽게말하면 k get nodes 한 내용에서 추가 컬럼을 더 살펴보는 겁니다. 이러면 인스턴스 타입, capacity type, zone 정보까지 다 나오죠.

 k get node
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-1-199.ap-northeast-2.compute.internal   Ready    <none>   37m   v1.34.4-eks-f69f56f
ip-192-168-2-160.ap-northeast-2.compute.internal   Ready    <none>   37m   v1.34.4-eks-f69f56f

 kubectl get node --label-columns=node.kubernetes.io/instance-type
NAME                                               STATUS   ROLES    AGE   VERSION               INSTANCE-TYPE
ip-192-168-1-199.ap-northeast-2.compute.internal   Ready    <none>   39m   v1.34.4-eks-f69f56f   t3.medium
ip-192-168-2-160.ap-northeast-2.compute.internal   Ready    <none>   39m   v1.34.4-eks-f69f56f   t3.medium

# 이렇게 컬럼이 잘못기재되면 안나오고
 kubectl get node --label-columns=node.kubernetes.io/capacityType
NAME                                               STATUS   ROLES    AGE   VERSION               CAPACITYTYPE
ip-192-168-1-199.ap-northeast-2.compute.internal   Ready    <none>   39m   v1.34.4-eks-f69f56f
ip-192-168-2-160.ap-northeast-2.compute.internal   Ready    <none>   39m   v1.34.4-eks-f69f56f

# 어느 레이블 컬럼에 맞는지 제대로 해야 나오죠
 kubectl get node --label-columns=eks.amazonaws.com/capacityType
NAME                                               STATUS   ROLES    AGE   VERSION               CAPACITYTYPE
ip-192-168-1-199.ap-northeast-2.compute.internal   Ready    <none>   40m   v1.34.4-eks-f69f56f   ON_DEMAND
ip-192-168-2-160.ap-northeast-2.compute.internal   Ready    <none>   40m   v1.34.4-eks-f69f56f   ON_DEMAND

# 한번에보기는 이렇게 할 수 있습니다.
 kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
NAME                                               STATUS   ROLES    AGE   VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
ip-192-168-1-199.ap-northeast-2.compute.internal   Ready    <none>   37m   v1.34.4-eks-f69f56f   t3.medium       ON_DEMAND      ap-northeast-2a
ip-192-168-2-160.ap-northeast-2.compute.internal   Ready    <none>   37m   v1.34.4-eks-f69f56f   t3.medium       ON_DEMAND      ap-northeast-2b

그럼 이어서 인증관련 내용도 살펴볼까요?

 kubectl get node -v=6
I0316 14:56:19.198880   84746 cmd.go:527] kubectl command headers turned on
I0316 14:56:19.256942   84746 loader.go:402] Config loaded from file:  /home/l4in/.kube/config
I0316 14:56:19.257162   84746 envvar.go:172] "Feature gate default state" feature="WatchListClient" enabled=false
I0316 14:56:19.257193   84746 envvar.go:172] "Feature gate default state" feature="ClientsAllowCBOR" enabled=false
I0316 14:56:19.257199   84746 envvar.go:172] "Feature gate default state" feature="ClientsPreferCBOR" enabled=false
I0316 14:56:19.257203   84746 envvar.go:172] "Feature gate default state" feature="InOrderInformers" enabled=true
I0316 14:56:19.257207   84746 envvar.go:172] "Feature gate default state" feature="InformerResourceVersion" enabled=false
I0316 14:56:20.003858   84746 round_trippers.go:632] "Response" verb="GET" url="https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500" status="200 OK" milliseconds=743
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-1-199.ap-northeast-2.compute.internal   Ready    <none>   41m   v1.34.4-eks-f69f56f
ip-192-168-2-160.ap-northeast-2.compute.internal   Ready    <none>   41m   v1.34.4-eks-f69f56f

kubeconfig 을 흔히 보는 yaml 양식에 맞게 다시 보시죠.

 k config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com
  name: arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/myeks
contexts:
- context:
    cluster: arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/myeks
    user: arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/myeks
  name: myeks
current-context: myeks
kind: Config
users:
- name: arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/myeks
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - --region
      - ap-northeast-2
      - eks
      - get-token
      - --cluster-name
      - myeks
      - --output
      - json
      command: aws
      env: null
      interactiveMode: IfAvailable
      provideClusterInfo: false

그리고 EKS 클러스터에 사용하는 임시 토큰도 살펴보시죠. JWT 토큰이 아닌 점에 유의하시기 바랍니다.

 aws eks get-token --cluster-name $CLUSTER_NAME --region ap-northeast-2 | jq
{
  "kind": "ExecCredential",
  "apiVersion": "client.authentication.k8s.io/v1beta1",
  "spec": {},
  "status": {
    "expirationTimestamp": "2026-03-16T06:13:51Z",
    "token": "k8s-aws-v1.<REDACTED>"
  }
}

시스템 파드정보 살펴보기

그렇다면 이 인증정보로 파드정보도 살펴봅시다.

당연한 얘기지만 헷갈리까봐....

kubectl 로 RBAC 범위 내의 작업이 된다는 것이지, 컨트롤플레인에 접속하는 것이 아닙니다! 즉,

  • 컨트롤플레인에 쉘 접근은 안됩니다
  • 컨트롤플레인 컴포넌트 일부(상술한 etcd, kube-apiserver, kube-controller-manager, kube-scheduler)는 안보입니다
    • kubeadm으로 띄웠다면 StaticPod로 다 볼 수 있었겠지만요. 이 관리를 AWS가 위임해주는거죠
 k get pods -n kube-system
NAME                      READY   STATUS    RESTARTS   AGE
aws-node-klqxp            2/2     Running   0          118m
aws-node-rlmg4            2/2     Running   0          118m
coredns-d487b6fcb-7hk2d   1/1     Running   0          117m
coredns-d487b6fcb-tjln6   1/1     Running   0          117m
kube-proxy-9x6j7          1/1     Running   0          117m
kube-proxy-r5skf          1/1     Running   0          117m

 k get pods -n kube-system -owide
NAME                      READY   STATUS    RESTARTS   AGE    IP              NODE                                               NOMINATED NODE   READINESS GATES
aws-node-klqxp            2/2     Running   0          121m   192.168.2.160   ip-192-168-2-160.ap-northeast-2.compute.internal   <none>           <none>
aws-node-rlmg4            2/2     Running   0          121m   192.168.1.199   ip-192-168-1-199.ap-northeast-2.compute.internal   <none>           <none>
coredns-d487b6fcb-7hk2d   1/1     Running   0          120m   192.168.2.58    ip-192-168-2-160.ap-northeast-2.compute.internal   <none>           <none>
coredns-d487b6fcb-tjln6   1/1     Running   0          120m   192.168.1.138   ip-192-168-1-199.ap-northeast-2.compute.internal   <none>           <none>
kube-proxy-9x6j7          1/1     Running   0          120m   192.168.1.199   ip-192-168-1-199.ap-northeast-2.compute.internal   <none>           <none>
kube-proxy-r5skf          1/1     Running   0          120m   192.168.2.160   ip-192-168-2-160.ap-northeast-2.compute.internal   <none>           <none>

 k get pod -A
NAMESPACE     NAME                      READY   STATUS    RESTARTS   AGE
kube-system   aws-node-klqxp            2/2     Running   0          123m
kube-system   aws-node-rlmg4            2/2     Running   0          123m
kube-system   coredns-d487b6fcb-7hk2d   1/1     Running   0          122m
kube-system   coredns-d487b6fcb-tjln6   1/1     Running   0          122m
kube-system   kube-proxy-9x6j7          1/1     Running   0          122m
kube-system   kube-proxy-r5skf          1/1     Running   0          122m

가만보면 온프렘 쿠버네티스에서의 코어 리소스가 안보입니다.

그리고 aws-node 라는 요소가 있습니다. 이것은 후술할 VPC CNI 플러그인 입니다. ENI로 파드에 VPC IP를 직접 할당하죠.

또한 kube-systemk get deploy,ds,pod,cm,secret,svc,ep,endpointslice,pdb,sa,role,rolebinding -n kube-system 명령을 내려, 어떤 요소가 있나 살펴봅시다.

그리고 이 파드의 컨테이너 정보도 함께 보시죠.

 kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort | uniq -c
      2 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon-k8s-cni:v1.21.1-eksbuild.3
      2 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon/aws-network-policy-agent:v1.3.1-eksbuild.1
      2 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/coredns:v1.13.2-eksbuild.3
      2 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/kube-proxy:v1.34.3-eksbuild.5

그리고 kube-proxy, coredns, aws-node 의 정보도 살펴보시죠. 이 정보는 너무 방대해서 별도 내용으로 살펴보는 것이 좋겠습니다.

네트워크 설정 살펴보기 - kube-proxy, coredns, aws-node 까지 살펴보기

애드온 정보 살펴보기

설치된 애드온의 내용도 읽어보시죠.

 aws eks list-addons --cluster-name myeks | jq
{
  "addons": [
    "coredns",
    "kube-proxy",
    "vpc-cni"
  ]
}

이를 AWS 콘솔에서 살펴보면 아래와 같이 볼 수 있습니다:

001-addon-lists

EKS의 워커노드 살펴보기

그렇다면 컨트롤플레인을 살펴봤으니 워커노드를 살펴봅시다. 먼저 관리형 노드그룹부터 살펴보시죠.

EKS 노드그룹은 EC2 ASG(Auto Scaling Group) 으로 관리됩니다. 따라서 이는 노드가 스케일아웃될 때 어떤 설정으로 EC2 가 구동되는지를 살펴보는 것입니다.

앞서 EKS 모듈생성 시 노드그룹을 생성할 때의 코드를 다시 보시죠. 일부를 살펴보겠습니다.

# EKS Managed Node Group(s)
eks_managed_node_groups = {
  default = {
    name             = "${var.ClusterBaseName}-node-group"
    use_name_prefix  = false
    instance_types   = ["${var.WorkerNodeInstanceType}"]
    desired_size     = var.WorkerNodeCount
    max_size         = var.WorkerNodeCount + 2
    min_size         = var.WorkerNodeCount - 1
    disk_size        = var.WorkerNodeVolumesize
    subnets          = module.vpc.public_subnets
    key_name         = "${var.KeyName}"
    vpc_security_group_ids = [aws_security_group.node_group_sg.id]

    # AL2023 전용 userdata 주입
    cloudinit_pre_nodeadm = [
      {
        content_type = "text/x-shellscript"
        content      = <<-EOT
          #!/bin/bash
          echo "Starting custom initialization..."
          dnf update -y
          dnf install -y tree bind-utils
          echo "Custom initialization completed."
        EOT
      }
    ]
  }
}

AL2023으로 구동되는 워커노드에, 앞서 코드로 살펴보았던 userdata가 nodeadm 구성 전 담고있는 컨텐츠가 담겨있는 것을 볼 수 있습니다.

002-worker-node-init

노드를 SSH로 접속해보기

아래와같이 public ip를 확인하고 ping 을 쏴봅시다.

aews on main [!?] on ☁️  (ap-northeast-2)
 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
--------------------------------------------------------------------
|                         DescribeInstances                        |
+------------------+-----------------+------------------+----------+
|   InstanceName   |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+------------------+-----------------+------------------+----------+
|  myeks-node-group|  192.168.2.160  |  16.184.46.4     |  running |
|  myeks-node-group|  192.168.1.199  |  13.125.181.227  |  running |
+------------------+-----------------+------------------+----------+

aews on main [!?] on ☁️  (ap-northeast-2)
 ping -c 1 $NODE1
PING 16.184.46.4 (16.184.46.4) 56(84) bytes of data.
64 bytes from 16.184.46.4: icmp_seq=1 ttl=119 time=5.07 ms

--- 16.184.46.4 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 5.067/5.067/5.067/0.000 ms

aews on main [!?] on ☁️  (ap-northeast-2)
 ping -c 1 $NODE2
PING 13.125.181.227 (13.125.181.227) 56(84) bytes of data.
64 bytes from 13.125.181.227: icmp_seq=1 ttl=119 time=5.46 ms

--- 13.125.181.227 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 5.456/5.456/5.456/0.000 ms

SSH가 붙을 수 있는지 보안그룹도 확인해보고,

 aws ec2 describe-security-groups --filters "Name=tag:Name,Values=myeks-node-group-sg" --query 'SecurityGroups[*].IpPermissions' --output text
-1
IPRANGES        <REDACTED>/32
IPRANGES        <REDACTED>/32

워커노드 SSH에 접속해봅시다.

# 설정을 마친 후
ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no ec2-user@$NODE1 hostname
ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no ec2-user@$NODE2 hostname
ssh -o StrictHostKeyChecking=no ec2-user@$NODE1 hostname
ssh -o StrictHostKeyChecking=no ec2-user@$NODE2 hostname

# 붙어봅시다
ssh ec2-user@$NODE1
exit

ssh ec2-user@$NODE2
exit

접속 후 아래 사항을 확인해봅시다 - 운영체제 수준의 설정

EKS 워커노드가 파드를 받고 실행하기 위해서는 운영체제 레벨에서 갖추어야 할 전제조건이 있습니다. 이를 위한 아래 요소들을 살펴볼 것입니다.

호스트 정보 살펴보기

기본 커널정보와 하드웨어 모델을 살펴봅니다.

[root@ip-192-168-1-199 ~]# hostnamectl
Virtualization: amazon
Operating System: Amazon Linux 2023.10.20260302
    CPE OS Name: cpe:2.3:o:amazon:amazon_linux:2023
        Kernel: Linux 6.12.73-95.123.amzn2023.x86_64
    Architecture: x86-64
Hardware Vendor: Amazon EC2
Hardware Model: t3.medium
Firmware Version: 1.0

SELinux 설정 살펴보기

쿠버네티스는 Permissive 상태를 권장하기 때문에 살펴봅니다

[root@ip-192-168-1-199 ~]# getenforce
Permissive
[root@ip-192-168-1-199 ~]# sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   permissive
Mode from config file:          permissive
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

Swap 비활성화

kubelet의 권장사항. swap 영역을 안쓰길 원하기때문에, 노드에서 안쓰는지 살펴봅니다

[root@ip-192-168-1-199 ~]# free -h
            total        used        free      shared  buff/cache   available
Mem:           3.7Gi       338Mi       2.1Gi       1.0Mi       1.3Gi       3.2Gi
Swap:             0B          0B          0B
[root@ip-192-168-1-199 ~]# cat /etc/fstab
#
UUID=d306b125-f320-4f7c-8e41-c19d118b25e5     /           xfs    defaults,noatime  1   1
UUID=3D07-3F7F        /boot/efi       vfat    defaults,noatime,uid=0,gid=0,umask=0077,shortname=winnt,x-systemd.automount 0 2

cgroup v2 를 사용중인지 확인

v1.31 부터 cgroup v2로 넘어가도록 권장되었기 때문입니다. 참고링크

[root@ip-192-168-1-199 ~]# stat -fc %T /sys/fs/cgroup/
cgroup2fs

overlay 커널모듈 로드 확인

컨테이너의 파일 시스템 레이어 구성때문[2]입니다.

[root@ip-192-168-1-199 ~]# lsmod | grep overlay
overlay               217088  7

containerd 스냅샷 목록 보기[3]

# committed, active 를 구별해서 봅니다
ctr -n k8s.io snapshots ls

WARN[0000] DEPRECATION: The `bin_dir` property of `[plugins."io.containerd.cri.v1.runtime".cni`] is deprecated since containerd v2.1 and will be removed in containerd v2.3. Use `bin_dirs` in the same section instead.
KEY                                                                     PARENT                                                                  KIND
0e262d76e07ecd76963de327daec1e9d140865cfbd718de0ed31470f583d762d        sha256:1d29ddbe3fde80e61545aa67618e2c9f9c66c6c10514bf0b25c462e6c7ca95ea Active
1049323a8f33b8ea94ed51af317f5ca67d24ed7636c36009d29dee369c3756ff        sha256:d04a986803441bd3b5d521f093922b292e6adf38b1d473f320f3175b564ac95c Active
...
sha256:0bd8e49760ea4d211127dd0f42d909f2c066a4eaeeff687dd69d024ce4beeb71                                                                         Committed
sha256:1d29ddbe3fde80e61545aa67618e2c9f9c66c6c10514bf0b25c462e6c7ca95ea sha256:ef780f5b36fc0627e5662be1a6fb38fecf3b631c7ce925c4f62c65210d336dc4 Committed
...
sha256:49a7e69f58181559d964d51d3b5599480c9efad4d5b163095d0d163f6ef96882 sha256:cc074875b494aa2a3446b7977f22ea0c3b7e37bf850611614c3c82d6df96253b Committed
sha256:53a62a8cd216d79469b5fa82ff265302c5c00878bdeaad00042da40a049311b9                                                                         Committed
sha256:6978733d716fbedf6708fb8062a8aaf2236c77356ed8b5f583b54e758bf3cd2f sha256:8da4d76fd64eb2a33751853a1f60f672fcc204d5c5e6d84a8350890b1aad0eee Committed
sha256:7aadb4f9fcfff538563b51bad2fa2eb5e44e4fdf55b5712720b09260f755e084 sha256:53a62a8cd216d79469b5fa82ff265302c5c00878bdeaad00042da40a049311b9 Committed
...
sha256:f51db364c6b29a572234d37e29bac943ca56de484a471a241f098192bafba7c0 sha256:332cd5791f6431b7da15c3d7f47cc734d62aaeda09959775946713a4e20ee88c Committed

# 실제 노드 디스크 어디에서 용량을 차지하고있나 살펴볼 수 있습니다
ls -la /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/

[root@ip-192-168-2-160 ~] ls -la /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/
total 16
drwx------. 36 root root 16384 Mar 16 05:15 .
drwx------.  3 root root    42 Mar 11 20:30 ..
drwx------.  4 root root    28 Mar 11 20:33 10
...
drwx------.  4 root root    28 Mar 16 05:15 42
drwx------.  4 root root    28 Mar 16 05:15 43

# 어떻게 합쳐져서 컨테이너의 rootfs가 되는지 볼 수 있습니다.
tree /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/ -L 3
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/
├── 12                          # 베이스 OS 레이어 (committed snapshot)
   ├── fs
   │   ├── bin -> usr/bin
   │   ├── etc
   │   ├── home
   │   ├── lib -> usr/lib
   │   ├── proc
   │   ├── usr
   │   └── var
   └── work
├── 13                          # 그 위에 쌓인 이미지 레이어 (committed snapshot)
   ├── fs
   │   ├── etc
   │   ├── usr
   │   └── var
   └── work
├── 10                          # pause 컨테이너 레이어
   ├── fs
   │   └── pause
   └── work
├── 42                          # coredns 바이너리 레이어
   ├── fs
   │   └── coredns
   └── work
├── 16                          # active snapshot (컨테이너 쓰기 레이어)
   ├── fs
   │   ├── etc
   │   ├── host
   │   └── run
   └── work
       └── work                # work 하위에 work가 있으면 overlayfs가
└── ...                         # 실제 마운트해서 사용 중이라는 뜻

containerd 의 경우, AI의 도움을 받아 레이어를 살펴보는 스크립트를 작성하여 구동해 보았습니다.

접속 후 아래 사항을 확인해봅시다 - 노드 서버 내 서비스 구동상태

위와같은 전제조건이 갖춰지면 각 컴포넌트가 정상작동 하고 있는지 살펴보면 되겠죠. 전제조건이 되어있다고 해서 클러스터가 정상일 보장은 없습니다. 설정이 어긋나거나 서비스가 죽어있다면 파드가 구동되지 않기 때문입니다. 그래서 아래 요소들을 살펴보기로 합니다.

time 동기화 확인

time sync 서비스의 주소값은 여기서 확인하실 수 있습니다. IPv4, IPv6 정보를 함께볼 수 있죠.

# 설정을 보고,
[root@ip-192-168-2-160 ~]# grep "^[^#]" /etc/chrony.conf
sourcedir /run/chrony.d
confdir /etc/chrony.d
sourcedir /etc/chrony.d
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
keyfile /etc/chrony.keys
ntsdumpdir /var/lib/chrony
logdir /var/log/chrony
log measurements statistics tracking

# time 서버의 풀을 확인합니다.
[root@ip-192-168-2-160 ~]# tree /run/chrony.d/
/run/chrony.d/
├── amazon-pool.sources -> /usr/share/amazon-chrony-config/amazon-pool_aws.sources
└── link-local-ipv4.sources -> /usr/share/amazon-chrony-config/link-local-ipv4_unspecified.sources

0 directories, 2 files

# 어느 서버로부터 값을 가져오는지 확인합니다.
[root@ip-192-168-2-160 ~]# cat /usr/share/amazon-chrony-config/link-local-ipv4_unspecified.sources
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html
server 169.254.169.123 prefer iburst minpoll 4 maxpoll 4

[root@ip-192-168-2-160 ~]# cat /usr/share/amazon-chrony-config/amazon-pool_aws.sources
# Use Amazon Public NTP leap-second smearing time sources
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html#configure-time-sync
pool time.aws.com iburst

# aws 의 타임서버에 nsloopup을 해봅니다.
[root@ip-192-168-2-160 ~]# nslookup time.aws.com
Server:         192.168.0.2
Address:        192.168.0.2#53

Non-authoritative answer:
Name:   time.aws.com
Address: 54.90.191.9
Name:   time.aws.com
Address: 44.201.148.133
Name:   time.aws.com
Address: 3.87.127.143
Name:   time.aws.com
Address: 54.197.201.248
Name:   time.aws.com
Address: 54.81.127.33
Name:   time.aws.com
Address: 2600:1f18:4a3:6900:46e8:2cd4:84f2:c189
Name:   time.aws.com
Address: 2600:1f18:4a3:6901:e748:7e32:6d5e:3a44
Name:   time.aws.com
Address: 2600:1f18:4a3:6902:6a8a:b72:c16e:db31
Name:   time.aws.com
Address: 2600:1f18:4a3:6901:2f65:3de:4135:a6a4
Name:   time.aws.com
Address: 2600:1f18:4a3:6902:2a6f:3bec:e00b:8fb

실제 시간 동기화에 대한 내용을 살펴봅시다.

[root@ip-192-168-2-160 ~]# timedatectl status
               Local time: Tue 2026-03-17 06:48:11 UTC
           Universal time: Tue 2026-03-17 06:48:11 UTC
                 RTC time: Tue 2026-03-17 06:48:10
                Time zone: n/a (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no
[root@ip-192-168-2-160 ~]# chronyc sources -v

  .-- Source mode  '^' = server, '=' = peer, '#' = local clock.
 / .- Source state '*' = current best, '+' = combined, '-' = not combined,
| /             'x' = may be in error, '~' = too variable, '?' = unusable.
||                                                 .- xxxx [ yyyy ] +/- zzzz
||      Reachability register (octal) -.           |  xxxx = adjusted offset,
||      Log2(Polling interval) --.      |          |  yyyy = measured offset,
||                                \     |          |  zzzz = estimated error.
||                                 |    |           \
MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
^* 169.254.169.123               3   4   377     7  -6614ns[  -11us] +/-  304us
^- ec2-52-207-222-50.comput>     4  10   377    58  -2568us[-2568us] +/-   91ms
^- ec2-54-210-225-137.compu>     4  10   377   720   +482us[ +492us] +/-   89ms
^- ec2-3-86-4-106.compute-1>     4  10   377   840  +1638us[+1671us] +/-   88ms
^- ec2-3-94-91-31.compute-1>     4  10   377   681   +730us[ +738us] +/-   92ms

containerd 작동 확인하기

그렇다면 이어서 containerd가 제대로 도는지 살펴보겠습니다.

[ec2-user@ip-192-168-2-160 ~]$ pstree -a
systemd --switched-root --system --deserialize=32
  ├─chronyd -F 2
  ├─containerd
   └─10*[{containerd}]
  ├─containerd-shim -namespace k8s.io -id 94f56b06be18... (aws-node Pod)
   ├─aws-vpc-cni
   ├─aws-k8s-agent
   └─8*[{aws-k8s-agent}]
   └─3*[{aws-vpc-cni}]
   ├─controller --enable-ipv6=false --enable-network-policy=false --enable-cloudwatch-logs=false ...
   └─7*[{controller}]
   ├─pause
   └─14*[{containerd-shim}]
  ├─containerd-shim -namespace k8s.io -id 1049323a8f33... (kube-proxy Pod)
   ├─kube-proxy --v=2 --config=/var/lib/kube-proxy-config/config ...
   └─5*[{kube-proxy}]
   ├─pause
   └─12*[{containerd-shim}]
  ├─containerd-shim -namespace k8s.io -id 35edb8727e17... (coredns Pod)
   ├─coredns -conf /etc/coredns/Corefile
   └─8*[{coredns}]
   ├─pause
   └─12*[{containerd-shim}]
  └─kubelet --node-ip=192.168.2.160 --cloud-provider=external --runtime-cgroups=/runtime.slice/containerd.service --config=/
      └─11*[{kubelet}]
[root@ip-192-168-2-160 ~]# systemctl status containerd --no-pager -l
 containerd.service - containerd container runtime
     Loaded: loaded (/usr/lib/systemd/system/containerd.service; disabled; preset: disabled)
    Drop-In: /etc/systemd/system/containerd.service.d
             └─00-runtime-slice.conf
     Active: active (running) since Mon 2026-03-16 05:15:04 UTC; 1 day 2h ago
       Docs: https://containerd.io
   Main PID: 2198 (containerd)
      Tasks: 52
     Memory: 744.7M
        CPU: 16min 54.304s
     CGroup: /runtime.slice/containerd.service
             ├─2198 /usr/bin/containerd
             ├─2295 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id 94f56b06be18b892f09b876165c0091f184f22212c8f387fbd88425426113594 -address /run/containerd/containerd.sock
             ├─2702 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id 1049323a8f33b8ea94ed51af317f5ca67d24ed7636c36009d29dee369c3756ff -address /run/containerd/containerd.sock
             └─2807 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id 35edb8727e171ef5e6951166c8166c994dc5fa8fc1b6d3cd93667f64890dbb56 -address /run/containerd/containerd.sock

이어서 systemd 서비스 유닛 파일도 살펴봅시다. systemd는 PID 1 이고, 시스템의 모든 서비스를 관리하는 프로세스이니 containerd 라는 자식 프로세스를 관리하는 것이죠.

[root@ip-192-168-2-160 ~]# cat /usr/lib/systemd/system/containerd.service
# Copyright The containerd Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target dbus.service

[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/bin/containerd

Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5

# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity
LimitNOFILE=infinity

# Comment TasksMax if your systemd version does not supports it.
# Only systemd 226 and above support this version.
TasksMax=infinity
OOMScoreAdjust=-999

[Install]
WantedBy=multi-user.target

containerd의 설정파일은 여기있습니다.

[root@ip-192-168-2-160 ~]# tree /etc/containerd/
/etc/containerd/
├── base-runtime-spec.json
└── config.toml

[root@ip-192-168-2-160 ~]# cat /etc/containerd/config.toml
version = 3
root = "/var/lib/containerd"
state = "/run/containerd"

[grpc]
address = "/run/containerd/containerd.sock"

[plugins.'io.containerd.cri.v1.images']
discard_unpacked_layers = true

[plugins.'io.containerd.cri.v1.images'.pinned_images]
sandbox = "localhost/kubernetes/pause"

[plugins."io.containerd.cri.v1.images".registry]
config_path = "/etc/containerd/certs.d:/etc/docker/certs.d"

[plugins.'io.containerd.cri.v1.runtime']
enable_cdi = true

[plugins.'io.containerd.cri.v1.runtime'.containerd]
default_runtime_name = "runc"

[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
base_runtime_spec = "/etc/containerd/base-runtime-spec.json"

[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
BinaryName = "/usr/sbin/runc"
SystemdCgroup = true

[plugins.'io.containerd.cri.v1.runtime'.cni]
bin_dir = "/opt/cni/bin"
conf_dir = "/etc/cni/net.d"

아래는 런타임 스펙에 대한 내용입니다.

[root@ip-192-168-2-160 ~]# cat /etc/containerd/base-runtime-spec.json  | jq
{
  "linux": {
    "maskedPaths": [           # 컨테이너 안에서 접근 차단되는 경로 (읽으면 빈 파일로 보임)
      "/proc/acpi",            # 하드웨어 전원 관리 정보
      "/proc/asound",          # 사운드 카드 정보
      "/proc/kcore",           # 커널 메모리 덤프 — 노출 시 호스트 메모리 유출 위험
      "/proc/keys",            # 커널 키링 — 노출 시 암호화 키 유출 위험
      "/proc/latency_stats",   # 커널 레이턴시 통계
      "/proc/sched_debug",     # 스케줄러 디버그 정보 — 호스트의 전체 프로세스 목록 노출 위험
      "/proc/scsi",            # SCSI 디바이스 정보
      "/proc/timer_list",      # 커널 타이머 목록 — 호스트 커널 내부 상태 노출
      "/proc/timer_stats",     # 타이머 통계
      "/sys/firmware"          # 펌웨어(BIOS/UEFI) 정보 — 호스트 하드웨어 정보 노출 위험
    ],
    "namespaces": [            # 컨테이너를 호스트로부터 격리하는 리눅스 네임스페이스
      { "type": "ipc" },       # 프로세스 간 통신(공유메모리, 세마포어) 격리
      { "type": "mount" },     # 파일시스템 마운트 격리 — 컨테이너마다 독립된 마운트 테이블
      { "type": "network" },   # 네트워크 스택 격리 — Pod 단위로 IP, 포트, 라우팅 테이블 분리
      { "type": "pid" },       # PID 격리 — 컨테이너 안에서 호스트의 다른 프로세스가 안 보임
      { "type": "uts" }        # hostname 격리 — 컨테이너마다 독립된 hostname
                               # ※ user 네임스페이스 없음 → UID/GID 매핑 격리 안 함 (EKS 기본값)
    ],
    "readonlyPaths": [         # 컨테이너 안에서 읽기만 가능한 경로 (수정 불가)
      "/proc/bus",             # 버스(USB, PCI) 정보
      "/proc/fs",              # 파일시스템 통계
      "/proc/irq",             # 인터럽트 할당 정보 — 수정되면 호스트 안정성 위협
      "/proc/sys",             # 커널 파라미터(sysctl) — 수정되면 호스트 전체에 영향
      "/proc/sysrq-trigger"    # 커널 긴급 명령 트리거 — 수정되면 호스트 리부팅 등 가능
    ],
    "resources": {
      "devices": [
        { "access": "rwm", "allow": false }  # 기본적으로 모든 디바이스 접근 차단 (r:읽기, w:쓰기, m:생성)
      ]
    }
  },
  "process": {
    "capabilities": {
      "bounding": [                    # 컨테이너에 허용되는 리눅스 capability (전체 root 권한이 아닌 필요한 것만)
        "CAP_AUDIT_WRITE",             # audit 로그 쓰기
        "CAP_CHOWN",                   # 파일 소유자 변경
        "CAP_DAC_OVERRIDE",            # 파일 권한 검사 우회 (읽기/쓰기/실행)
        "CAP_FOWNER",                  # 파일 소유자 관련 권한 검사 우회
        "CAP_FSETID",                  # setuid/setgid 비트 유지
        "CAP_KILL",                    # 다른 프로세스에 시그널 전송
        "CAP_MKNOD",                   # 디바이스 파일 생성
        "CAP_NET_BIND_SERVICE",        # 1024 이하 포트 바인딩 허용
        "CAP_NET_RAW",                 # raw socket 사용 허용 (ping 등)
        "CAP_SETFCAP",                 # 파일 capability 설정
        "CAP_SETGID",                  # GID 변경
        "CAP_SETPCAP",                 # 다른 프로세스에 capability 전달
        "CAP_SETUID",                  # UID 변경
        "CAP_SYS_CHROOT"              # chroot 사용
                                       # ※ CAP_SYS_ADMIN 없음 → mount, namespace 조작 등 불가
                                       # ※ CAP_SYS_PTRACE 없음 → 다른 프로세스 디버깅 불가
      ]
    },
    "cwd": "/",                        # 컨테이너 프로세스의 기본 working directory
    "noNewPrivileges": true,           # 컨테이너 프로세스가 추가 privilege 획득 금지
                                       # setuid binary 실행해도 root 권한 상승 불가
    "rlimits": [
      {
        "type": "RLIMIT_NOFILE",       # 컨테이너 프로세스 파일 디스크립터 제한
        "soft": 65536,                 # 기본 사용 가능 → 65536
        "hard": 1048576                # 최대 상승 가능 → 1048576
      }
    ],
    "user": {
      "uid": 0,                        # 컨테이너 프로세스 기본 root 실행
      "gid": 0                         # 단, noNewPrivileges + capability 제한으로 호스트 root와 권한 다름
    }
  }
}

아래는 실제 containerd의 유닉스 도메인 소켓 통신상태를 살펴봅니다.

# kubelet이 CRI요청을 보내면, 이 소켓에 연결합니다.
[root@ip-192-168-2-160 ~]# containerd config dump | grep -n containerd.sock
11:  address = '/run/containerd/containerd.sock'

# 소켓파일에 대해 소유와 타입을 살펴봅니다.
[root@ip-192-168-2-160 ~]# ls -l /run/containerd/containerd.sock
srw-rw----. 1 root root 0 Mar 16 05:15 /run/containerd/containerd.sock

# ss -xl: LISTEN 상태인 유닉스 도메인 소켓을 확인합니다.
#   containerd.sock          → kubelet이 CRI 요청을 보내는 gRPC 소켓 (kubelet → containerd)
#   containerd.sock.ttrpc    → containerd가 listen, shim이 connect해서 상태를 보고하는 경량 소켓 (shim → containerd)
#   /run/containerd/s/<해시> → 각 containerd-shim이 listen하는 소켓, containerd가 여기로 명령을 보냄 (containerd → shim)
#
#   여기서 shim 소켓 3개는 pstree에서 확인한 Pod 3개(aws-node, kube-proxy, coredns)와 대응됩니다.
[root@ip-192-168-2-160 ~]# ss -xl | grep containerd
u_str LISTEN 0      4096   /run/containerd/s/56931e7f59f9fa33ed767462752e5b7eeda0942c640bdd877d286114a6b804d8 7075               * 0
u_str LISTEN 0      4096                                                /run/containerd/containerd.sock.ttrpc 4936               * 0
u_str LISTEN 0      4096                                                      /run/containerd/containerd.sock 5559               * 0
u_str LISTEN 0      4096   /run/containerd/s/5acdf4c83aea3c378be8fc8aa32a0c886ca0ab8de618ad4b167ef65f95d19591 6166               * 0
u_str LISTEN 0      4096   /run/containerd/s/4dbbec28e58840996f813b4d25ea7705bf25167c3f1766459744206766f64bed 7602               * 0

# ss -xnp: ESTAB 상태인 유닉스 도메인 소켓 연결을 확인합니다.
#   containerd(pid=2198)와 containerd-shim(pid=2295, 2702, 2807) 사이에
#   실제로 소켓 연결이 맺어져 있음을 확인할 수 있습니다.
#   위의 LISTEN 소켓이 실제로 사용되고 있다는 뜻입니다.
[root@ip-192-168-2-160 ~]# ss -xnp | grep containerd
u_str ESTAB 0      0                                                                                       * 7355               * 6759    users:(("containerd",pid=2198,fd=48))
u_str ESTAB 0      0      /run/containerd/s/56931e7f59f9fa33ed767462752e5b7eeda0942c640bdd877d286114a6b804d8 7081               * 7830    users:(("containerd-shim",pid=2807,fd=10))
u_str ESTAB 0      0                                                                                       * 5820               * 5821    users:(("containerd-shim",pid=2295,fd=3))
u_str ESTAB 0      0                                                                                       * 6170               * 6171    users:(("containerd",pid=2198,fd=18))
u_str ESTAB 0      0      /run/containerd/s/5acdf4c83aea3c378be8fc8aa32a0c886ca0ab8de618ad4b167ef65f95d19591 6453               * 6044    users:(("containerd-shim",pid=2295,fd=12))
u_str ESTAB 0      0                                                   /run/containerd/containerd.sock.ttrpc 6991               * 7681    users:(("containerd",pid=2198,fd=26))
u_str ESTAB 0      0      /run/containerd/s/4dbbec28e58840996f813b4d25ea7705bf25167c3f1766459744206766f64bed 6967               * 7605    users:(("containerd-shim",pid=2702,fd=10))
u_str ESTAB 0      0                                                                                       * 8039               * 8737    users:(("containerd",pid=2198,fd=72))
u_str ESTAB 0      0                                                   /run/containerd/containerd.sock.ttrpc 5821               * 5820    users:(("containerd",pid=2198,fd=19))
u_str ESTAB 0      0                                                                                       * 7830               * 7081    users:(("containerd",pid=2198,fd=58))
u_str ESTAB 0      0                                                                                       * 7605               * 6967    users:(("containerd",pid=2198,fd=23))
u_str ESTAB 0      0      /run/containerd/s/56931e7f59f9fa33ed767462752e5b7eeda0942c640bdd877d286114a6b804d8 8737               * 8039    users:(("containerd-shim",pid=2807,fd=12))
u_str ESTAB 0      0                                                                                       * 6044               * 6453    users:(("containerd",pid=2198,fd=35))
u_str ESTAB 0      0                                                                                       * 7681               * 6991    users:(("containerd-shim",pid=2702,fd=11))
u_str ESTAB 0      0      /run/containerd/s/5acdf4c83aea3c378be8fc8aa32a0c886ca0ab8de618ad4b167ef65f95d19591 6759               * 7355    users:(("containerd-shim",pid=2295,fd=21))
u_str ESTAB 0      0      /run/containerd/s/5acdf4c83aea3c378be8fc8aa32a0c886ca0ab8de618ad4b167ef65f95d19591 6171               * 6170    users:(("containerd-shim",pid=2295,fd=10))
u_str ESTAB 0      0                                                                                       * 7902               * 7903    users:(("containerd-shim",pid=2807,fd=11))
u_str ESTAB 0      0                                                                                       * 7948               * 7949    users:(("containerd",pid=2198,fd=66))
u_str ESTAB 0      0                                                         /run/containerd/containerd.sock 5582               * 5581    users:(("containerd",pid=2198,fd=14))
u_str ESTAB 0      0                                                   /run/containerd/containerd.sock.ttrpc 7903               * 7902    users:(("containerd",pid=2198,fd=53))
u_str ESTAB 0      0                                                         /run/containerd/containerd.sock 4953               * 5580    users:(("containerd",pid=2198,fd=13))
u_str ESTAB 0      0      /run/containerd/s/4dbbec28e58840996f813b4d25ea7705bf25167c3f1766459744206766f64bed 7949               * 7948    users:(("containerd-shim",pid=2702,fd=12))
u_str ESTAB 0      0                                                                                       * 5528               * 4895    users:(("containerd",pid=2198,fd=2),("containerd",pid=2198,fd=1))

kubelet 작동 확인하기

kubelet이 떠있나 살펴봅시다.

[root@ip-192-168-2-160 ~]# ps afxuwww | grep kubelet
root      533869  0.0  0.0 222336  2240 pts/1    S+   09:29   0:00                              \_ grep --color=auto kubelet
root        2235  0.9  2.1 2124144 83164 ?       Ssl  Mar16  16:25 /usr/bin/kubelet --node-ip=192.168.2.160 --cloud-provider=external --runtime-cgroups=/runtime.slice/containerd.service --config=/etc/kubernetes/kubelet/config.json --config-dir=/etc/kubernetes/kubelet/config.json.d --kubeconfig=/var/lib/kubelet/kubeconfig --image-credential-provider-config=/etc/eks/image-credential-provider/config.json --hostname-override=ip-192-168-2-160.ap-northeast-2.compute.internal --image-credential-provider-bin-dir=/etc/eks/image-credential-provider --node-labels=eks.amazonaws.com/sourceLaunchTemplateVersion=1,eks.amazonaws.com/nodegroup-image=ami-0c19bc6c6295a611b,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=myeks-node-group,eks.amazonaws.com/sourceLaunchTemplateId=lt-030f587db6333ef76
...

[root@ip-192-168-2-160 ~]# systemctl status kubelet --no-pager
 kubelet.service - Kubernetes Kubelet
     Loaded: loaded (/etc/systemd/system/kubelet.service; disabled; preset: disabled)
     Active: active (running) since Mon 2026-03-16 05:15:05 UTC; 1 day 4h ago
       Docs: https://github.com/kubernetes/kubernetes
   Main PID: 2235 (kubelet)
      Tasks: 12 (limit: 4516)
     Memory: 82.8M
        CPU: 18min 22.339s
     CGroup: /runtime.slice/kubelet.service
             └─2235 /usr/bin/kubelet --node-ip=192.168.2.160 --cloud-provider=external (이하생략)

그러면 쿠버네티스 구성에 필요한 root ca 구성도 살펴봅시다.

[root@ip-192-168-2-160 ~]# cat /etc/kubernetes/pki/ca.crt | openssl x509 -text -noout
Certificate:
    Data:
Issuer: CN=kubernetes
        Validity
            Not Before: Mar 16 05:05:48 2026 GMT
            Not After : Mar 13 05:10:48 2036 GMT
        Subject: CN=kubernetes
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    (생략)
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment, Certificate Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier:
                <REDACTED>
            X509v3 Subject Alternative Name:
                DNS:kubernetes
    Signature Algorithm: sha256WithRSAEncryption
    
    ...

kubelet 설정과 EKS의 kubelet 추가설정도 살펴보겠습니다.

[root@ip-192-168-2-160 ~]# cat /etc/kubernetes/kubelet/config.json | jq
{
  "address": "0.0.0.0",
  "authentication": {
    "x509": {
      "clientCAFile": "/etc/kubernetes/pki/ca.crt"
    },
  ...
}

# 이 노드에는 17개의 파드만 구동될 수 있습니다.
#    VPC CNI는 파드마다 VPC IP를 할당하는데,
#    인스턴스 타입별로 maxPods가 정해집니다.
#    지금 구동중인 t3.medium 에서는 그 제한이 이정도입니다.
#    Pod가 진짜 VPC IP를 받아서 네트워크 성능과 호환성은 좋지만, 노드당 Pod 수에 하드 리밋이 생기지요.
[root@ip-192-168-2-160 ~]# cat /etc/kubernetes/kubelet/config.json.d/40-nodeadm.conf
{
    "apiVersion": "kubelet.config.k8s.io/v1beta1",
    "clusterDNS": [
        "10.100.0.10"
    ],
    "kind": "KubeletConfiguration",
    "maxPods": 17
}

kubelet 디렉터리는 이렇습니다.

[root@ip-192-168-2-160 ~]# tree /var/lib/kubelet -L 2
/var/lib/kubelet
├── actuated_pods_state
├── allocated_pods_state
├── checkpoints
├── cpu_manager_state
├── device-plugins
│   └── kubelet.sock
├── dra_manager_state
├── kubeconfig
├── memory_manager_state
├── pki
│   ├── kubelet-server-2026-03-16-05-15-24.pem
│   ├── kubelet-server-2026-03-16-05-15-40.pem
│   └── kubelet-server-current.pem -> /var/lib/kubelet/pki/kubelet-server-2026-03-16-05-15-40.pem
├── plugins
├── plugins_registry
├── pod-resources
│   └── kubelet.sock
└── pods
    ├── 9b98f2ea-407b-4936-b656-68ccb3cb30c3
    ├── 9d7b65ea-4d88-4a0a-8551-7f4a261b1422
    └── c96e3659-f1ce-4b84-a9a7-8b57e48c3dab

kubeconfig 값은 이렇습니다.

[root@ip-192-168-2-160 ~]# cat /var/lib/kubelet/kubeconfig
---
apiVersion: v1
kind: Config
clusters:
  - name: kubernetes
    cluster:
      certificate-authority: /etc/kubernetes/pki/ca.crt
      server: https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com
current-context: kubelet
contexts:
  - name: kubelet
    context:
      cluster: kubernetes
      user: kubelet
users:
  - name: kubelet
    user:
      exec:
        apiVersion: client.authentication.k8s.io/v1beta1
        command: aws
        args:
          - "eks"
          - "get-token"
          - "--cluster-name"
          - "myeks"
          - "--region"
          - "ap-northeast-2"

그리고 EKS API서버가 kubelet에 HTTPS로 요청을 보낼 때가 있습니다. 이때, SAN(Subject Alternative Name) 에 IP 주소도 같이 들어가있는데요. 이 부분도 함께 살펴보시죠.

[root@ip-192-168-2-160 ~]# curl ipinfo.io/ip ; echo
16.184.46.4

[root@ip-192-168-2-160 ~]# openssl x509 -in /var/lib/kubelet/pki/kubelet-server-current.pem -noout -text | grep -A 1 "Subject Alternative Name"
            X509v3 Subject Alternative Name:
                DNS:ec2-16-184-46-4.ap-northeast-2.compute.amazonaws.com, DNS:ip-192-168-2-160.ap-northeast-2.compute.internal, IP Address:16.184.46.4, IP Address:192.168.2.160

CNI, 네트워크, 스토리지, cgroup 정보 확인

CNI 확인은 아래와 같이 진행할 수 있습니다. 테라폼 코드에서 vpc-cni 를 애드온으로 두었는데, 이게 정상작동하는지 살펴본다고 할 수 있겠네요.

# CNI 관련 내용 확인
[root@ip-192-168-2-160 ~]# tree -pug /opt/cni/
/opt/cni/
└── [drwxr-xr-x root     root    ]  bin
    ├── [-rw-rw-r-- root     root    ]  LICENSE
    ├── [-rwxr-xr-x root     root    ]  aws-cni
    ├── [-rwxr-xr-x root     root    ]  aws-cni-support.sh
    ├── [-rw-r--r-- root     root    ]  bandwidth
    ├── [-rwxr-xr-x root     root    ]  bridge
    ├── [-rwxr-xr-x root     root    ]  dhcp
    ├── [-rwxr-xr-x root     root    ]  dummy
    ├── [-rwxr-xr-x root     root    ]  egress-cni
    ├── [-rwxr-xr-x root     root    ]  firewall
    ├── [-rwxr-xr-x root     root    ]  host-device
    ├── [-rwxr-xr-x root     root    ]  host-local
    ├── [-rwxr-xr-x root     root    ]  ipvlan
    ├── [-rwxr-xr-x root     root    ]  loopback
    ├── [-rwxr-xr-x root     root    ]  macvlan
    ├── [-rwxr-xr-x root     root    ]  portmap
    ├── [-rwxr-xr-x root     root    ]  ptp
    ├── [-rwxr-xr-x root     root    ]  sbr
    ├── [-rwxr-xr-x root     root    ]  static
    ├── [-rwxr-xr-x root     root    ]  tap
    ├── [-rwxr-xr-x root     root    ]  tuning
    ├── [-rwxr-xr-x root     root    ]  vlan
    └── [-rwxr-xr-x root     root    ]  vrf

1 directory, 22 files

# CNI 바이너리를 확인합니다.
[root@ip-192-168-2-160 ~]# /opt/cni/bin/aws-cni -h
AWS CNI
CNI protocol versions supported: 0.1.0, 0.2.0, 0.3.0, 0.3.1, 0.4.0, 1.0.0, 1.1.0

# VPC CNI 플러그인 설정은 여기서 확인할 수 있습니다.
[root@ip-192-168-2-160 ~]# tree /etc/cni
/etc/cni
└── net.d
    └── 10-aws.conflist

# 실제 내용은 아래와 같습니다.
[root@ip-192-168-2-160 ~]# cat /etc/cni/net.d/10-aws.conflist | jq
{
  "cniVersion": "0.4.0",
  "name": "aws-cni",
  "disableCheck": true,
  "plugins": [
    {
      "name": "aws-cni"

...

다음은 네트워크 구성을 확인해봅시다.

[root@ip-192-168-2-160 ~]# ip route
default via 192.168.2.1 dev ens5 proto dhcp src 192.168.2.160 metric 512
192.168.0.2 via 192.168.2.1 dev ens5 proto dhcp src 192.168.2.160 metric 512
192.168.2.0/24 dev ens5 proto kernel scope link src 192.168.2.160 metric 512
192.168.2.1 dev ens5 proto dhcp scope link src 192.168.2.160 metric 512
192.168.2.58 dev eni9f7e1aecc5e scope link
[root@ip-192-168-2-160 ~]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    link/ether 06:4d:03:18:6e:61 brd ff:ff:ff:ff:ff:ff
    altname enp0s5
    inet 192.168.2.160/24 metric 512 brd 192.168.2.255 scope global dynamic ens5
       valid_lft 3406sec preferred_lft 3406sec
    inet6 fe80::44d:3ff:fe18:6e61/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
3: eni9f7e1aecc5e@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP group default
    link/ether 5e:d5:54:bb:60:79 brd ff:ff:ff:ff:ff:ff link-netns cni-91e845b9-e928-ab06-6469-0c5dade6a8c8
    inet6 fe80::5cd5:54ff:febb:6079/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
4: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    link/ether 06:e4:6f:a5:0a:c3 brd ff:ff:ff:ff:ff:ff
    altname enp0s6
    inet 192.168.2.186/24 brd 192.168.2.255 scope global ens6
       valid_lft forever preferred_lft forever
    inet6 fe80::4e4:6fff:fea5:ac3/64 scope link proto kernel_ll
       valid_lft forever preferred_lft forever
[root@ip-192-168-2-160 ~]# lsns -t net
        NS TYPE NPROCS   PID USER     NETNSID NSFS                                                COMMAND
4026531840 net     114     1 root  unassigned                                                     /usr/lib/systemd/systemd --switched-root --system --deseria
4026532210 net       2  2832 65535          0 /run/netns/cni-91e845b9-e928-ab06-6469-0c5dade6a8c8 /paus

iptables 규칙은 아래와 같이 확인하실 수 있습니다. CNI를 이용해 잘 생성된 것을 확인하실 수 있습니다.

# iptables 규칙 확인
iptables -t nat -S
iptables -t filter -S
iptables -t mangle -S

이어서 스토리지 정보 입니다. 디스크 하나만 잡아두었으니, 잘 잡혀있는지. 그리고 컨테이너 rootfs와 findmnt 결과를 살펴보겠습니다.

[root@ip-192-168-2-160 ~]# lsblk
NAME          MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
nvme0n1       259:0    0  20G  0 disk
├─nvme0n1p1   259:1    0  20G  0 part /
├─nvme0n1p127 259:2    0   1M  0 part
└─nvme0n1p128 259:3    0  10M  0 part /boot/efi

[root@ip-192-168-2-160 ~]# df -hT
# 호스트 루트 파일시스템: 20G 중 3.5G 사용 (18%)
/dev/nvme0n1p1   xfs        20G  3.5G   17G  18% /

# 컨테이너 rootfs: overlayfs로 마운트, 호스트 디스크를 공유
overlay          overlay    20G  3.5G   17G  18% /run/containerd/.../94f56b06be18.../rootfs  # aws-node sandbox
overlay          overlay    20G  3.5G   17G  18% /run/containerd/.../3addfa4c8a0f.../rootfs  # aws-node
overlay          overlay    20G  3.5G   17G  18% /run/containerd/.../0e262d76e07e.../rootfs  # aws-eks-nodeagent

# Pod별 서비스어카운트 토큰이 projected volume으로 마운트됨
tmpfs            tmpfs     3.3G   12K  3.3G   1% /var/lib/kubelet/pods/.../kube-api-access-s7p7s

# sandbox별 공유메모리 (64M)
shm              tmpfs      64M     0   64M   0% /run/containerd/.../sandboxes/94f56b06be18.../shm


[root@ip-192-168-2-160 ~]# findmnt
# cgroup v2 확인 (첫째 글에서 다룬 전제조건)
/sys/fs/cgroup    cgroup2    cgroup2    rw,nosuid,nodev,noexec,relatime,seclabel

# Pod 네트워크 네임스페이스가 파일로 마운트되어 있음
/run/netns/cni-91e845b9-...    nsfs    nsfs    rw

cgroup 버전은 아래와같이 확인하실 수 있습니다.

[root@ip-192-168-2-160 ~]# stat -fc %T /sys/fs/cgroup/
cgroup2fs
[root@ip-192-168-2-160 ~]# findmnt | grep -i cgroup
 ├─/sys/fs/cgroup  cgroup2    cgroup2    rw,nosuid,nodev,noexec,relatime,seclabel

  1. 다양한 게시글들을 읽어보았습니다. 이해에 악분님의 게시글이 도움되었습니다. https://malwareanalysis.tistory.com/724 ↩︎

  2. 이해에 Interlude 님의 게시글이 도움되었습니다. 파일 시스템 레이어를 통해 읽기전용/읽기쓰기 영역으로 나누어 효율적으로 관리하기 위함이죠. https://interlude-3.tistory.com/47 ↩︎

  3. 컨테이너 이미지를 디스크에 풀어놓은 목록을 보는 것입니다. 컨테이너를 구동하면 containerd가 read only인 committed snapshot 위에, r/w인 active snapshot이 있는데, 이걸 보는 커맨드들입니다. ↩︎