Amazon EKS의 네트워킹에서 Kubernetes의 네트워크와 인그레스 트래픽을 AWS에서 어떻게 구현했는지 살펴보겠습니다.

지난 포스트에서는 EKS Networking Part 1으로 VPC CNI 개요와 노드 환경 구성을 살펴보고, 파드 통신 과정을 살펴봤습니다. 이번 포스트는 EKS Networking Part2로 LoadBalancer 유형의 서비스의 구성과 Ingress에 대해서 확인해 보겠습니다.

그리고 이들의 구현이 AKS(Azure Kubernetes Service)에서는 어떻게 다른지 설명을 해보겠습니다.

 

 

목차

  1. 실습 환경
  2. Kubernetes의 네트워크 개요
  3. AWS의 LoadBalancer 유형
  4. EKS의 LoadBalancer 구현
  5. EKS의 LoadBalancer 실습
  6. AKS의 LoadBalancer 구현
  7. EKS의 Ingress 구현 및 실습
  8. AKS의 Ingress 구현
  9. 리소스 정리

 

 

1 . 실습 환경

실습 환경

EKS Networking Part1에서 사용한 EKS를 그대로 사용하겠습니다. 아래와 같은 구성입니다.

 

혹시나 동일한 테스트를 진행하고자 한다면, 지난 포스트 EKS Networking Part1(https://a-person.tistory.com/30)의 3. 실습 환경 배포 절을 참고 부탁드립니다.

 

 

클러스터 관련 정보는 아래와 같습니다. EKS 1.31.5 버전입니다.

$ kubectl cluster-info
Kubernetes control plane is running at https://04559E327E31C75E5F6A835B3E0B6AED.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://04559E327E31C75E5F6A835B3E0B6AED.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'.

$ eksctl get cluster
NAME    REGION          EKSCTL CREATED
myeks   ap-northeast-2  True

$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-228.ap-northeast-2.compute.internal   Ready    <none>   116m   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2a
ip-192-168-2-91.ap-northeast-2.compute.internal    Ready    <none>   116m   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2b
ip-192-168-3-155.ap-northeast-2.compute.internal   Ready    <none>   116m   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2c

 

 

2. Kubernetes의 네트워크 개요

Kubernetes의 네트워크에 관련된 주요 컴포넌트와 오브젝트로 CNI, Service, Ingress 등이 있는데, 이들은 각각 어떤 역할을 하는 걸까요? 이러한 요소들은 Kubernetes의 네트워크를 구성해주기도 하고, 한편으로는 워크로드의 서비스를 노출해주는 역할을 합니다.

 

먼저 CNI는 컨테이너 네트워킹을 가능하도록 구성해주는 역할을 합니다. 이를 통해 파드는 통신 가능한 상태가 됩니다.

파드에 실행된 워크로드를 사용자가 접근하기 위해서는 Service가 필요합니다. Service에는 Cluster IP, Nodeport, LoadBalancer 유형이 있습니다. 간단히 클러스터 내의 다른 Micro 서비스에 대한 호출은 Cluster IP를 사용하고, 외부 통신은 Nodeport나 LoadBalancer 유형의 Service로 가능합니다.

 

다만 LoadBalancer 유형의 서비스는 클러스터 외부에 IP를 노출해야 하기 때문에 다른 구현체(컨트롤러)를 통해서 Loadbalancing을 구현하고, 엔드포인트 IP를 클러스터의 LoadBalancer 유형의 서비스에 노출 해야합니다. 그래서 보통은 CSP에서 Cloud Controller를 통해 구현을 하거나, On-Premise에서는 LoxiLB 와 같은 솔루션을 사용할 수 있습니다.

또한 노드 수준에서는 iptables, IPVS, eBPF 로 Service 와 접점(Endpoint)를 구성해야 하는데, kube-proxy 를 통해 iptables를 사용하는 방법이 일반적입니다.

이때 Service는 L4 수준의 통신을 가능하도록 하므로, L7 통신을 가능하게 하기 위해 Ingress 리소스를 사용할 수 있습니다.

 

본 포스트에서는 기본적인 Cluster IP, Nodeport에 대한 설명은 제외하고, EKS에서 LoadBalancer 유형의 서비스와 Ingress 서비스를 어떤 식으로 구현했는지를 살펴보겠습니다.

 

 

3. AWS의 LoadBalancer 유형

EKS의 Loadbalancer와 Ingress는 각 다른 유형의 Loadbalancer 로 구현되어 있습니다. EKS 실습에 앞서 AWS의 Loadbalancer 서비스 유형을 간단히 살펴보겠습니다.

참고: https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html

 

Elastic Load Balancing은 다음 유형의 로드 밸런서를 지원합니다.

  • Application Load Balancers (ALB)
  • Network Load Balancer (NLB)
  • Gateway Load Balancer (GWLB)
  • Classic Load Balancer(CLB)

CLB는 가장 오래된 ELB의 형태로, L4/L7 계층의 로드밸런싱이 가능하지만, 하나의 주소에 하나의 대상 그룹 밖에 지정을 할 수 없는 것과 같은 몇가지 제한사항이 있습니다. CLB는 Legacy로 분류되어 많이 사용하지 않습니다.

 

ALB는 L7계층의 로드밸런싱이 가능합니다. L7의 로드밸런싱이 가능하기 때문에 HTTP 헤더 정보를 바탕으로 라우팅을 하거나 Path Based 라우팅도 가능합니다.

 

NLB는 L4 계층이 로드밸런서 입니다. 고성능 부하 분산을 하기에 적합하며, EIP로 공인 IP를 고정할 수 있는 장점이 있습니다. 노출된 IP를 통해 DNS를 제공해줘 접근도 가능합니다.

 

GWLB는 L3 계층에서 작동합니다. GWLB를 통해서 방화벽, 침입 탐지, 패킷 검사와 같은 가상 어플라인언스를 배포 확장하는데 활요될 수 있습니다. GWLB는 앞서 설명한 LoadBalancer와는 역할이 다소 다른 것 같습니다.

 

EKS에서는 NLB 혹은 ALB를 사용하여 워크로드의 인바운드 서비스를 가능하도록 합니다.

 

 

4. EKS의 LoadBalancer 구현

EKS에서 LoadBalancer 유형의 서비스가 생성되면 AWS의 Load Balancer가 프로비저닝 됩니다.

어떤 방식으로 구현되었지를 살펴보기 위해서 아래 문서를 바탕으로 정리했습니다.

https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

EKS에서는 로드밸런서를 프로비저닝을 하는 두 가지 방법이 있습니다.

  • AWS Cloud Provider Load balancer Controller (레거시)
  • AWS Load Balancer Controller (권장)

기본적으로 Kubernetes의 LoadBalancer 유형의 서비스는 kube-controller-manager 혹은 cloud-controller-manager(in-tree controller)의 Cloud Provider 컴포넌트에 내장된 Service Controller 의해서 조정됩니다.

이때 AWS Cloud Provider Load balancer Controller는 레거시로, 현재는 critical bug fix만 가능합니다. AWS Cloud Provider Load balancer Controller에서는 CLB가 기본적으로 생성되고, 알맞은 annotation을 통해 NLB를 생성할 수 있습니다.

 

AWS 에서는 확장된 기능을 제공하기 위해서 AWS Load Balancer Controller를 EKS에 설치하는 방식을 제공하며, 이 경우 LoadBalancerClass에서 service.beta.kubernetes.io/aws-load-balancer-type Annotation으로 Load Balancer Controller로 명시적으로 오프로드를 해야합니다.

 

Note: 확인해보면 lodbalancerClass라는 리소스는 없는데, 실제 어떤 의미인지 정확하지 않습니다. 이후 실습에서 사용하는 LoadBalancer 유형의 서비스에서 loadBalancerClass: service.k8s.aws/nlb 로 지정하기는 합니다.

 

이제 Load Balancer 대상 유형에 따른 차이를 살펴보겠습니다.

EKS의 LoadBalancer 유형의 서비스에 service.beta.kubernetes.io/aws-load-balancer-nlb-target-type Annotation을 통해 대상 유형을 instance 혹은 ip 유형을 지정할 수 있습니다.

 

먼저 instance 로 지정한 경우 아래와 같은 구성으로 연결됩니다.

출처: https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

이 구성에서는 NodePort 를 통해서 대상이 등록되는 것을 알 수 있습니다. 이로 인해서 패킷은 노드로 전달되고 이후 iptables를 따라서 파드로 패킷이 전달되는 추가 hop이 발생합니다.

 

이 때문에 대상 유형을 ip로 지정하는 것이 권장되며, 이때 아래와 같이 구성됩니다.

 

출처: https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

이 방식은 Load Balancer의 대상이 바로 파드 IP로 등록됨에 따라, 네트워크 경로가 단순화 되고 iptables의 오버헤드가 제거되는 장점을 가지게 됩니다.

 

 

5. EKS의 LoadBalancer 실습

먼저 아무런 설정이 없이 LoadBalancer 서비스를 생성해 보겠습니다.

# 터미널1 (모니터링)
watch -d 'kubectl get pod,svc'

# 수퍼마리오 디플로이먼트 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mario
  labels:
    app: mario
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mario
  template:
    metadata:
      labels:
        app: mario
    spec:
      containers:
      - name: mario
        image: pengbai/docker-supermario
---
apiVersion: v1
kind: Service
metadata:
   name: mario
spec:
  selector:
    app: mario
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  type: LoadBalancer
EOF


# 배포 확인 : CLB 배포 확인
kubectl get deploy,svc,ep mario

# 마리오 게임 접속 : CLB 주소로 웹 접속
kubectl get svc mario -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Maria URL = http://"$1 }'

 

배포가 되고 서비스에 EXTERNAL-IP가 할당되었습니다.

NAME                         READY   STATUS    RESTARTS   AGE
pod/mario-6d8c76fd8d-kqmft   1/1     Running   0          22s

NAME                 TYPE           CLUSTER-IP     EXTERNAL-IP                                                                    PORT(S)        AGE
service/kubernetes   ClusterIP      10.100.0.1     <none>                                                                         443/TCP        50m
service/mario        LoadBalancer   10.100.40.60   ac12f1c2b92b84516b339ffeea5afd12-1085404426.ap-northeast-2.elb.amazonaws.com   80:31207/TCP   22s

 

이 상황에서 웹 콘솔에서 배포된 리소스를 확인해보겠습니다.

 

Service 오브젝트에 아무런 annotation이 없는 상황으로 CLB가 배포된 것을 확인할 수 있습니다.

 

 

AWS Load Balancer Controller 배포

이제 AWS Load Balancer Controller를 배포해 보겠습니다. Helm(https://helm.sh/docs/intro/install/)을 사용합니다.

주요 컴포넌트가 addon이 아닌 형태로 제공되면 이후에 업그레이드나 관리에 어려움이 있을텐데 이 컴포넌트는 왜 addon이 아닐지 의문이 듭니다. (Helm은 Life Cycle Management를 할 수 없음)

# 설치 전 CRD 확인
$ kubectl get crd
NAME                                         CREATED AT
cninodes.vpcresources.k8s.aws                2025-02-15T14:39:16Z
eniconfigs.crd.k8s.amazonaws.com             2025-02-15T14:41:52Z
policyendpoints.networking.k8s.aws           2025-02-15T14:39:16Z
securitygrouppolicies.vpcresources.k8s.aws   2025-02-15T14:39:16Z

$ kubectl get ingressclass
No resources found

# Helm Chart 설치
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

 

배포가 완료되면 2개의 CRD와 ingress class가 생성된 것을 확인할 수 있습니다.

## 설치 확인
$ kubectl get crd
NAME                                         CREATED AT
cninodes.vpcresources.k8s.aws                2025-02-15T14:39:16Z
eniconfigs.crd.k8s.amazonaws.com             2025-02-15T14:41:52Z
ingressclassparams.elbv2.k8s.aws             2025-02-15T15:35:37Z
policyendpoints.networking.k8s.aws           2025-02-15T14:39:16Z
securitygrouppolicies.vpcresources.k8s.aws   2025-02-15T14:39:16Z
targetgroupbindings.elbv2.k8s.aws            2025-02-15T15:35:37Z

$ kubectl get ingressclass
NAME   CONTROLLER            PARAMETERS   AGE
alb    ingress.k8s.aws/alb   <none>       104s

# 생성된 CRD
$ kubectl explain ingressclassparams.elbv2.k8s.aws
GROUP:      elbv2.k8s.aws
KIND:       IngressClassParams
VERSION:    v1beta1

DESCRIPTION:
    IngressClassParams is the Schema for the IngressClassParams API

FIELDS:
  apiVersion    <string>
    APIVersion defines the versioned schema of this representation of an object.
    Servers should convert recognized schemas to the latest internal value, and
    may reject unrecognized values. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

  kind  <string>
    Kind is a string value representing the REST resource this object
    represents. Servers may infer this from the endpoint the client submits
    requests to. Cannot be updated. In CamelCase. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

  metadata      <ObjectMeta>
    Standard object's metadata. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

  spec  <Object>
    IngressClassParamsSpec defines the desired state of IngressClassParams


$ kubectl explain targetgroupbindings.elbv2.k8s.aws
GROUP:      elbv2.k8s.aws
KIND:       TargetGroupBinding
VERSION:    v1beta1

DESCRIPTION:
    TargetGroupBinding is the Schema for the TargetGroupBinding API

FIELDS:
  apiVersion    <string>
    APIVersion defines the versioned schema of this representation of an object.
    Servers should convert recognized schemas to the latest internal value, and
    may reject unrecognized values. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

  kind  <string>
    Kind is a string value representing the REST resource this object
    represents. Servers may infer this from the endpoint the client submits
    requests to. Cannot be updated. In CamelCase. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

  metadata      <ObjectMeta>
    Standard object's metadata. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

  spec  <Object>
    TargetGroupBindingSpec defines the desired state of TargetGroupBinding

  status        <Object>
    TargetGroupBindingStatus defines the observed state of TargetGroupBinding

 

실제 생성된 디플로이먼트도 확인해봅니다.

$ kubectl get deployment -n kube-system aws-load-balancer-controller
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
aws-load-balancer-controller   2/2     2            2           5m59s
$ kubectl describe deploy -n kube-system aws-load-balancer-controller
Name:                   aws-load-balancer-controller
Namespace:              kube-system
CreationTimestamp:      Sun, 16 Feb 2025 00:35:40 +0900
Labels:                 app.kubernetes.io/instance=aws-load-balancer-controller
                        app.kubernetes.io/managed-by=Helm
                        app.kubernetes.io/name=aws-load-balancer-controller
                        app.kubernetes.io/version=v2.11.0
                        helm.sh/chart=aws-load-balancer-controller-1.11.0
Annotations:            deployment.kubernetes.io/revision: 1
                        meta.helm.sh/release-name: aws-load-balancer-controller
                        meta.helm.sh/release-namespace: kube-system
Selector:               app.kubernetes.io/instance=aws-load-balancer-controller,app.kubernetes.io/name=aws-load-balancer-controller
Replicas:               2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:           app.kubernetes.io/instance=aws-load-balancer-controller
                    app.kubernetes.io/name=aws-load-balancer-controller
  Annotations:      prometheus.io/port: 8080
                    prometheus.io/scrape: true
  Service Account:  aws-load-balancer-controller
  Containers:
   aws-load-balancer-controller:
    Image:       public.ecr.aws/eks/aws-load-balancer-controller:v2.11.0
    Ports:       9443/TCP, 8080/TCP
    Host Ports:  0/TCP, 0/TCP
    Args:
      --cluster-name=myeks
      --ingress-class=alb
    Liveness:     http-get http://:61779/healthz delay=30s timeout=10s period=10s #success=1 #failure=2
    Readiness:    http-get http://:61779/readyz delay=10s timeout=10s period=10s #success=1 #failure=2
    Environment:  <none>
    Mounts:
      /tmp/k8s-webhook-server/serving-certs from cert (ro)
  Volumes:
   cert:
    Type:               Secret (a volume populated by a Secret)
    SecretName:         aws-load-balancer-tls
    Optional:           false
  Priority Class Name:  system-cluster-critical
  Node-Selectors:       <none>
  Tolerations:          <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   aws-load-balancer-controller-554fbd9d (2/2 replicas created)
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  8m35s  deployment-controller  Scaled up replica set aws-load-balancer-controller-554fbd9d to 2

$ kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
  Service Account:  aws-load-balancer-controller

# 클러스터롤, 롤 확인
$ kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
Name:         aws-load-balancer-controller-rolebinding
Labels:       app.kubernetes.io/instance=aws-load-balancer-controller
              app.kubernetes.io/managed-by=Helm
              app.kubernetes.io/name=aws-load-balancer-controller
              app.kubernetes.io/version=v2.11.0
              helm.sh/chart=aws-load-balancer-controller-1.11.0
Annotations:  meta.helm.sh/release-name: aws-load-balancer-controller
              meta.helm.sh/release-namespace: kube-system
Role:
  Kind:  ClusterRole
  Name:  aws-load-balancer-controller-role
Subjects:
  Kind            Name                          Namespace
  ----            ----                          ---------
  ServiceAccount  aws-load-balancer-controller  kube-system

$ kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
...
PolicyRule:
  Resources                                     Non-Resource URLs  Resource Names  Verbs
  ---------                                     -----------------  --------------  -----
  targetgroupbindings.elbv2.k8s.aws             []                 []              [create delete get list patch update watch]
  events                                        []                 []              [create patch]
  ingresses                                     []                 []              [get list patch update watch]
  services                                      []                 []              [get list patch update watch]
  ingresses.extensions                          []                 []              [get list patch update watch]
  services.extensions                           []                 []              [get list patch update watch]
  ingresses.networking.k8s.io                   []                 []              [get list patch update watch]
  services.networking.k8s.io                    []                 []              [get list patch update watch]
  endpoints                                     []                 []              [get list watch]
  namespaces                                    []                 []              [get list watch]
  nodes                                         []                 []              [get list watch]
  pods                                          []                 []              [get list watch]
  endpointslices.discovery.k8s.io               []                 []              [get list watch]
  ingressclassparams.elbv2.k8s.aws              []                 []              [get list watch]
  ingressclasses.networking.k8s.io              []                 []              [get list watch]
  ingresses/status                              []                 []              [update patch]
  pods/status                                   []                 []              [update patch]
  services/status                               []                 []              [update patch]
  targetgroupbindings/status                    []                 []              [update patch]
  ingresses.elbv2.k8s.aws/status                []                 []              [update patch]
  pods.elbv2.k8s.aws/status                     []                 []              [update patch]
  services.elbv2.k8s.aws/status                 []                 []              [update patch]
  targetgroupbindings.elbv2.k8s.aws/status      []                 []              [update patch]
  ingresses.extensions/status                   []                 []              [update patch]
  pods.extensions/status                        []                 []              [update patch]
  services.extensions/status                    []                 []              [update patch]
  targetgroupbindings.extensions/status         []                 []              [update patch]
  ingresses.networking.k8s.io/status            []                 []              [update patch]
  pods.networking.k8s.io/status                 []                 []              [update patch]
  services.networking.k8s.io/status             []                 []              [update patch]
  targetgroupbindings.networking.k8s.io/status  []                 []              [update patch]

 

해당 디플로이먼트는 aws-load-balancer-controller 라는 ServiceAccount를 사용하는데, Service/Ingerss 및 관련된 구성요소들에 대한 권한을 가진 것을 알 수 있습니다.

 

 

대상 유형별 리소스 배포 실습

AWS Load Balancer Controller 를 배포하면 Service 나 Ingress의 매니페스트에 추가된 Annotation이 다릅니다.

 

Note: The configuration of the provisioned load balancer is controlled by annotations that are added to the manifest for the Service or Ingress object and are different when using the AWS Load Balancer Controller than they are when using the AWS cloud provider load balancer controller.
https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

 

대상 유형(Target Type)의 차이를 확인하기 위해서 각각 instance 타입과 ip 타입으로 Loadbalancer 서비스를 생성해서 확인해보겠습니다.

# instance type을 위한 디플로이먼트 & 서비스 생성
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv1
  template:
    metadata:
      labels:
        app: deploy-websrv1
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: aews-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-instance-type
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: deploy-websrv1
EOF

 

그 다음은 대상 유형을 IP로 배포해 보겠습니다.

# 디플로이먼트 & 서비스 생성
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: aews-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: deploy-websrv
EOF

 

아래와 같이 CLB로 구성된 mario 서비스와 NLB로 구성된 instance type과 ip type으로 서비스가 각각 생성되었습니다.

$ kubectl get svc
NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                         PORT(S)        AGE
kubernetes              ClusterIP      10.100.0.1       <none>                                                                              443/TCP        74m
mario                   LoadBalancer   10.100.40.60     ac12f1c2b92b84516b339ffeea5afd12-1085404426.ap-northeast-2.elb.amazonaws.com        80:31207/TCP   24m
svc-nlb-instance-type   LoadBalancer   10.100.73.240    k8s-default-svcnlbin-9674736b71-4aae57171be6e3b8.elb.ap-northeast-2.amazonaws.com   80:32332/TCP   14s
svc-nlb-ip-type         LoadBalancer   10.100.124.220   k8s-default-svcnlbip-d503e33dc2-2ecb99f47d0bc5bf.elb.ap-northeast-2.amazonaws.com   80:30264/TCP   47s

 

웹 콘솔에서 두 대상 유형이 다른 NLB 구성이 어떻게 차이가 나는지 확인해보겠습니다.

 

먼저 instance로 생성한 NLB의 resource map을 살펴보면 Target이 NodePort로 등록되어 있는걸 알 수 있습니다.

이상한 점은 분명 서비스 엔드포인트로 호출이 되고 NodePort도 정상인데, 대상들이 모두 Unhealthy: Health checks failed로 표시된다는 점입니다. 이 부분이 by design인지 정확하지 않습니다.

 

아래는 ip로 생성한 NLB의 resource map입니다. 이 경우에는 파드 IP가 직접 등록된 것을 확인할 수 있습니다.

 

아래로 endpoint 정보를 확인하면 동일한 것을 알 수 있습니다.

$ kubectl get svc
NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                         PORT(S)        AGE
kubernetes              ClusterIP      10.100.0.1       <none>                                                                              443/TCP        92m
mario                   LoadBalancer   10.100.40.60     ac12f1c2b92b84516b339ffeea5afd12-1085404426.ap-northeast-2.elb.amazonaws.com        80:31207/TCP   42m
svc-nlb-instance-type   LoadBalancer   10.100.73.240    k8s-default-svcnlbin-9674736b71-4aae57171be6e3b8.elb.ap-northeast-2.amazonaws.com   80:32332/TCP   18m
svc-nlb-ip-type         LoadBalancer   10.100.124.220   k8s-default-svcnlbip-d503e33dc2-2ecb99f47d0bc5bf.elb.ap-northeast-2.amazonaws.com   80:30264/TCP   18m
$ kubectl get ep
NAME                    ENDPOINTS                              AGE
kubernetes              192.168.2.190:443,192.168.3.91:443     92m
mario                   192.168.1.136:8080                     42m
svc-nlb-instance-type   192.168.2.180:8080,192.168.3.56:8080   18m
svc-nlb-ip-type         192.168.1.137:8080,192.168.3.45:8080   18m

 

 

6. AKS의 LoadBalancer 구현

Azure의 LoadBalancer 유형의 서비스는 Azure LoadBalancer를 통해 구현됩니다.

AWS의 ELB들이 실제 서브넷에 위치해 파드 IP로 접근한 것과 다르게, Azure Loadbalancer는 서브넷에 위치하지 않습니다. 이러한 이유로 Azure Loadbalancer는 파드 IP로 직접 접근을 하지 못합니다.

 

Note: 파드IP가 가상 네트워크에서 유효한 IP를 가지고 있다고 하더라도 파드 IP로 다른 리소스에 접근하는 것과 다른 리소스에서 파드IP로 접근하는 것은 다릅니다. 파드 IP로 다른 리소스를 접근하는 것은 SNAT로 가능하지만 보통 반대 방향의 통신은 불가합니다.

 

실제 대상이 가상 네트워크에 있지 않으면 리소스는 파드IP로 접근할 수 없습니다.

 

이러한 이유로 AKS의 Azure Loadbalancer는 노드 IP를 통한 백엔드 구성을 합니다. 이 경우 EKS의 service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance 와 동일하다고 생각할 수 있지만, Azure Loadbalancer에서는 Floating IP(https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-floating-ip#floating-ip)라는 방식을 통해서 Loadbalancer에서 SNAT/DNAT이 없이 VIP로 서버로 패킷을 전달하고 iptables에서 DSR(Direct Server Return)할 수 있도록 구현되어 있습니다.

 

service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance와 동일한 구성도 가능한데, AKS에서 Loadbalancer 서비스에서 service.beta.kubernetes.io/azure-disable-load-balancer-floating-ip : "true"을 통해 Floating IP를 disable하면 DIP로 패킷이 전달되고, 이 때는 백엔드가 Nodeport 방식으로 구성됩니다.

참고: https://cloud-provider-azure.sigs.k8s.io/topics/loadbalancer/#loadbalancer-annotations

 

참고로 Azure의 Application Gateway는 서브넷에 위치하는 리소스 입니다. AKS의 Ingress 구현체 중 Application gateway Ingresss controller를 사용할 수 있는데, Appilcation Gateway의 백엔드 대상은 파드 IP를 직접 대상으로 등록합니다.

 

 

7. EKS의 Ingress 구현 및 실습

EKS에서 Ingress를 생성하면 ALB가 프로비저닝 됩니다. 또한 아래 문서의 사전 조건을 살펴보면 이를 위해서 AWS Load Balancer Controller 가 있어야 하는 것을 알 수 있습니다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/alb-ingress.html

 

아래와 같이 실습을 진행해 보겠습니다.

# 게임 파드와 Service, Ingress 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
  name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: game-2048
  name: deployment-2048
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: app-2048
  replicas: 2
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app-2048
    spec:
      containers:
      - image: public.ecr.aws/l6m2t8p7/docker-2048:latest
        imagePullPolicy: Always
        name: app-2048
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: game-2048
  name: service-2048
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort
  selector:
    app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: service-2048
              port:
                number: 80
EOF

 

서비스 매니페스트를 확인해보면 앞서 AWS Load Balancer Controller에서 생성된 IngressClass인 alb를 지정한 것을 알 수 있습니다.

ingress가 배포되었습니다.

$ kubectl get ing -n game-2048
NAME           CLASS   HOSTS   ADDRESS                                                                        PORTS   AGE
ingress-2048   alb     *       k8s-game2048-ingress2-70d50ce3fd-1587098903.ap-northeast-2.elb.amazonaws.com   80      3m9s

$ kubectl get svc -n game-2048
NAME           TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service-2048   NodePort   10.100.251.246   <none>        80:30521/TCP   5m31s

$ kubectl get ep -n game-2048
NAME           ENDPOINTS                           AGE
service-2048   192.168.1.239:80,192.168.3.219:80   5m23s

 

 

다시 웹 콘솔로 로드밸런서 정보를 확인해보면 ALB가 생성되었습니다.

 

ALB의 대상그룹을 확인해보면 ALB에서 파드 IP로 직접 전달하는 것을 알 수 있습니다. Ingress 이므로 Rules가 추가되어 시각화 되어 있는 것을 알 수 있습니다.

 

 

EKS의 Ingress에서는 Ingress Group를 지정해서 하나의 Ingress를 바라보도록 해서, 서로 다른 주체가 관리하도록 하는 개념이 있습니다.

 

출처: https://aws.amazon.com/ko/blogs/tech/a-deeper-look-at-ingress-sharing-and-target-group-binding-in-aws-load-balancer-controller/

 

아래와 같이 서로 다른 Ingerss를 사용하면서 하나의 ingress group으로 묶어서 배포하는 방식을 사용할 수 있는 점이 독특한 것 같습니다.

https://github.com/aws-samples/containers-blog-maelstrom/blob/main/aws-lb-controller-blog/ingress-grouping/orange-purple-ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: orange-purple-ingress
  namespace: orange-purple-ns
  labels:
    app: color-2
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/group.name: app-color-lb
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /orange
            pathType: Prefix
            backend:
              service:
                name: orange-service
                port:
                  number: 80                        
          - path: /purple
            pathType: Prefix
            backend:
              service:
                name: purple-service
                port:
                  number: 80

 

https://github.com/aws-samples/containers-blog-maelstrom/blob/main/aws-lb-controller-blog/ingress-grouping/blue-green-ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blue-green-ingress
  namespace: blue-green-ns
  labels:
    app: color-1
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/group.name: app-color-lb
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /blue
            pathType: Prefix
            backend:
              service:
                name: blue-service
                port:
                  number: 80                        
          - path: /green
            pathType: Prefix
            backend:
              service:
                name: green-service
                port:
                  number: 80

 

 

여기까지 EKS에서 살펴본 LoadBalancer 유형의 서비스와 Ingress가 사용하는 NLB, ALB 구성에 대한 전체적인 구성이 아래와 같다는 것을 알 수 있습니다.

출처: https://www.youtube.com/watch?v=E49Q3y9wsUo

 

 

8. AKS의 Ingress 구현

AKS에서 Ingress를 구현한 방식에는 크게 3가지가 있습니다. Application Gateway Ingress Controller, Application Gateway for Containers, Application Routing addon 입니다.

 

Note: 아래 문서를 보시면 Istio-based service mesh 또한 Ingress 옵션으로 이야기 하고 있지만 이는 istio Ingress API 이므로, 제외하고 설명하도록 하겠습니다.
https://learn.microsoft.com/en-us/azure/aks/concepts-network-ingress#compare-ingress-options

 

Application Gateway Ingress Controller는 AGIC 파드가 내부에서 API 서버를 모니터링 하다가 Azure Resource Manager로 설정 변경을 하는 아키텍처를 가지고 있습니다. Azure Load Balancer와 다르게 Applicaton Gateway는 서브넷에 위치하는 리소스로 파드 IP를 직접 백엔드로 구성합니다.

다만 Ingress의 다양한 설정을 하기에는 기존에 Application Gateway 라는 제품에서 가지고 있는 기능에서만 구현이 가능하다는 점에서 확장성이 부족한 솔루션으로 알려져 있습니다.

참고: https://learn.microsoft.com/en-us/azure/application-gateway/ingress-controller-overview

 

이로 인해서 비교적 최근 Application Gateway for Containers(이하 AGC) 라는 AKS를 위한 별도의 상품이 추가 되었습니다. AGC는 상대적으로 AGIC에 비해 다양한 고급 라우팅 기능이 추가 되었으며, Gateway API도 지원하고 있습니다.

참고: https://learn.microsoft.com/en-us/azure/application-gateway/for-containers/overview

 

다만 이러한 솔루션을 사용하지 않고 사용자가 직접 Nginx Ingress Controller 배포하여 사용하는 경우가 있습니다. 이를 위해서 AKS에서는 Application Routing addon으로 managed Nginx ingress를 제공하고 있습니다.

Managed Nginx ingress는 기존 Nginx Ingress Controller를 addon 형태로 제공할 뿐 구성되는 아키텍처는 동일합니다. 즉 Ingress controller가 파드로 실행되고, LoadBalancer 유형의 서비스로 엔드포인트를 구성합니다. Addon으로 제공하므로 설치 및 업그레이드와 같은 부분에서 Managed 서비스를 제공합니다. 다만 Manaed 서비스이기 때문에 오픈 소스 Nginx Ingress의 모든 설정을 지원하지 않으므로 사용하기 전에 평가가 필요합니다.

참고: https://learn.microsoft.com/en-us/azure/aks/app-routing

 

 

9. 리소스 정리

아래 절차로 생성된 EKS와 CloudFormation을 삭제합니다.

eksctl delete cluster --name $CLUSTER_NAME

# EKS 삭제 완료 후 실행
aws cloudformation delete-stack --stack-name myeks

 

 

마무리

EKS Networking을 두 포스트를 통해서 알아보고, 그 과정에서 EKS와 AKS의 차이를 살펴 봤습니다.

 

기본적으로 EKS의 VPC와 Azure의 Virtual Network에 차이가 있기 때문에 구현 방식이 달라지는 부분이 있었고, CNI를 구현하는 방식에는 유사한 듯 하면서도 서로 일부 제약사항이 있는 상황입니다.

전반적으로 EKS가 단일 CNI와 ELB 상품을 통한 Service/Ingress를 구현한 반면, AKS는 CNI나 Ingress 쪽에서 구현체가 다양한 모습을 확인할 수 있었습니다.

 

다음 포스트에서는 EKS의 노드 그룹과 스토리지 옵션을 살펴보도록 하겠습니다.

'EKS' 카테고리의 다른 글

[2-1] EKS Networking Part1 - VPC CNI와 파드 통신  (0) 2025.02.16
[1] EKS 생성과 리소스 살펴보기  (2) 2025.02.07

이번 포스트에서는 EKS Networking이라는 주제로 이어가 보겠습니다.

내용이 길어질 것으로 보아 파트를 나눠서 작성할 예정이며, EKS Networking Part1에서는 EKS의 VPC CNI와 기본 환경을 검토해보고, 파드와 노드 통신을 확인해보겠습니다. 이후 EKS Networking Part2에서 Loadbalancer 유형의 서비스의 구성과 Ingress에 대해서 확인해 보겠습니다.

 

 

목차

  1. AWS VPC CNI 개요
  2. 파드 생성 갯수 제한
  3. 실습 환경 배포
  4. 노드 정보 확인
  5. 파드 통신 확인

 

1. AWS VPC CNI 개요

CNI란?

CNI(Container Network Interface)는 CNCF(Cloud Native Computing Foundation)의 프로젝트로 Specification과 리눅스 컨테이너의 네트워크 인터페이스를 구성하기 위한 plugin을 작성하기 위한 라이브러리로 구성됩니다.

CNI는 컨테이너의 네트워크 연결성과 컨테이너가 삭제되었을 때 할당된 리소스를 제거하는 역할에 집중합니다.

참고: https://github.com/containernetworking/cni

 

보통 Kubernetes에 어떤 CNI를 쓰느냐라고 얘기를 하면 의미가 통하기는 하지만, 실제로 calico, cilium, flannel 등은 CNI plugin이라고 할 수 있습니다.

Kubernetes 에서 CNI Plugin의 동작은 간략히 아래와 같이 이뤄집니다.

  1. Kubelet이 Container Runtime에 컨테이너 생성을 요청
  2. Container Runtime이 컨테이너의 Network Namespace를 생성
  3. Container Runtime이 CNI 설정과 환경변수를 표준 입력으로 CNI Plugin 호출
  4. CNI Plugin이 컨테이너의 네트워크 인터페이스를 구성하고, IP를 할당하고, 호스트 네트워크 간의 veth pair 를 생성
  5. CNI Plugin이 호스트 네트워크 네임스페이스와 컨테이너 네트워크 네임스페이스에 라우팅을 구성

 

오래되긴 했지만 CNI와 CNI Plugin에 대해 잘 설명한 영상이 있습니다.

https://www.youtube.com/watch?v=4E_l-B988Ek&t=1341s

 

간단히 영상의 실습을 따라해보겠습니다.

먼저 go가 설치된 환경에서 아래와 같이 샘플 CNI plugin을 가져와 빌드를 진행합니다. 빌드가 끝나면 bin 폴더에 바이너리들이 위치합니다.

root@jumpVM:~# git clone https://github.com/containernetworking/plugins.git
Cloning into 'plugins'...
remote: Enumerating objects: 19825, done.
remote: Counting objects: 100% (268/268), done.
remote: Compressing objects: 100% (180/180), done.
remote: Total 19825 (delta 151), reused 86 (delta 86), pack-reused 19557 (from 2)
Receiving objects: 100% (19825/19825), 16.41 MiB | 19.77 MiB/s, done.
Resolving deltas: 100% (11214/11214), done.
root@jumpVM:~# cd plugins/
root@jumpVM:~/plugins# ls
CONTRIBUTING.md  README.md         go.mod       plugins
DCO              RELEASING.md      go.sum       test_linux.sh
LICENSE          build_linux.sh    integration  test_windows.sh
OWNERS.md        build_windows.sh  pkg          vendor
root@jumpVM:~/plugins# ./build_linux.sh
Building plugins
  bandwidth
  firewall
  portmap
  sbr
  tuning
  vrf
  bridge
  dummy
  host-device
  ipvlan
  loopback
  macvlan
  ptp
  tap
  vlan
  dhcp
  host-local
  static
root@jumpVM:~/plugins# ls
CONTRIBUTING.md  README.md       build_windows.sh  pkg              vendor
DCO              RELEASING.md    go.mod            plugins
LICENSE          bin             go.sum            test_linux.sh
OWNERS.md        build_linux.sh  integration       test_windows.sh
root@jumpVM:~/plugins# cd bin
root@jumpVM:~/plugins/bin# ls
bandwidth  dummy        host-local  macvlan  sbr     tuning
bridge     firewall     ipvlan      portmap  static  vlan
dhcp       host-device  loopback    ptp      tap     vrf

 

그리고 3개의 세션을 만들어서 아래의 명령을 실행합니다.

## 좌측 세션 (demons 생성)
$ sudo ip netns add demons
## 우측 상단 세션 (host ns의 ip/route 정보 확인)
$ watch -d -n 1 'ip a; echo ""; ip route'
## 우측 하단 세션 (domons의 ip/route 정보 확인)
$ watch -d -n 1 'ip netns exec demons ip a; echo ""; ip netns exec demons ip route;'

최초 상태는 아래와 같습니다.

 

컨테이너와 호스트간에 veth 디바이스를 생성해주는 ptp plugin을 사용합니다.

https://www.cni.dev/plugins/current/main/ptp/

 

ptp 바이너리를 실행하면 version을 알 수 없다고 합니다. CNI는 단순히 필요한 정보를 환경 변수로, 그리고 spec을 표준 입력으로 전달합니다. CNI_COMMAND로 VERSION을 넣고 다시 바이너리를 실행하면 제공하는 버전을 알려줍니다.

root@jumpVM:~/plugins/bin# ./ptp
CNI ptp plugin version unknown
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
root@jumpVM:~/plugins/bin# CNI_COMMAND=VERSION ./ptp
{"cniVersion":"1.1.0","supportedVersions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4.0","1.0.0","1.1.0"]}

 

아래와 같이 표준 입력으로 전달할 CNI 설정도 준비합니다. CNI plugin에는 IPAM과 컨테이너 네트워크를 구성하는 역할을 하는 plugin으로 나뉩니다. 여기에서는 ptp와 ipam으로 host-local을 사용했습니다.

{
    "cniVersion": "0.3.1",
    "name": "demonet",
    "type": "ptp", ## CNI binary
    "ipam": {
        "type": "host-local", ## CNI binary for IPAM
        "subnet": "192.168.0.0/24"
    }
}

 

이제 실제로 ADD 명령을 전달합니다. 하지만 여러가지 변수들이 지정되지 않아 에러가 발생하였습니다.

root@jumpVM:~/plugins/bin# CNI_COMMAND=ADD ./ptp < config
{
    "code": 4,
    "msg": "required env variables [CNI_CONTAINERID,CNI_NETNS,CNI_IFNAME,CNI_PATH] missing"
}

 

해당 변수들까지 환경 변수로 지정하고 명령을 다시 수행합니다.

$ CNI_COMMAND=ADD CNI_CONTAINERID=1234 CNI_NETNS=/var/run/netns/demons CNI_IFNAME=domoeth0 CNI_PATH=/root/plugins/bin ./ptp < config

 

명령 수행 결과로 생성된 interface에 대한 정보와 IPAM에서 전달된 ip 정보를 확인할 수 있으며, 우측 세션을 보면 새로운 veth pair와 라우팅이 추가되었음을 알 수 있습니다.

 

마지막으로 DEL 명령으로 테스트 구성을 삭제합니다.

CNI_COMMAND=DEL CNI_CONTAINERID=1234 CNI_NETNS=/var/run/netns/demons CNI_IFNAME=domoeth0 CNI_PATH=/root/plugins/bin ./ptp < config  

이렇게 샘플 CNI plugin을 실행해 봄으로서 앞서 설명한 과정을 이해할 수 있으며, Container Runtime이 어떤 방식으로 CNI plugin을 호출하는지를 이해할 수 있습니다. 실제로 ADD, DEL 과 같은 요청을 합니다.

 

 

AWS VPC CNI 소개

오픈소스 CNI plugin들은 파드 네트워크를 VxLAN이나 IPIP와 같은 프로토콜을 이용해 overlay 네트워크로 구성하는 경우가 많습니다. 이것은 노드가 위치한 네트워크가 물리적으로 구성되어 있거나 하는 이유로 CNI가 직접 IP를 관리하기 어려운 이유도 있을 수 있고, 한편으로는 어떤 네트워크 환경에서든 유연하게 사용 가능하게 하기 위한 목적일 수도 있습니다.

 

반면 AWS VPC CNI는 AWS 환경에 최적화되어 VPC 네트워크와 동일한 IP 주소를 파드에 할당합니다. 또 AWS VPC CNI는 eBPF 기반의 Network Policy를 기본으로 제공하고 있습니다. (앞서 노드에 배포된 파드를 확인하면서 aws-node의 컨테이너가 각 VPC CNI, Network Policy Agent 인것을 확인했습니다)

 

AWS VPC CNI의 컴포넌트는 CNI 바이너리와 ipamd로 이뤄져 있습니다.

  • CNI binary: 파드 네트워크를 구성
  • ipamd: IP Address Management(IPAM) 데몬으로, ENI를 관리하고 사용가능한 IP 혹은 prefix를 warm-pool로 관리합니다.

앞서 실습을 통해 EKS를 생성한 이후 노드에 ENI가 2개 생성된 경우가 확인했는데 이것은 VPC CNI의 동작과도 관련이 있습니다.

 

먼저 인스턴스가 생성되면 EC2는 첫번째 ENI를 생성해 연결하고, VPC CNI가 hostNetwork 모드로 Primary IP로 실행됩니다. 노드가 프로비저닝 되면, CNI 플러그인 Primary ENI에 IP 혹은 Prefix의 slots를 가져와 IP Pool에 할당합니다. 이러한 IP Pool을 warm pool 이라고 하며 warm pool의 사이즈는 노드의 인스턴스 타입에 결정됩니다.

각 ENI는 인스턴스는 타입에 따라 제한된 slot 개수를 지원하기 때문에, 추가로 필요한 slot에 따라 CNI는 인스턴스에 추가 ENI를 할당 요청 합니다.

 

요약하면, 인스턴스 타입에 따라 IP Pool의 개수가 정해져 있고, 이때 slots 단위로 IP를 warm pool에 저장해두고, IP가 필요하면 warm pool에서 파드에 할당합니다. 인스턴스 타입에 따라 ENI가 가질 수 있는 IP 개수도 제한되기 때문에 warm pool이 증가할 때 추가 ENI가 필요할 수 있습니다.

 

아래 VPC CNI 문서의 그림을 살펴보면 flow를 확인할 수 있습니다.

새 slot이 필요할 때, Primary ENI에 여유 slot이 있으면 파드에 이 slot을 할당합니다. Primary ENI에 여유 slot이 없으면, Secondary ENI가 있는지 확인하고, 다시 Secondary ENI에서 여유 slot을 확인해 파드에 slot을 할당합니다.

Secondary ENI가 없는 경우, 인스턴스에 새로운 ENI 가용을 확인하여, 가능하다면 새로운 ENI를 할당합니다.

참고: https://docs.aws.amazon.com/eks/latest/best-practices/vpc-cni.html

 

이러한 절차에서 복잡한 느낌을 지울 수 없습니다.

 

AKS의 Azure CNI는 AWS VPC CNI와 유사하게 Virtual Network(EKS의 VPC)에서 Pod IP를 가져와 사용합니다. 이때 하나의 NIC에서 할당 가능한 IP개수의 제한은 없으므로, Azure CNI은 단순히 정의된 max-pods 개수 만큼 secondary IP로 배정합니다.

 

물론 VPC CNI는 slots 만큼 동적 할당을 하기 때문에 불필요한 IP를 static 하게 할당받지 않는 장점이 있을 수 있습니다.

참고로 AKS는 다양한 CNI plugin을 제공하고 있습니다.

먼저 VPC CNI와 유사하게 Virtual Network의 IP를 사용하는 Azure CNI가 있고, Azure CNI에서 동적으로 파드 IP할당하는 방식인 Azure CNI with dynamic IP allocation 이 있습니다. 또한 overlay network 사용하는 kubenet, Azure CNI overlay가 있습니다.

이때, Azure CNI overlay와 Azure CNI with dynamic IP allocation의 경우 dataplane을 cilium으로 오프로딩하는 Azure CNI powered by cilium 를 사용할 수 있습니다.

참고: https://learn.microsoft.com/en-us/azure/aks/concepts-network-cni-overview#choosing-a-cni

 

 

VPC CNI의 동작 과정은 아래 그림에서 살펴보실 수 있습니다.

출처: https://docs.aws.amazon.com/eks/latest/best-practices/vpc-cni.html

  1. 파드가 스케줄링 됩니다.
  2. kubelet은 Add 요청을 VPC CNI로 보냅니다.
  3. VPC CNI는 L-IPAM에 파드 IP를 요청합니다.
  4. L-IPAM에서 파드 IP를 반환합니다.
  5. VPC CNI는 네트워크 네임스페이스를 구성합니다.
  6. VPC CNI는 파드 IP를 kubelet으로 반환합니다.
  7. kubelet은 파드 IP를 파드에 할당합니다.

 

이 절차가 사실 일반적인 CNI plugin의 동작 과정과 조금 다르게 묘사되어 있습니다. kubelet이 IP를 할당한다라.. 쉽게 설명하려고 한건지 정확하진 않네요.

이 그림에서 VPC CNI의 구성요소와 전반적인 흐름을 이해하는 정도로 넘어가는게 좋을 것 같습니다.

 

추가로 해당 문서를 보면 L-IPAM에서 iptables의 NAT rules를 업데이트 한다고 표시도 있습니다. 아마도 VPC IP에 대한 정보를 알고 있으므로, 이를 바탕으로 VPC를 제외한 네트워크에 대해 SNAT 정책을 업데이트 하는 것으로 이해됩니다.

 

 

2. 파드 생성 갯수 제한

AWS는 인스턴스 타입별로 최대 ENI 개수와 ENI별 할당가능한 최대 IP 개수가 정해져 있습니다.

아래와 같은 명령으로 t3 시리즈에 해당하는 값을 확인할 수 있습니다.

$ aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.\* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table
 --------------------------------------
|        DescribeInstanceTypes       |
+----------+----------+--------------+
| IPv4addr | MaxENI   |    Type      |
+----------+----------+--------------+
|  15      |  4       |  t3.2xlarge  |
|  6       |  3       |  t3.medium   |
|  12      |  3       |  t3.large    |
|  15      |  4       |  t3.xlarge   |
|  2       |  2       |  t3.nano     |
|  2       |  2       |  t3.micro    |
|  4       |  3       |  t3.small    |
+----------+----------+--------------+

 

예를 들어, t3.medium은 최대 3개의 ENI를 연결할 수 있고, ENI별 6개의 IP가 할당 가능 합니다.

총 3*6=18개 IP가 사용가능합니다만, 각 ENI에 할당되는 IP는 파드에서 사용할 수 없게 됩니다. 또한 VPC CNI와 kube-proxy가 hostNetwork를 사용하므로 이들은 IP가 필요 없습니다.

 

결론적으로, EKS에서 최대 파드 개수는 인스턴스 타입에 따라 아래와 같은 공식으로 계산됩니다.

(Number of network interfaces for the instance type * (the number of IP addresses per network interface - 1)) + 2

즉, t3.medium은 3*(6-1)+2= 17개 파드가 실행될 수 있습니다.

 

이는 인스턴스 유형마다 다를 수 있지만 최대 파드 개수에는 limit이 있습니다. vCPU 30개 미만의 EC2 인스턴스 유형에서는 최대 파드 개수가 110개로 제한되고, vCPU 30이상의 EC2 인스턴스 유형에서는 250개로 제한을 권고 합니다.

VPC CNI 절에서 설명한 ENI와 파드 생성 갯수 제한에 대한 테스트는 이후 노드 정보를 확인하는 과정에서 다시 살펴보겠습니다.

 

 

참고로 AKS는 노드에 생성 가능한 파드 갯수의 제한은 인터페이스와 관련이 없이 250으로 지정되어 있으며 또한 --max-pods 옵션으로 정의할 수 있습니다. 다만 --max-pods 옵션을 지정하지 않은 경우 CNI별로 일부 차이가 있으므로 이는 문서를 확인하시기 바랍니다.

https://learn.microsoft.com/en-us/azure/aks/quotas-skus-regions#service-quotas-and-limits

 

 

EKS에서는 인스턴스 타입과 ENI 제약으로 인해 최대 파드 개수가 제한되는 독특한 문제를 가지고 있습니다. 이를 해소하기 위해 ENI에 특정 IP prefix를 할당하는 Prefix Delegation(https://www.eksworkshop.com/docs/networking/vpc-cni/prefix/)와 같은 방식을 제공하고 있습니다.

 

 

AWS VPC CNI의 컨셉을 살펴보면 Azure CNI와 비슷한 문제점이 있고, 또한 해결을 위한 유사한 접근 방법을 가지고 있습니다.

첫번째 문제는 다수의 파드/노드가 생성되는 경우 VPC 자체의 IP가 고갈되는 문제입니다.

근본적으로 더 큰 VPC를 가진 클러스터를 생성해야 합니다. 또한 EKS와 AKS는 동일하게 VPC, Virtual Network에 Address Space를 추가하는 방식을 제공하고 있습니다.

추가로 AKS에서는 이러한 제약사항을 극복하기 위해서 overlay network를 사용하는 CNI(kubenet, Azure CNI overlay)를 선택할 수 있다는 점에서 차이점이 있습니다.

 

두번째 문제는 Pre-IP allocation으로 이미 할당은 되었지만 미사용되는 IP로 인한 낭비입니다. 이런 warm pool이 다수의 노드에 존재한다면 IP가 상당히 낭비될 수 있습니다.

AWS VPC CNI에서는 Warm Pool 에 관련된 설정을 변경하는 방식으로 할당되는 IP를 최소화 할 수 있습니다. 이때 WARM_IP_TARGET, MINIMUM_IP_TARGET 과 같은 값을 조정할 수 있습니다.

AKS에서는 Azure CNI with Dynamic Pod IP Allocation 방식을 통해 Pod가 사용하는 서브넷을 지정할 수 있습니다. 이 방식은 Azure CNI와 다르게, batch 사이즈 만큼의 IP를 할당하고 지정된 퍼센티지 이상 사용하는 경우 batch 사이즈 만큼을 추가로 할당하는 방식을 사용합니다. 이 방식은 EKS의 VPC CNI의 worm pool 방식과 Custom Networking(https://www.eksworkshop.com/docs/networking/vpc-cni/custom-networking/)을 섞은 것 같기도 합니다.

 

 

3. 실습 환경 배포

먼저 아래를 따라 기본적인 실습 환경을 구성합니다.

아래 CloudFormation을 배포하면서 Key를 인자로 전달하기 때문에 사전에 key pair가 있는지 확인하시고, 없으면 생성을 해야 합니다. (EC2>Key pairs>Create key pair>.pem 형식으로 생성)

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

# 배포
# aws cloudformation deploy --template-file myeks-1week.yaml --stack-name mykops --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 --region <리전>
예시) aws cloudformation deploy --template-file ./myeks-2week.yaml \
     --stack-name myeks --parameter-overrides KeyName=ekskey SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 운영서버 EC2 IP 확인
ec2ip=$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text)

# 운영서버 EC2 에 SSH 접속
ssh -i <ssh 키파일> ec2-user@$ec2ip

 

CloudFormation으로 배포되는 실습 환경은 아래와 같습니다.

 

myeks-vpc와 operator-vpc를 각각 만들고 VPC Peering으로 연결합니다.

operator-vpc에는 CloudFormation으로 미리 operator-host 라는 서버를 배포했습니다.

myeks-vpc에는 각 가용 영역에 PublicSubnet과 PrivateSubnet을 구성했고, PublicSubnet에 EKS를 배포할 예정입니다.

 

이제 EKS를 배포하겠습니다.

# 환경 변수 지정
export CLUSTER_NAME=myeks

# myeks-VPC/Subnet 정보 확인 및 변수 지정
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
echo $VPCID

export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
echo $PubSubnet1 $PubSubnet2 $PubSubnet3

# 출력된 내용 참고해 아래 yaml에서 vpc/subnet id, ssh key 경로 수정
eksctl create cluster --name $CLUSTER_NAME --region=ap-northeast-2 --nodegroup-name=ng1 --node-type=t3.medium --nodes 3 --node-volume-size=30 --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version 1.31 --with-oidc --external-dns-access --full-ecr-access --alb-ingress-access --node-ami-family AmazonLinux2023 --ssh-access --dry-run > myeks.yaml

 

이후 생성된 myeks.yaml 에서 # 각자 환경 정보로 수정로 커맨트된 열을 확인하고 정보가 다른 경우 수정합니다.

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: myeks
  region: ap-northeast-2
  version: "1.31"

kubernetesNetworkConfig:
  ipFamily: IPv4

iam:
  vpcResourceControllerPolicy: true
  withOIDC: true

accessConfig:
  authenticationMode: API_AND_CONFIG_MAP

vpc:
  autoAllocateIPv6: false
  cidr: 192.168.0.0/16
  clusterEndpoints:
    privateAccess: true # if you only want to allow private access to the cluster
    publicAccess: true # if you want to allow public access to the cluster
  id: vpc-0ab40d2acbda845d8  # 각자 환경 정보로 수정
  manageSharedNodeSecurityGroupRules: true # if you want to manage the rules of the shared node security group
  nat:
    gateway: Disable
  subnets:
    public:
      ap-northeast-2a:
        az: ap-northeast-2a
        cidr: 192.168.1.0/24
        id: subnet-014dc12ab7042f604  # 각자 환경 정보로 수정
      ap-northeast-2b:
        az: ap-northeast-2b
        cidr: 192.168.2.0/24
        id: subnet-01ba554d3b16a15a7  # 각자 환경 정보로 수정
      ap-northeast-2c:
        az: ap-northeast-2c
        cidr: 192.168.3.0/24
        id: subnet-0868f7093cbb17c34  # 각자 환경 정보로 수정

addons:
  - name: vpc-cni # no version is specified so it deploys the default version
    version: latest # auto discovers the latest available
    attachPolicyARNs: # attach IAM policies to the add-on's service account
      - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
    configurationValues: |-
      enableNetworkPolicy: "true"

  - name: kube-proxy
    version: latest

  - name: coredns
    version: latest

  - name: metrics-server
    version: latest

privateCluster:
  enabled: false
  skipEndpointCreation: false

managedNodeGroups:
- amiFamily: AmazonLinux2023
  desiredCapacity: 3
  disableIMDSv1: true
  disablePodIMDS: false
  iam:
    withAddonPolicies:
      albIngress: false # Disable ALB Ingress Controller
      appMesh: false
      appMeshPreview: false
      autoScaler: false
      awsLoadBalancerController: true # Enable AWS Load Balancer Controller
      certManager: true # Enable cert-manager
      cloudWatch: false
      ebs: false
      efs: false
      externalDNS: true # Enable ExternalDNS
      fsx: false
      imageBuilder: true
      xRay: false
  instanceSelector: {}
  instanceType: t3.medium
  preBootstrapCommands:
    # install additional packages
    - "dnf install nvme-cli links tree tcpdump sysstat ipvsadm ipset bind-utils htop -y"
    # disable hyperthreading
    - "for n in $(cat /sys/devices/system/cpu/cpu*/topology/thread_siblings_list | cut -s -d, -f2- | tr ',' '\n' | sort -un); do echo 0 > /sys/devices/system/cpu/cpu${n}/online; done"
  labels:
    alpha.eksctl.io/cluster-name: myeks
    alpha.eksctl.io/nodegroup-name: ng1
  maxSize: 3
  minSize: 3
  name: ng1
  privateNetworking: false
  releaseVersion: ""
  securityGroups:
    withLocal: null
    withShared: null
  ssh:
    allow: true
    publicKeyName: mykeyname  # 각자 환경 정보로 수정
  tags:
    alpha.eksctl.io/nodegroup-name: ng1
    alpha.eksctl.io/nodegroup-type: managed
  volumeIOPS: 3000
  volumeSize: 30
  volumeThroughput: 125
  volumeType: gp3

 

 

마지막으로 생성된 myeks.yaml을 바탕으로 EKS를 배포합니다.

# kubeconfig 파일 경로 위치 지정 : 
export KUBECONFIG=$HOME/.kube/config

# 배포
eksctl create cluster -f myeks.yaml --verbose 4

 

생성이 완료되면 클러스터 정보를 확인합니다.

# 클러스터 확인
$ kubectl cluster-info
Kubernetes control plane is running at https://F141CFF9E7E8776AF6826A7D1341FBEA.yl4.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://F141CFF9E7E8776AF6826A7D1341FBEA.yl4.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'.
$ eksctl get cluster
NAME    REGION          EKSCTL CREATED
myeks   ap-northeast-2  True

# 노드 확인
$ 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-203.ap-northeast-2.compute.internal   Ready    <none>   2m37s   v1.31.4-eks-aeac579   t3.medium       ON_DEMAND      ap-northeast-2a
ip-192-168-2-77.ap-northeast-2.compute.internal    Ready    <none>   2m35s   v1.31.4-eks-aeac579   t3.medium       ON_DEMAND      ap-northeast-2b
ip-192-168-3-141.ap-northeast-2.compute.internal   Ready    <none>   2m35s   v1.31.4-eks-aeac579   t3.medium       ON_DEMAND      ap-northeast-2c

# 생성 노드 확인
$ aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
-----------------------------------------------------------------------------------------
|                                   DescribeInstances                                   |
+----------------------+-----------------+----------------+-----------------+-----------+
|      InstanceID      |  InstanceName   | PrivateIPAdd   |   PublicIPAdd   |  Status   |
+----------------------+-----------------+----------------+-----------------+-----------+
|  i-095eb2faccd652e06 |  myeks-ng1-Node |  192.168.3.141 |  43.201.9.71    |  running  |
|  i-035d1900144a7ba5f |  operator-host  |  172.20.1.100  |  3.34.186.167   |  running  |
|  i-0b35dccaffe41de41 |  myeks-ng1-Node |  192.168.1.203 |  43.202.40.202  |  running  |
|  i-0543c6996984a0290 |  myeks-ng1-Node |  192.168.2.77  |  15.164.237.196 |  running  |
+----------------------+-----------------+----------------+-----------------+-----------+

# 관리형 노드 그룹 확인
$ eksctl get nodegroup --cluster $CLUSTER_NAME
CLUSTER NODEGROUP       STATUS  CREATED                 MIN SIZE        MAX SIZE        DESIRED CAPACITY        INSTANCE TYPE   IMAGE ID                ASG NAMETYPE
myeks   ng1             ACTIVE  2025-02-14T13:28:22Z    3               3               3                       t3.medium       AL2023_x86_64_STANDARD  eks-ng1-7cca8252-ac30-3af4-6c5e-a3b2d91981c3     managed

 

AWS는 서브넷이 각 가용 영역에 배치되는 리소스입니다. Azure에서는 서브넷을 생성해도 이는 가용 영역을 걸쳐서 생성되는 리전 리소스입니다. 이러한 이유로 EKS는 생성 시 가용 영역별로 서브넷을 제공하면 노드들은 자동으로 가용 영역에 배치됩니다.

 

AKS는 서브넷을 지정해도 가용 영역의 구분이 없기 때문에 생성 시점에 가용 영역 여부를 별도로 지정해야 합니다. 아래는 포탈에서 AKS를 생성하며 가용 영역을 지정하는 화면입니다.

 

아래로 AKS의 availability zone 문서를 참고하실 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/availability-zones-overview#zone-spanning

 

 

파드 정보도 확인해봅니다.

# 파드 확인
$ kubectl get pod -A
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE
kube-system   aws-node-5skww                    2/2     Running   0          13m
kube-system   aws-node-qmmn8                    2/2     Running   0          13m
kube-system   aws-node-x5rxg                    2/2     Running   0          13m
kube-system   coredns-9b5bc9468-8prkc           1/1     Running   0          18m
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running   0          18m
kube-system   kube-proxy-d7l2t                  1/1     Running   0          13m
kube-system   kube-proxy-fp99v                  1/1     Running   0          13m
kube-system   kube-proxy-sxq5k                  1/1     Running   0          13m
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running   0          18m
kube-system   metrics-server-86bbfd75bb-xm8ds   1/1     Running   0          18m

$ kubectl get ds -A
NAMESPACE     NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
kube-system   aws-node     3         3         3       3            3           <none>          18m
kube-system   kube-proxy   3         3         3       3            3           <none>          18m

# eks addon 확인
$ eksctl get addon --cluster $CLUSTER_NAME
2025-02-14 22:43:26 [ℹ]  Kubernetes version "1.31" in use by cluster "myeks"
2025-02-14 22:43:26 [ℹ]  getting all addons
2025-02-14 22:43:27 [ℹ]  to see issues for an addon run `eksctl get addon --name <addon-name> --cluster <cluster-name>`
NAME            VERSION                 STATUS  ISSUES  IAMROLE                                                                         UPDATE AVAILABLE        CONFIGURATION VALUES     POD IDENTITY ASSOCIATION ROLES
coredns         v1.11.3-eksbuild.1      ACTIVE  0                                                                                       v1.11.4-eksbuild.2,v1.11.4-eksbuild.1,v1.11.3-eksbuild.2
kube-proxy      v1.31.2-eksbuild.3      ACTIVE  0                                                                                       v1.31.3-eksbuild.2
metrics-server  v0.7.2-eksbuild.1       ACTIVE  0
vpc-cni         v1.19.0-eksbuild.1      ACTIVE  0       arn:aws:iam::430118812536:role/eksctl-myeks-addon-vpc-cni-Role1-fdAOfLzN8tNl    v1.19.2-eksbuild.5,v1.19.2-eksbuild.1

 

앞서 설명드린 바와 같이 EKS의 노드에는 aws-node 가 daemonset 으로 실행되고, VPC CNI와 Network Policy Agent로 멀티 컨테이너로 구성되어 있습니다.

 

Azure CNI에서는 azure-vnet과 azure-vnet-ipam가 binary로 구현되어 있고 별도의 파드가 생성되지 않습니다. 다만 azure CNI overlay과 같은 유형의 CNI에서는 IPAM으로 azure-cns 데몬셋이 사용됩니다.

 

 

한편 EKS의 VPC CNI는 L-IPAM 에서 NAT rules를 iptables로 반영합니다.

AKS의 Azure CNI도 Virtual Network 이 외의 네트워크에 대해서 NAT rules을 생성하는데, ip-masq-agent를 daemonset으로 사용하는 차이가 있습니다.

# Virtual Network에 대해서 nonMasqeuradeCIDRs 로 정의되어 있음
$ kubectl get cm azure-ip-masq-agent-config-reconciled -n kube-system -oyaml
apiVersion: v1
data:
  ip-masq-agent-reconciled: |-
    nonMasqueradeCIDRs:
      - 10.244.0.0/16
    masqLinkLocal: true
kind: ConfigMap
...
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    component: ip-masq-agent
    kubernetes.io/cluster-service: "true"
  name: azure-ip-masq-agent-config-reconciled
  namespace: kube-system

# 노드의 iptables 정보
root@aks-nodepool1-76251328-vmss000000:/# iptables -t nat -S |grep ip-masq-agent
-A POSTROUTING -m comment --comment "\"ip-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom IP-MASQ-AGENT chain\"" -m addrtype ! --dst-type LOCAL -j IP-MASQ-AGENT
-A IP-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -m comment --comment "ip-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE

 

 

4. 노드 정보 확인

앞서 살펴본 VPC CNI의 특성을 확인하기 위해서 노드 정보를 살펴보겠습니다.

 

먼저 웹 콘솔에서 노드를 살펴봅니다. t3.medium으로 생성되었습니다.

 

Networking 탭을 살펴보면 2개의 ENI에 해당하는 Private IP가 2개 확인되며, Secondary IP가 10개 확인됩니다.

 

아래를 살펴보면 2개의 ENI가 연결되었음을 알 수 있습니다.

 

노드에 할당된 IP를 확인해보면 hostNetwork를 제외하고 3개의 IP가 secondary IP에서 사용되고 있습니다.

$ kubectl get po -A -owide |grep ip-192-168-3-141.ap-northeast-2.compute.internal
kube-system   aws-node-x5rxg                    2/2     Running   0          70m   192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-8prkc           1/1     Running   0          75m   192.168.3.45    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running   0          75m   192.168.3.48    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   kube-proxy-sxq5k                  1/1     Running   0          70m   192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running   0          75m   192.168.3.69    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>

 

물론 노드에는 2개의 ENI에 해당하는 ens5, ens6의 IP만 확인됩니다.

[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni98b6af8f1d6@if3 UP             fe80::54a1:b9ff:fe80:c915/64

 

앞서 EKS 노드에서 최대 파드 개수는 인스턴스 타입에 따라 아래와 같은 공식으로 계산되고, t3.medium에서는 3*(6-1)+2= 17개 파드가 실행될 수 있다고 알아봤습니다.

 

이 내용을 테스트를 위해서 테스트 디플로이먼트를 배포합니다.

$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: netshoot-pod
spec:
  replicas: 3
  selector:
    matchLabels:
      app: netshoot-pod
  template:
    metadata:
      labels:
        app: netshoot-pod
    spec:
      containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

 

아래와 같이 replicas를 늘여서 실제 변경되는 부분을 확인해 보겠습니다.

# 파드 확인 
$ kubectl get pod -o wide
NAME                            READY   STATUS    RESTARTS   AGE     IP              NODE                                               NOMINATED NODE   READINESS GATES
netshoot-pod-744bd84b46-g7hzp   1/1     Running   0          6m18s   192.168.3.223   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
netshoot-pod-744bd84b46-llqjz   1/1     Running   0          6m18s   192.168.1.220   ip-192-168-1-203.ap-northeast-2.compute.internal   <none>           <none>
netshoot-pod-744bd84b46-rwblp   1/1     Running   0          6m18s   192.168.2.140   ip-192-168-2-77.ap-northeast-2.compute.internal    <none>           <none>
nsenter-vg4pv8                  1/1     Running   0          7m1s    192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>

# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment netshoot-pod --replicas=8

# 노드에 접속 후 확인 (사전)
[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni98b6af8f1d6@if3 UP             fe80::54a1:b9ff:fe80:c915/64

# 노드에 접속 후 확인 (사후)
[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni98b6af8f1d6@if3 UP             fe80::54a1:b9ff:fe80:c915/64
eni9c40e9afed0@if3 UP             fe80::58ea:7dff:fe10:23f/64
ens7             UP             192.168.3.104/24 fe80::883:eff:fe48:b1bf/64

 

확인해보면 파드 개수가 늘어나자 ens7이 신규로 생성된 것을 알 수 있습니다.

웹 콘솔에서도 Secondary IP가 15개로 증가한 것을 확인할 수 있습니다.

 

이때 Primary ENI가 아닌 ENI는 통신에 관여를 할지 한번 확인해보겠습니다.

kubectl get po -A -owide |grep ip-192-168-3-141.ap-northeast-2.compute.internal
default       netshoot-pod-744bd84b46-7ggdq     1/1     Running   0          7m10s   192.168.3.83    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
default       netshoot-pod-744bd84b46-g7hzp     1/1     Running   0          15m     192.168.3.223   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
default       nsenter-vg4pv8                    1/1     Running   0          15m     192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   aws-node-x5rxg                    2/2     Running   0          88m     192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-8prkc           1/1     Running   0          92m     192.168.3.45    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running   0          92m     192.168.3.48    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   kube-proxy-sxq5k                  1/1     Running   0          88m     192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running   0          93m     192.168.3.69    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   metrics-server-86bbfd75bb-xm8ds   1/1     Running   0          93m     192.168.3.56    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>

 

ENI 정보를 확인해보면 192.168.3.83만 두번째 ENI에 할당된 IP로 확인됩니다.

 

해당 파드에서 ping을 하고, tcpdump를 확인해봐도 Primary ENI에서만 패킷이 확인되는 것을 알 수 있습니다.

(정정) 이후 파드 통신 절에서 파드 통신 테스트를 한번 더 해보니 이건 ENI가 통신 관여를 하지 않는 건 아니고, 외부 통신은 Primary ENI에서 내부 통신은 해당 IP를 가진 Secondary ENI에서 통신이 이뤄졌습니다.

 

이번에는 replicas를 50개까지 증가시켜보겠습니다.

$ kubectl scale deployment netshoot-pod --replicas=50
deployment.apps/netshoot-pod scaled

$ kubectl get po |grep -v Running
NAME                            READY   STATUS    RESTARTS   AGE
netshoot-pod-744bd84b46-69r4x   0/1     Pending   0          23s
netshoot-pod-744bd84b46-bhprl   0/1     Pending   0          23s
netshoot-pod-744bd84b46-g6pp4   0/1     Pending   0          24s
netshoot-pod-744bd84b46-gs4dt   0/1     Pending   0          24s
netshoot-pod-744bd84b46-jpxbc   0/1     Pending   0          24s
netshoot-pod-744bd84b46-spdw6   0/1     Pending   0          24s
netshoot-pod-744bd84b46-v56h5   0/1     Pending   0          23s
netshoot-pod-744bd84b46-vhlzn   0/1     Pending   0          24s
netshoot-pod-744bd84b46-vngbr   0/1     Pending   0          23s
netshoot-pod-744bd84b46-wf58t   0/1     Pending   0          24s
$ kubectl get po -A |wc
     62     372    5026
$ kubectl get po |grep -v Running|wc
     11      55     725

 

t3.medium 에서는 17개의 파드가 실행 가능하고, 노드가 3대이므로 총 51개의 파드가 실행 가능합니다. 결국 11개의 파드는 unschedulable 한 상태로 빠집니다.

 

 

5. 파드 통신 확인

EKS의 VPC CNI는 파드에 VPC의 IP를 할당하는 방식으로 NAT 통신이 없이 파드간 통신이 이뤄집니다.

 

클러스터 내 파드 간 통신 확인

현재 파드는 3개로 각 노드에 배포되어 있습니다. 이때, 파드간 통신을 확인해보고, 노드에서 tcpdump로 확인합니다.

$ kubectl get po -A -owide
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE    IP              NODE                                               NOMINATED NODE   READINESS GATES
default       netshoot-pod-744bd84b46-8mrkj     1/1     Running   0          15s    192.168.3.31    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
default       netshoot-pod-744bd84b46-lgkxf     1/1     Running   0          15s    192.168.2.140   ip-192-168-2-77.ap-northeast-2.compute.internal    <none>           <none>
default       netshoot-pod-744bd84b46-qzq29     1/1     Running   0          15s    192.168.1.126   ip-192-168-1-203.ap-northeast-2.compute.internal   <none>           <none>
..

# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].metadata.name}')
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].metadata.name}')
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].metadata.name}')

# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].status.podIP}')
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].status.podIP}')
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].status.podIP}')

echo $PODIP1 $PODIP2 $PODIP3
192.168.3.31 192.168.2.140 192.168.1.126

# 파드1 Shell 에서 파드2로 ping 테스트, 8.8.8.8로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
 kubectl exec -it $PODNAME1 -- ping -c 2 8.8.8.8

# 워커 노드 EC2 : TCPDUMP 확인
## For Pod to external (outside VPC) traffic, we will program iptables to SNAT using Primary IP address on the Primary ENI.
[root@ip-192-168-3-141 /]# sudo tcpdump -i any -nn icmp
tcpdump: data link type LINUX_SLL2
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
15:31:58.525323 eni89548af8467 In  IP 192.168.3.31 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
15:31:58.525366 ens6  Out IP 192.168.3.31 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
15:31:58.528035 ens6  In  IP 192.168.2.140 > 192.168.3.31: ICMP echo reply, id 7, seq 1, length 64
15:31:58.528089 eni89548af8467 Out IP 192.168.2.140 > 192.168.3.31: ICMP echo reply, id 7, seq 1, length 64
...
15:35:38.173442 eni89548af8467 In  IP 192.168.3.31 > 8.8.8.8: ICMP echo request, id 13, seq 1, length 64
15:35:38.173494 ens5  Out IP 192.168.3.141 > 8.8.8.8: ICMP echo request, id 24172, seq 1, length 64
15:35:38.202228 ens5  In  IP 8.8.8.8 > 192.168.3.141: ICMP echo reply, id 24172, seq 1, length 64
15:35:38.202403 eni89548af8467 Out IP 8.8.8.8 > 192.168.3.31: ICMP echo reply, id 13, seq 1, length 64
..

 

파드 간 통신에서 출발지는 컨테이너의 veth 인터페이스이며, 이후 ens6를 통해 Pod IP에서 Pod IP로 직접 통신이 이뤄지는 것을 알 수 있습니다. 192.168.3.31은 두번째 EN에 있는 secondary IP입니다.

 

반면 파드의 외부 통신은 출발지는 컨테이너의 veth 인터페이스이며, 이후 ens5로 통신이 이뤄지는데 이 구간에서 노드 IP로의 SNAT이 이뤄지는 것을 확인할 수 있습니다. 독특하네요!

 

아래 CNI 제안 문서에서 제공하는 그림을 보시면 이해할 수 있습니다.

출처: https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md

 

그렇다면 3번째 ENI의 secondary IP의 파드는 ens7을 사용할까요? 그렇습니다.

 

통신을 해보면 ens7로 나갑니다.

[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni89548af8467@if3 UP             fe80::c8:88ff:fea3:5679/64
enidefacbb986c@if3 UP             fe80::2825:89ff:fec5:d261/64
eni56e598ef751@if3 UP             fe80::6ce6:79ff:fe8e:7f2c/64
eni7e0515a0713@if3 UP             fe80::40f2:24ff:fedb:164a/64
eniea2b33504a4@if3 UP             fe80::6492:f6ff:fe5d:371a/64
eni4dfe955a07f@if3 UP             fe80::c435:82ff:feae:b50b/64
ens7             UP             192.168.3.123/24 fe80::880:71ff:feec:d51b/64
[root@ip-192-168-3-141 /]# sudo tcpdump -i any -nn icmp
tcpdump: data link type LINUX_SLL2
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
16:40:48.254174 eni9e794cfd960 In  IP 192.168.3.189 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
16:40:48.254222 ens7  Out IP 192.168.3.189 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
16:40:48.257570 ens7  In  IP 192.168.2.140 > 192.168.3.189: ICMP echo reply, id 7, seq 1, length 64
16:40:48.257619 eni9e794cfd960 Out IP 192.168.2.140 > 192.168.3.189: ICMP echo reply, id 7, seq 1, length 64

 

 

파드의 외부 통신 확인

두번째 테스트는 VPC Peering된 네트워크에 있는 서버와의 통신이 어떻게 이뤄지는지 테스트 해보겠습니다.

 

 

해당 서버의 IP는 172.20.1.100 입니다.

[root@operator-host ~]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             172.20.1.100/24 fe80::3f:d8ff:fe93:8b1f/64
docker0          DOWN           172.17.0.1/16

 

EKS에서는 아래와 같은 SNAT rules에 따라 VPC Cidr(192.168.0.0/16)이 아닌 경우 SNAT을 수행합니다.

[root@ip-192-168-3-141 /]#  iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j RETURN
-A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.3.141 --random-fully

 

이때 Pod1에서 서버로 Ping을 테스트 해보겠습니다.

# kubectl exec -it $PODNAME1 -- ping -c 1 172.20.1.100
PING 172.20.1.100 (172.20.1.100) 56(84) bytes of data.
64 bytes from 172.20.1.100: icmp_seq=1 ttl=254 time=1.73 ms

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

 

이 구간에서는 SNAT가 수행되는 것을 확인할 수 있습니다.

 

다만 Pod -> 서버로는 통신이 되지만 서버 -> pod로는 통신이 되지 않습니다.

 

 

만약 서버가 있는 네트워크가 DX/VPN/TGW로 연결된 확장된 네트워크라고 가정을 해보고, 해당 VPC Cidr에 대해서도 NAT rules를 제외하는방법을 알아 보겠습니다.

 

아래와 같이 aws-node에 AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS를 환경 변수로 전달하는 방법을 사용할 수 있습니다. 이 경우 aws-node 파드들이 재시작 하는 것을 확인할 수 있습니다.

$ kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.20.0.0/16
daemonset.apps/aws-node env updated

# 파드 롤링 업데이트 수행
$ kubectl get po -A -w
NAMESPACE     NAME                              READY   STATUS            RESTARTS   AGE
default       netshoot-pod-744bd84b46-8mrkj     1/1     Running           0          36m
default       netshoot-pod-744bd84b46-lgkxf     1/1     Running           0          36m
default       netshoot-pod-744bd84b46-qzq29     1/1     Running           0          36m
default       nsenter-vg4pv8                    1/1     Running           0          83m
kube-system   aws-node-hg2mg                    0/2     PodInitializing   0          1s
kube-system   aws-node-nlqk9                    2/2     Running           0          5s
kube-system   aws-node-x5rxg                    2/2     Running           0          156m
kube-system   coredns-9b5bc9468-8prkc           1/1     Running           0          160m
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running           0          160m
kube-system   kube-proxy-d7l2t                  1/1     Running           0          156m
kube-system   kube-proxy-fp99v                  1/1     Running           0          156m
kube-system   kube-proxy-sxq5k                  1/1     Running           0          156m
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running           0          160m
kube-system   metrics-server-86bbfd75bb-xm8ds   1/1     Running           0          160m
kube-system   aws-node-hg2mg                    1/2     Running           0          2s
kube-system   aws-node-hg2mg                    2/2     Running           0          3s
kube-system   aws-node-x5rxg                    2/2     Terminating       0          156m
kube-system   aws-node-x5rxg                    0/2     Completed         0          156m
kube-system   aws-node-5qxln                    0/2     Pending           0          1s
kube-system   aws-node-5qxln                    0/2     Pending           0          1s


이제 노드에서도 확인해보면 172.20.0.0/16 에 대해서 AWS SNAT CHAIN EXCLUSION으로 추가된 것을 알 수 있습니다.

[root@ip-192-168-3-141 /]# iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 -d 172.20.0.0/16 -m comment --comment "AWS SNAT CHAIN EXCLUSION" -j RETURN
-A AWS-SNAT-CHAIN-0 -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j RETURN
-A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.3.141 --random-fully

 

다시 통신 테스트를 진행해보면 SNAT이 없이 Pod -> 서버 통신이 가능한 것으로 확인됩니다.

 

 

다만 kubectl set env daemonset aws-node -n kube-system으로 적용하는 방법은 영구적으로 적용되지 않기 때문에 아래와 같이 Addon 에 Configuration values를 추가해 줄 수 있습니다.

{
    "env": {
        "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS" : "172.20.0.0/16"
    }
}

 

 

웹 콘솔에서 EKS에서 클러스터를 선택하고, Add-ons탭에서 Amazon VPC CNI를 진입하고 Edit을 선택합니다.

Add-on configuration schema를 통해서 내용을 하고, json 구조에 맞춰 작성이 필요합니다.

 

적용을 하면 aws-node 파드들이 자동으로 재배포 됩니다.

kubectl get po -A -w
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE
kube-system   aws-node-b7qj7                    2/2     Running   0          40s
kube-system   aws-node-j77kr                    2/2     Running   0          36s
kube-system   aws-node-qtchj                    2/2     Running   0          44s

 

확인해보면 AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS 이 적용되어 있는 것을 확인할 수 있습니다.

kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
[
  {
    "name": "ADDITIONAL_ENI_TAGS",
    "value": "{}"
  },
..
  {
    "name": "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS",
    "value": "172.20.0.0/16"
  },
..

 

 

AKS에서 사용하는 ip-masq-agent의 기본 설정은 azure-ip-masq-agent-config-reconciled라는 configmap에 정보를 확인할 수 있습니다.

# Virtual Network에 대해서 nonMasqeuradeCIDRs 로 정의되어 있음
$ kubectl get cm azure-ip-masq-agent-config-reconciled -n kube-system -oyaml
apiVersion: v1
data:
  ip-masq-agent-reconciled: |-
    nonMasqueradeCIDRs:
      - 10.244.0.0/16
    masqLinkLocal: true
kind: ConfigMap
...
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    component: ip-masq-agent
    kubernetes.io/cluster-service: "true"
  name: azure-ip-masq-agent-config-reconciled
  namespace: kube-system

# 노드의 iptables 정보
root@aks-nodepool1-76251328-vmss000000:/# iptables -t nat -S |grep ip-masq-agent
-A POSTROUTING -m comment --comment "\"ip-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom IP-MASQ-AGENT chain\"" -m addrtype ! --dst-type LOCAL -j IP-MASQ-AGENT
-A IP-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -m comment --comment "ip-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE

 

 

다만 이 configmap은 addonmanager.kubernetes.io/mode: Reconcile으로 지정되어 addonManager에 의해서 관리되므로, 사용자가 임의로 변경해도 다시 Reconcile 됩니다.

 

아래와 같은 절차로 configmap을 생성하는 경우 iptables가 변경되는 것을 확인하실 수 있습니다.

$ cat config-custom.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: azure-ip-masq-agent-config
  namespace: kube-system
  labels:
    component: ip-masq-agent
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: EnsureExists
data:
  ip-masq-agent: |-
    nonMasqueradeCIDRs:
      - 172.31.1.0/24
    masqLinkLocal: false
    masqLinkLocalIPv6: true

$ kubectl apply -f config-custom.yaml
configmap/azure-ip-masq-agent-config created

# 노드의 iptables 정보 (172.31.1.0/24가 추가됨)
root@aks-nodepool1-76251328-vmss000000:/# iptables -t nat -S |grep ip-masq-agent
-A POSTROUTING -m comment --comment "\"ip-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom IP-MASQ-AGENT chain\"" -m addrtype ! --dst-type LOCAL -j IP-MASQ-AGENT
-A IP-MASQ-AGENT -d 172.31.1.0/24 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -m comment --comment "ip-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE

 

 

Azure의 공식 문서에서 가이드가 확인하기 어렵지만, 아래 깃헙 리파지터를 확인해보시면 daemonset과 동일한 네임스페이스에 configmap을 생성하면 되는 것을 알 수 있습니다.

https://github.com/Azure/ip-masq-agent-v2

 

 

마무리

EKS Networking이라는 주제로 CNI의 의미를 살펴보고 AWS VPC CNI의 특성과 EKS에서 인스턴스 타입에 따른 파드 생성 제한에 대해서 확인해 보았습니다.

이후 EKS 클러스터를 생성해보고, 앞서 확인한 정보를 바탕으로 노드와 파드 정보를 확인하는 실습을 다뤄봤습니다.

한편으로 EKS는 단일 VPC CNI만 제공하고 있는 반면 AKS에서는 다양한 CNI를 제공하는 차이점이 있었습니다.

 

다음 포스트에는 EKS Networking 두번째 주제로, Loadbalancer 유형의 서비스와 Ingress를 EKS에서 어떻게 구현하는 지 살펴보겠습니다.

'EKS' 카테고리의 다른 글

[2-2] EKS Networking Part2 - LoadBalancer와 Ingress  (0) 2025.02.16
[1] EKS 생성과 리소스 살펴보기  (2) 2025.02.07

Amazon Elastic Kubernetes Service(이하 EKS)를 설치해보고 EKS가 어떻게 구성되는지 살펴보겠습니다.

개인적으로 Azure를 자주 사용하기 때문에 Azure Kubernetes Service(이하 AKS)와 어떤 차이가 있는 지도 간략하게 설명하겠습니다.

목차

  1. 환경 개요
  2. EKS 생성하기
  3. 웹 콘솔 살펴보기
  4. 주요 컴포넌트 비교
  5. 노드 비교
  6. 리소스 정리

 

 

1. 환경 개요

Windows 11의 WSL(Windows Subsystem for Linux) 환경에서 실습을 진행합니다. 버전 정보는 아래를 참고 부탁드립니다.

> wsl --version
WSL 버전: 2.3.26.0
커널 버전: 5.15.167.4-1
WSLg 버전: 1.0.65
MSRDC 버전: 1.2.5620
Direct3D 버전: 1.611.1-81528511
DXCore 버전: 10.0.26100.1-240331-1435.ge-release
Windows 버전: 10.0.22631.4751
> wsl --status
기본 배포: Ubuntu
기본 버전: 2
> wsl --list
Linux용 Windows 하위 시스템 배포:
Ubuntu(기본값)

참고로 Windows 11환경에서 WSL 을 구성하는 것은 아래를 참고하시면 됩니다.

https://learn.microsoft.com/ko-kr/windows/wsl/install

오래되서 잘 기억나지 않지만, 저는 예전에 wsl을 설치를 해서 아래와 같은 방식을 사용 했던 것 같기도 합니다.

https://wikidocs.net/219899

 

2. EKS 생성하기

EKS의 배포 방식에는 AWS 콘솔을 이용한 방식, eksctl을 이용한 방식, IaC 도구(terraform 등)를 이용한 방식이 있습니다.

eksctl를 통해서 클러스터를 생성하는 것이 가장 간단하지만, 내부적으로 CloudFormation을 이용하여 EKS클러스터와 노드그룹 2가지 stack을 만들어 생성을 진행하는 방식이라 속도가 느립니다. 반면 terraform을 사용하는 경우는 AWS API로 직접 생성을 요청하여 eksctl 보다는 빠르다고 합니다.

 

해당 포스트에서는 eksctl 을 통해서 클러스터를 구성하려고 합니다.

이를 위해서 기본적으로 AWS CLI, eksctl, kubectl과 같은 도구를 먼저 설치하겠습니다.

https://docs.aws.amazon.com/eks/latest/userguide/setting-up.html

 

AWS CLI 설치

AWS에서 제공하는 가이드에 따라 AWS CLI를 설치합니다.

https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

*참고로 wsl 기본적으로 unzip이 없어서 apt install unzip -y 를 먼저 수행해야 합니다.

아래와 같이 나오면 정상입니다.

# sudo ./aws/install
You can now run: /usr/local/bin/aws --version
# aws --version
aws-cli/2.23.13 Python/3.12.6 Linux/5.15.167.4-microsoft-standard-WSL2 exe/x86_64.ubuntu.20

또한 aws configure를 수행해 필요한 정보를 입력해야만 이후 eksctl 사용이 가능합니다.

 

kubectl 설치

이미 kubernetes를 자주 사용하기 때문에 언젠가 설치한 kubectl이 설치되어 있어 이 부분은 생략하겠습니다.

아래에서 알맞은 OS 유형을 바탕으로 설치를 진행하시면 됩니다.

https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html#_step_2_install_or_update_kubectl

 

eksctl 설치

마지막으로 eksctl을 설치합니다.

https://eksctl.io/installation/#for-unix

# for ARM systems, set ARCH to: `arm64`, `armv6` or `armv7`
ARCH=amd64
PLATFORM=$(uname -s)_$ARCH

curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz"

# (Optional) Verify checksum
curl -sL "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check

tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz

sudo mv /tmp/eksctl /usr/local/bin

이런식으로 결과가 나오면 됩니다.

# eksctl version
0.203.0

 

EKS 클러스터 생성

eksctl을 통해 클러스터를 생성하고 삭제하는 가이드는 아래를 참고할 수 있습니다.

https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html

## 생성
eksctl create cluster --name my-cluster --region region-code

## 삭제
eksctl delete cluster --name my-cluster --region region-code

참고: region-code

https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/using-regions-availability-zones.html

 

아래 블로그를 보면 다양한 옵션을 통해 클러스터를 생성할 수 있음을 알 수 있습니다.

https://learnk8s.io/terraform-eks#three-popular-options-to-provision-an-eks-cluster

예를 들어, Node type, Node 개수, 노드 min/max(Cluster Autoscaler)를 조정할 수 있습니다.

eksctl create cluster \
  --name learnk8s-cluster \
  --node-type t2.micro \
  --nodes 3 \
  --nodes-min 3 \
  --nodes-max 5 \
  --region eu-central-1

 

이를 보다 규격화 해서 아래와 같은 형태로 cluster.yaml 파일을 생성하고 eksctl create cluster -f cluster.yaml와 같이 실행하는 방법도 있습니다.

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: learnk8s
  region: eu-central-1
nodeGroups:
  - name: worker-group
    instanceType: t2.micro
    desiredCapacity: 3
    minSize: 3
    maxSize: 5

간단한 테스트를 위한 목적이라면 eksctl 에 옵션을 추가하는 방식으로 사용할 수 있겠지만, yaml에 환경 구성을 저장해 놓고 재활용할 수 있는 장점이 있을 것 같습니다.

 

본 포스트에서는 테스트 목적이므로 eksctl 에 옵션을 추가하여 클러스터를 생성하도록 하겠습니다.

AWS_DEFAULT_REGION=ap-northeast-2
CLUSTER_NAME=myeks1

eksctl create cluster \
  --name $CLUSTER_NAME \
  --region $AWS_DEFAULT_REGION \
  --nodegroup-name ${CLUSTER_NAME}-nodegroup \
  --node-type t3.medium \
  --node-volume-size=30 \
  --version 1.31 \
  --ssh-access \
  --external-dns-access \
  --verbose 4

참고로 기존에 생성된 ssh key(~/.ssh/id_rsa.pub)를 활용하므로, 생성된 ssh key가 없는 경우에는 ssh key-gen을 미리 수행하시면 됩니다.

ssh-keygen -t rsa -b 4096

eksctl 수행 로그를 바탕으로 진행과정을 살펴보겠습니다.

## 이와 같은 로그로 시작합니다.
2025-02-06 21:52:29 [ℹ]  eksctl version 0.203.0
## eksctl에서 클러스터와 managed nodegroup을 위해 2개의 CloudFormation stacks을 만드는 것을 알 수 있습니다.
2025-02-06 21:52:29 [ℹ]  will create 2 separate CloudFormation stacks for cluster itself and the initial managed nodegroup
## eksctl-myeks1-cluster를 stack을 배포합니다.
2025-02-06 21:52:31 [ℹ]  deploying stack "eksctl-myeks1-cluster"
## 어느정도 시간이 지나면 myeks1의 컨트롤 플레인이 생성이 완료된 것을 알 수 있습니다.
2025-02-06 22:00:32 [▶]  completed task: create cluster control plane "myeks1"
## Addon을 설치합니다. 이 과정에서 metrics-server, kube-proxy, vpc-cni, coredns 와 같은 컴포넌트 들이 설치됩니다.
2025-02-06 22:00:33 [ℹ]  creating addon
2025-02-06 22:00:33 [▶]  addon: &{metrics-server v0.7.2-eksbuild.1  [] map[]  {false false false false false false false} map[]  <nil> false  true [] [] []}
..
## 노드 그룹에 대한 stack 배포를 시작합니다.
2025-02-06 22:02:37 [ℹ]  deploying stack "eksctl-myeks1-nodegroup-myeks1-nodegroup"
## 노드 그룹 생성이 완료 되었습니다.
2025-02-06 22:05:13 [✔]  created 1 managed nodegroup(s) in cluster "myeks1"
## kubeconfig 를 저장합니다.
2025-02-06 22:05:13 [✔]  saved kubeconfig as "/root/.kube/config"
## myeks1 클러스터의 생성이 종료 되었습니다.
2025-02-06 22:05:15 [✔]  EKS cluster "myeks1" in "ap-northeast-2" region is ready

대략 생성 요청 이후 15분 내에 완료가 되었습니다. terraform으로 수행하는 경우는 시간이 더 단축될 수 있으므로 이후 다시 확인을 해봐야할 것 같습니다

 

AKS의 경우는 보통 5분 내에 생성이 완료되어 생성 시간 자체는 AKS가 빠른 느낌입니다.

또한 수행 과정을 살펴보면 kubeconfig를 직접 설정해 주는 것을 알 수 있습니다. 이로써 eksctl로 클러스터를 생성하면 바로 kubectl 을 사용할 수 있습니다.

반면, AKS에서는 클러스터 생성 후 az aks get-credentials 와 같은 명령어로 kubeconfig를 가져올 수 있습니다.

 

(업데이트_2025-2-11) EKS에서도 aws eks update-kubeconfig 로 추후 kubeconfig를 가져올 수 있습니다.

 

3. 웹 콘솔 살펴보기

EKS를 생성할 때 기본적으로 생성되는 리소스를 확인하기 위해서 웹 콘솔을 접근 해봅니다.

앞서 설명한 바와 같이 eksctl은 CloudFormation을 사용하는데 이를 웹 콘솔에서 확인하실 수 있습니다.

생성된 EKS로 들어와서 정보를 확인해 봅니다.

현재 버전과 언제까지 유효한지, 그리고 클러스터 상태와 업그레이드 인사이트(업그레이드에 충족한 상태인지 검증하는 기능으로 보임), 노드 상태 문제를 한 눈에 볼 수 있는 점이 인상적입니다.

클러스터 정보 아래에 탭으로 다양한 클러스터 정보를 확인해볼 수 있습니다. 이 중 몇가지를 살펴보겠습니다.

 

 

[개요] 탭에서 API Server 엔드포인트 정보를 확인할 수 있습니다. (혹시나 보안 이슈를 우려하여 실제 정보는 캡처하지 않았습니다)

EKS의 API Server 엔드포인트를 확인하며 신기한 점은 이 엔드포인트에 대해서 dns query를 하면 2개의 IP가 응답을 합니다. API Server가 2개가 있다는 의미는 아니고, NLB에서 2개의 IP를 응답해 주는 것이라고 합니다.

# dig +short xxx.gr7.ap-northeast-2.eks.amazonaws.com
3.35.116.125
3.35.119.23

노드에 진입하여 확인해 볼 때도 kubelet과 kube-proxy가 각 다른 API Server 엔드포인트로 연결한 것을 알 수 있습니다.

# kubectl get no
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-32-233.ap-northeast-2.compute.internal   Ready    <none>   24m   v1.31.4-eks-aeac579
ip-192-168-67-87.ap-northeast-2.compute.internal    Ready    <none>   24m   v1.31.4-eks-aeac579
# kubectl node-shell ip-192-168-32-233.ap-northeast-2.compute.internal
[root@ip-192-168-32-233 /]# ss -tnp | egrep "kubelet|kube-proxy"
ESTAB 0      0               192.168.32.233:59018              3.35.119.23:443   users:(("kubelet",pid=2901,fd=20))
..
ESTAB 0      0               192.168.32.233:51418             3.35.116.125:443   users:(("kube-proxy",pid=3122,fd=9))

 

 

그리고 [컴퓨팅] 탭을 확인해보면 노드그룹이 EC2 Auto Scaling Group을 통해서 제공되며, 2개의 노드가 생성된 것을 알 수 있습니다.

 

[네트워킹] 탭을 보면 노드가 배포된 VPN와 서브넷 정보를 확인할 수 있습니다.

여기서 중요한 부분은 API 서버 엔드포인트 엑세스퍼블릭 액세스 소스 허용 목록입니다.

API 서버 엔드포인드 엑세스는 EKS에서 API 서버를 퍼블릭 혹은 프라이빗으로 제공하는지 여부이고, 퍼블릭인 경우 퍼블릭 엑세스 소스 허용 목록에서 접근을 허용할 IP를 등록해 줄 수 있습니다.

퍼블릭으로 설정된 클러스터에서는 최소한 접근 허용 목록을 지정하는 것이 보안적으로 안정적입니다.

 

EKS는 API 서버 엔드포인트를 퍼브릭/퍼블릭 및 프라이빗/프라이빗 세 가지 유형으로 제공하고 있습니다. 여기서 상단의 엔드포인트 엑세스 관리를 눌러보면 상세한 정보를 확인하실 수 있습니다.

 

이를 그림으로 살펴보면 더 쉽게 이해할 수 있습니다.

먼저 Public을 살펴보면 kubectl과 같이 API 서버를 접근할 때 퍼블릭 엔드포인트로 접근하게 되며 또한 워커 노드의 접근도 퍼블릭 엔드포인트로 접근합니다.

출처: https://www.youtube.com/watch?v=bksogA-WXv8&t=5s

 

두번째 유형은 Public and Private 형태입니다.

kubectl과 같이 API 서버를 접근할 때 퍼블릭 엔드포인트로 접근하게 되는 것은 동일합니다. 반면 워커 노드는 API Server에 대한 dns query에 대해서 Route 53의 Private Hosted Zone을 통해 EKS owned ENI으로 응답을 받게되어 프라이빗한 연결을 제공합니다.

출처: https://www.youtube.com/watch?v=bksogA-WXv8&t=5s

 

세번째 유형은 Private 입니다.

이 구성에서는 API Server 엔드포인트를 퍼블릭으로 노출하지 않기 때문에 kubectl 과 같은 접근과 노드의 접근 모두가 EKS Owned ENI를 통해서 이뤄 집니다.

보다 자세한 내용은 아래의 문서를 참고하시기 바랍니다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/cluster-endpoint.html

 

이러한 API 서버 엔드포인트에 대한 보안 강화는 AKS에서도 유사하게 구현되어 있습니다.

먼저 API 서버 엔드포인트 Public을 Public Cluster로 생각할 수 있으며, Authorized IP range(https://learn.microsoft.com/ko-kr/azure/aks/api-server-authorized-ip-ranges?tabs=azure-cli)를 통해 API 서버로의 접근을 제한할 수 있습니다.

또한 API 서버 엔드포인트 Private 에 대응하는 Private Cluster(https://learn.microsoft.com/en-us/azure/aks/private-clusters?tabs=default-basic-networking%2Cazure-portal)를 구성하는 방식을 제공하고 있습니다.

 

 

다만 EKS에서 설명하는 API 서버 엔드포인트 Private에서 다른 AWS 서비스를 접근하는 경우에도 VPC Endpoint가 필요하다는 언급이 있는 것을 볼 때, EKS의 Private의 의미는 워커 노드 관점에서 외부로의 접근을 Isolated하겠다는 중의적인 표현이 있는 것 같습니다.

 

 

반대로 AKS의 Private cluster는 단순히 API 서버를 Private 하게 제공한다는 의미만 담고 있습니다. 다른 서비스로의 접근은 해당 서비스에서 Private endpoint 방식으로 구성을 하거나 해야하고, 또한 워커 노드의 환경 자체를 Isolated하기 위해서는 UDR(User-defined Routing)을 통해 Firewall을 이용하는 방식을 취할 수 있습니다.

한편 AKS에서도 최근 Network Isolated Cluster를 Public Preview 로 제공하고 있습니다. 이는 워커 노드 환경에서 외부 접근을 Isolated 하는 방식을 제공합니다.

https://learn.microsoft.com/en-us/azure/aks/concepts-network-isolated

 

 

마지막으로 [추가 기능] 탭에서는 AddOn을 확인해볼 수 있습니다.

 

주요 컴포넌트 비교 절에서 확인하겠지만 AKS는 kube-proxy, cordns, metrics-server 와 같은 컴포넌트를 시스템 컴포넌트 이야기하고 미리 설치되어 제공됩니다. 또한 이러한 컴포넌트는 보통 kubernetes 버전과 상응하여 자동으로 업그레이드 되는 방식을 취하고 있습니다.

EKS에서 확인을 해보면 이러한 컴포넌트들이 개별 AddOn으로 인식되며, 또한 개별 컴포넌트에 대해서 버전 업데이트가 가능한 점에 차이가 있습니다.

추가 기능 가져오기를 눌러보면 CSI Driver 와 같은 형태의 컴포넌트들도 AddOn으로 추가가 가능한 것을 알 수 있으며, 3rd Party에서 제공하는 AddOn도 AWS 마켓플레이스를 통해서 설치 가능한 것으로 보입니다.

 

Kubernetes 버전 별로 제공되는 AddOn에 차이가 있을 수 있으므로 아래의 명령을 통해서 확인하시기 바랍니다.

aws eks describe-addon-versions --kubernetes-version 1.31  --query 'addons[].{MarketplaceProductUrl: marketplaceInformation.productUrl, Name: addonName, Owner: owner Publisher: publisher, Type: type}' --output table

 

 

웹 콘솔에서 EKS 화면을 떠나 노드 중 한대를 확인해보겠습니다.

 

Private IP가 2개가 있습니다. 노드에서도 확인해보면 eth0, eth1 두개의 인터페이스가 있는 것을 알 수 있습니다. (아직 AWS를 잘 몰라서 왜 2개인지는 잘 모르겠네요..)

[root@ip-192-168-32-233 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             192.168.32.233/19 fe80::8f9:d9ff:febf:d407/64
enia4cb1502028@if3 UP             fe80::e0bd:c3ff:feec:294c/64
enifc2da76e4e6@if3 UP             fe80::88e6:29ff:fe17:10f8/64
eth1             UP             192.168.34.104/19 fe80::864:31ff:fea4:c503/64

 

(업데이트_2025-2-11)
AWS의 Instance는 Instance type마다 할당 가능한 ENI 개수와 ENI별 할당 가능한 IP개수가 정해져 있습니다. 또한 파드에서 필요한 IP를 미리 할당해 Warm Pool 개념으로 로컬에 일부 가지고 있습니다. 이때 1개의 ENI로 할당가능한 IP가 부족하면 자동으로 추가 ENI 를 연결합니다.

 

 

Public IP도 하나 할당되어 있습니다. 노드에 할당된 파드에 진입해보니 해당 Public IP로 통신을 하는 것을 알 수 있습니다.

# kubectl get po -owide
NAME                        READY   STATUS              RESTARTS   AGE   IP               NODE                                                NOMINATED NODE   READINESS GATES
nettools-65789c8677-5l444   0/1     ContainerCreating   0          3s    <none>           ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>
nettools-65789c8677-j9dzp   1/1     Running             0          29s   192.168.86.177   ip-192-168-67-87.ap-northeast-2.compute.internal    <none>           <none>
nsenter-tpremx              1/1     Running             0          72m   192.168.32.233   ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>
# kubectl exec -it nettools-65789c8677-5l444 -- bash
[root@nettools-65789c8677-5l444 /]# curl ifconfig.me
52.79.138.69

 

아마 노드들이 Public Subnet을 사용하기 때문에 이런 구성이 된 것일 수도 있고, 혹은 컨셉이 다른 것일 수 있습니다만, AKS에서는 Instance Level Public IP(https://learn.microsoft.com/en-us/azure/aks/use-node-public-ips)를 별도로 지정하지 않는 이상 노드들은 Private IP로만 구성됩니다.

노드(파드)의 외부 통신의 방식은 클러스터의 Outbound-type을 통해서 지정되며 상세한 내용은 아래의 문서를 참고하실 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/egress-outboundtype

 

 

해당 인스턴스의 네트워크 인터페이스를 살펴보면 다수의 secondary IP가 할당된 것을 알 수 있습니다.

 

AWS는 VPC CNI로 파드의 IP Range가 VPC의 IP와 동일한 IP 대역을 사용합니다. 해당 노드에 배포된 파드들을 살펴보면 host network를 사용하지 않는 파드들이 사용하는 IP로 보입니다.

# kubectl get po -A -owide |grep ip-192-168-32-233
default       nettools-65789c8677-5l444         1/1     Running   0          8m52s   192.168.47.31    ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>
default       nsenter-tpremx                    1/1     Running   0          81m     192.168.32.233   ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>
kube-system   aws-node-fgrrx                    2/2     Running   0          106m    192.168.32.233   ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-dvv5c           1/1     Running   0          110m    192.168.49.120   ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>
kube-system   kube-proxy-xpnsb                  1/1     Running   0          106m    192.168.32.233   ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>
kube-system   metrics-server-86bbfd75bb-wmk4d   1/1     Running   0          110m    192.168.42.2     ip-192-168-32-233.ap-northeast-2.compute.internal   <none>           <none>

 

마지막으로 노드의 네트워크 인터페이스와 EKS Owned ENI를 비교해 보면, EKS Owned ENI에 연결된 Instance owner를 확인해보실 필요가 있습니다. 이 것은 ENI에 연결된 인스턴스 정보인데, 확인해보면 ENI 소유자와 다른 것을 알 수 있습니다. 즉 다른 소유자에서 생성한 것이라는 것을 알 수 있습니다. 

 

이를 Cross-Account ENI 라고 하며, 아래와 같은 다른 형태로 구성됩니다.

출처: https://www.youtube.com/watch?app=desktop&v=zGs13xbRBMg

 

노드에서 EKS Owned ENI를 확인할 수 있는 방법이 있습니다.

kubectl exec나 kubectl logs와 같은 명령은 API 서버로 요청을 하여 노드에 실행 중인 파드나 로그를 보여줍니다. 그렇기 때문에 이 명령을 수행하는 순간은 API 서버에서 노드로의 접근으로 이뤄집니다.

아래와 같이 사전에 ss -tnp를 수행하고, 세션1에서 kubectl exec를 수행한 뒤, 다시 ss -tnp를 수행해보면 EKS Owned ENI를 통한 연결이 확인됩니다.

 

참고로 AKS에서는 API 서버와 노드의 연결을 konnectivity를 통해 제공한다는 차이가 있습니다.

 

 

4. 주요 컴포넌트 비교

앞서 생성한 EKS에서 배포된 파드를 바탕으로 기본 컴포넌트를 살펴보겠습니다.

# kubectl get po -A
NAMESPACE     NAME                              READY   STATUS        RESTARTS   AGE
kube-system   aws-node-6mm9l                    2/2     Running       0          119m
kube-system   aws-node-fgrrx                    2/2     Running       0          119m
kube-system   coredns-9b5bc9468-dvv5c           1/1     Running       0          123m
kube-system   coredns-9b5bc9468-pzm47           1/1     Running       0          123m
kube-system   kube-proxy-4ml68                  1/1     Running       0          119m
kube-system   kube-proxy-xpnsb                  1/1     Running       0          119m
kube-system   metrics-server-86bbfd75bb-j888c   1/1     Running       0          123m
kube-system   metrics-server-86bbfd75bb-wmk4d   1/1     Running       0          123m

metrics-server, coredns, kube-proxy 와 같은 컴포넌트가 있고, aws-node가 있습니다.

aws-node는 이미지를 살펴보면 AWS CNI와 Netowrk Policy Agent에 해당하는 컨테이너로 이루어져 있다는 걸 알 수 있습니다.

      image: 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon-k8s-cni-init:v1.19.0-eksbuild.1
      image: 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon/aws-network-policy-agent:v1.1.5-eksbuild.1

 

상당히 간결한 느낌입니다. 한편으로 필요한 기능이 있을 때 AddOn을 상당히 설치해야 한다는 의미이기도 합니다. 

4GiB 인스턴스에서 대략 550Gib 정도를 kube-reserved로 사용하는 걸로 보입니다. 실행 파드가 적고, limit 설정도 안된 파드가 있어서 Allocated resources가 여유가 있습니다.

Capacity:
  cpu:                2
  ephemeral-storage:  31444972Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             3943300Ki
  pods:               17
Allocatable:
  cpu:                1930m
  ephemeral-storage:  27905944324
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             3388292Ki
  pods:               17
...
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests    Limits
  --------           --------    ------
  cpu                350m (18%)  0 (0%)
  memory             270Mi (8%)  570Mi (17%)
  ephemeral-storage  0 (0%)      0 (0%)
  hugepages-1Gi      0 (0%)      0 (0%)
  hugepages-2Mi      0 (0%)      0 (0%)

 

비교를 위해 AKS를 기본 생성하고, 배포된 컴포넌트를 살펴보겠습니다.

az group create --name aks-rg --location eastus
az aks create \
    --resource-group aks-rg \
    --name myaks1 \
    --node-count 2 

 

상대적으로 많은 컴포넌트가 설치되어 있는 것을 알 수 있습니다.

$ az aks get-credentials -g aks-rg -n myaks1
Merged "myaks1" as current context in /home/xx/.kube/config
$ kubectl get po -A
NAMESPACE     NAME                                  READY   STATUS    RESTARTS   AGE
kube-system   azure-cns-65cr5                       1/1     Running   0          4m29s
kube-system   azure-cns-zc944                       1/1     Running   0          4m37s
kube-system   azure-ip-masq-agent-7ccvx             1/1     Running   0          4m37s
kube-system   azure-ip-masq-agent-fjkz5             1/1     Running   0          4m29s
kube-system   cloud-node-manager-jg2p7              1/1     Running   0          4m37s
kube-system   cloud-node-manager-rjh58              1/1     Running   0          4m29s
kube-system   coredns-54b69f46b8-fsgqh              1/1     Running   0          4m15s
kube-system   coredns-54b69f46b8-qsqnr              1/1     Running   0          5m13s
kube-system   coredns-autoscaler-bfcb7c74c-dk26h    1/1     Running   0          5m13s
kube-system   csi-azuredisk-node-hcmcn              3/3     Running   0          4m37s
kube-system   csi-azuredisk-node-hzp7x              3/3     Running   0          4m29s
kube-system   csi-azurefile-node-clzjj              3/3     Running   0          4m37s
kube-system   csi-azurefile-node-gt9mj              3/3     Running   0          4m29s
kube-system   konnectivity-agent-546bc6d8dc-5d9xs   1/1     Running   0          15s
kube-system   konnectivity-agent-546bc6d8dc-j8pzd   1/1     Running   0          12s
kube-system   kube-proxy-vs88d                      1/1     Running   0          4m29s
kube-system   kube-proxy-wtqth                      1/1     Running   0          4m37s
kube-system   metrics-server-7d95c7bd8d-rklw7       2/2     Running   0          4m7s
kube-system   metrics-server-7d95c7bd8d-xsgn4       2/2     Running   0          4m7s

 

상대적으로 많은 파드들이 실행되어 Allocated resources를 많이 사용 중입니다. 이 때문에 AKS에서는 시스템 컴포넌트들과 사용자 워크로드를 분리하도록 권장하고 있습니다. 시스템 노드풀과 사용자 노드풀을 분리하는 방식을 취할 수 있습니다. 다만 이 경우에도 daemonset 유형의 파드(ex. CSI driver)들은 전체 노드에도 동일하게 생성됩니다.

 

Capacity:
  cpu:                2
  ephemeral-storage:  129886128Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             7097596Ki
  pods:               250
Allocatable:
  cpu:                1900m
  ephemeral-storage:  119703055367
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             5160188Ki
  pods:               250
 ...
  Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
  --------           --------     ------
  cpu                682m (35%)   2042m (107%)
  memory             766Mi (15%)  4252Mi (84%)
  ephemeral-storage  0 (0%)       0 (0%)
  hugepages-1Gi      0 (0%)       0 (0%)
  hugepages-2Mi      0 (0%)       0 (0%)

 

7GiB 노드에서 1.9GiB 정도가 kube-reserved 로 예약되어 있습니다. AKS에서는 1.29 이상에서 kube-reserved 매커니즘에 상당한 개선이 있었습니다. 다만 default로 생성된 노드의 max-pods 가 250으로 설정되어 이 부분이 kube-reserved로 반영된 영향일 수 있습니다.


상세한 내용은 아래 문서를 참고할 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/node-resource-reservations#memory-reservations

 

 

특이한 점은 AWS의 AddOn들은 CPU limit을 지정하지 않고 있습니다. (자신감일까요..?)

 

 

5. 노드 구성 비교

노드 관점에서 정보를 비교해 보겠습니다.

EKS의 노드입니다. Amazon Linux 2를 사용하고 있고 containerd를 사용한다는 것을 알 수 있습니다. 인스턴스에 Public IP가 구성되어 있고, 아래 정보에서 EXTERNAL-IP가 표시되어 있습니다.

# kubectl get no -owide
NAME                                                STATUS   ROLES    AGE    VERSION               INTERNAL-IP      EXTERNAL-IP     OS-IMAGE         KERNEL-VERSION                  CONTAINER-RUNTIME
ip-192-168-32-233.ap-northeast-2.compute.internal   Ready    <none>   137m   v1.31.4-eks-aeac579   192.168.32.233   52.79.138.69    Amazon Linux 2   5.10.233-223.887.amzn2.x86_64   containerd://1.7.25
ip-192-168-67-87.ap-northeast-2.compute.internal    Ready    <none>   137m   v1.31.4-eks-aeac579   192.168.67.87    15.165.15.152   Amazon Linux 2   5.10.233-223.887.amzn2.x86_64   containerd://1.7.25

 

AKS의 노드입니다. Ubuntu 22.0.4.5 를 사용하고 containerd를 사용합니다. 노드들은 default에서 Private IP만 구성되어 EXTERNAL-IP가 <none>으로 표시됩니다.

$ kubectl get no -owide
NAME                                STATUS   ROLES    AGE     VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
aks-nodepool1-21558960-vmss000000   Ready    <none>   4m19s   v1.30.7   10.224.0.4    <none>        Ubuntu 22.04.5 LTS   5.15.0-1079-azure   containerd://1.7.25-1
aks-nodepool1-21558960-vmss000001   Ready    <none>   4m27s   v1.30.7   10.224.0.5    <none>        Ubuntu 22.04.5 LTS   5.15.0-1079-azure   containerd://1.7.25-1

 

EKS 노드에 진입하여 일반적인 구성을 살펴보겠습니다.

[root@ip-192-168-32-233 /]# hostname
ip-192-168-32-233.ap-northeast-2.compute.internal
[root@ip-192-168-32-233 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             192.168.32.233/19 fe80::8f9:d9ff:febf:d407/64
enia4cb1502028@if3 UP             fe80::e0bd:c3ff:feec:294c/64
enifc2da76e4e6@if3 UP             fe80::88e6:29ff:fe17:10f8/64
eth1             UP             192.168.34.104/19 fe80::864:31ff:fea4:c503/64
[root@ip-192-168-32-233 /]# stat -fc %T /sys/fs/cgroup/
tmpfs

 

AKS는 1.25 버전에서 cgroupv2를 default로 사용하고 있는 점에 차이가 있습니다.

root@aks-nodepool1-21558960-vmss000000:/# hostname
aks-nodepool1-21558960-vmss000000
root@aks-nodepool1-21558960-vmss000000:/# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128 
eth0             UP             10.224.0.4/16 metric 100 fe80::6245:bdff:fea7:ab7a/64 
enP40656s1       UP             
azvae85acd5c66@if4 UP             fe80::a8aa:aaff:feaa:aaaa/64 
azve6bd6a1fcff@if6 UP             fe80::a8aa:aaff:feaa:aaaa/64 
azv61ea7670b68@if8 UP             fe80::a8aa:aaff:feaa:aaaa/64 
root@aks-nodepool1-21558960-vmss000000:/# stat -fc %T /sys/fs/cgroup/
cgroup2fs

 

cgroupv2로 변경을 하는 경우, 애플리케이션 프레임워크에서 cgroupv2를 인지하지 못하는 상황에서 메모리 리포팅 버그나 OOM과 같은 이슈가 발생할 수 있습니다. 이러한 리스크로 인해서 아직 cgroupv2으로 전환을 하지 않은 것인지 분명하지는 않습니다.

 

(업데이트_2025-2-11)
현 시점 EKS의 defualt 노드 타입은 amazon linux2 입니다. azure linux2는 2026-06-30 에 EOL 될 예정으로 이로 인해서 기능 개선이 없는 것이 아닐까 생각됩니다. amazon linux 2023의 경우 cgroupv2로 설정되어 있습니다.

 

 

일단 현 시점에는 노드 수준의 큰 차이점을 확인하기 어려워 기본적인 정보만 보는 수준에서 실습을 마무리 하겠습니다.

 

 

6. 리소스 정리

실습을 마무리하고 eksctl로 생성한 클러스터를 삭제합니다.

eksctl delete cluster --name $CLUSTER_NAME

 

 

마무리

해당 포스트는 AEWS(AWS EKS Workshop Study) 3기를 참여하면서 과제로 작성을 시작했습니다.

 

이번 주는 개인 환경을 세팅하고 EKS를 설치해 EKS의 기본적인 아키텍처와 구성 정보를 확인해봤습니다. 그 과정에서 AKS와 다양한 관점에서 비교를 해봤습니다.

한편으로 AWS를 사용할 일이 없었는데, 이번 과정을 통해서 AWS 콘솔이 어떤 식으로 정보를 노출하는지, CLI를 사용하는 방식 등을 이해할 수 있었습니다.

 

매주 다양한 주제에 대해서 EKS를 스터디할 계획이라, 각 주제별로 Azure의 구성이나 관점에 어떤 차이가 있는지 비교해 보도록 하겠습니다.

보통 블로그를 포스팅 할 때 markdown editor를 사용해서 글을 작성하고, 이후에 블로그의 글쓰기에 붙여 넣는 방식을 사용했습니다.

markdown editor를 사용하면 글을 정리하기도 편하고 이미지도 바로 복/붙이 되어서 편한 부분이 있습니다.

예전에는 typora라는 markdown editor를 사용했는데 이후에 유료로 전환을 한 것 같습니다.

 

확인해보니 VS Code도 markdown editor를 사용 가능한 걸로 확인해서 간단히 소개합니다.

 

먼저 VS Code를 켜고 폴더를 열어, .md 확장자로 내용을 작성합니다. 다행이 이미지 복/붙을 하면 해당 폴더에 저장하는 방식으로 지원이 됩니다.

 

그리고 작성된 글을 확인하고자 할 때, 우측 상단의 'Open Preview to the Side'를 누릅니다.

VS Code for markdown editor #1

 

아래와 같이 미리 보기로 확인이 가능합니다. 코드 처리가 제대로 안되는거 같긴 합니다만..

VS Code for markdown editor #2

 

사실 typora 방식이 편하긴 하지만 VS Code로 무료로 쓸 수 있다는 장점이 있습니다.

'기타' 카테고리의 다른 글

VS Code에서 REST 테스트 하기  (0) 2023.11.05
wsl: docker, kind 설치  (0) 2023.11.05
curl 에 timeout 주기  (0) 2023.10.04
개행문자(\n)를 줄바꿈으로 변환하기  (0) 2022.03.19
Browser IDE, code-server 사용해보기  (0) 2022.03.18

이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 31장 써머리입니다.

이 책의 마지막 스토디 노트입니다.

Todo 리스트 웹 서비스 만들기

Todo 리스트 웹 서비스는 프론트 엔드 코드와 백엔드 코드로 나눠진다.

프론트 엔드는 웹서비스의 화면을 담당하고, 백엔드는 데이터와 로직을 담당한다.

구현 순서

  1. 먼저 RESTful API에 맞춰 서비스를 정의한다.
  2. Todo 구조체를 만든다.
  3. RESTful API에 맞춰 각 핸들러를 만든다.
  4. 화면을 구성하는 HTML 문서를 만든다.
  5. 프론트엔드 동작을 나타내는 자바스크립트 코드를 만든다.
  6. 웹 브라우저로 동작을 확인한다.

시작 하기 전에 웹서버를 만들기 앞서 gorilla/mux 외 두 가지 패키지를 더 설치한다.

  • urfave/negroni 패키지: 자주 사용되는 웹 핸들러를 제공하는 패키지이다. 추가로 로그 기능, panic 복구 기능, 파일 서버 기능을 제공한다.
  • unrolled/render 패키지: 웹 서버 응답으로 HTML, JSON, TEXT 같은 포맷을 간단히 사용할 수 있다.
$ go mod init goprojects/todo31
$ go get github.com/gorilla/mux
$ go get github.com/urfave/negroni
$ go get github.com/unrolled/render

 

이제 백엔드의 RESTful API를 아래와 같이 작성한다.

// ch31/ex31.1/ex31.1.go
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"sort"
	"strconv"

	"github.com/gorilla/mux"
	"github.com/unrolled/render"
	"github.com/urfave/negroni"
)

var rd *render.Render

type Todo struct { // 할 일 정보를 담는 Todo 구조체
	ID        int    `json:"id,omitempty"` // json 포맷으로 변환 옵션 -> JSON 포맷으로 변환시 ID가 아닌 id로 변환됨
	Name      string `json:"name"`
	Completed bool   `json:"completed,omitempty"`
}

var todoMap map[int]Todo
var lastID int = 0

func MakeWebHandler() http.Handler { // 웹 서버 핸들러 생성
	rd = render.New()
	todoMap = make(map[int]Todo)
	mux := mux.NewRouter()
	mux.Handle("/", http.FileServer(http.Dir("public"))) // "/"" 경로에 요청이 들어올 때 public 아래 폴더의 파일을 제공하는 파일 서버
	// "/todos" 에 대해서 GET, POST, DELETE, PUT에 대한 핸들러 구현
	mux.HandleFunc("/todos", GetTodoListHandler).Methods("GET")
	mux.HandleFunc("/todos", PostTodoHandler).Methods("POST")
	mux.HandleFunc("/todos/{id:[0-9]+}", RemoveTodoHandler).Methods("DELETE")
	mux.HandleFunc("/todos/{id:[0-9]+}", UpdateTodoHandler).Methods("PUT")
	return mux
}

type Todos []Todo // ID로 정렬하는 인터페이스

func (t Todos) Len() int {
	return len(t)
}

func (t Todos) Swap(i, j int) {
	t[i], t[j] = t[j], t[i]
}

func (t Todos) Less(i, j int) bool {
	return t[i].ID > t[j].ID
}

func GetTodoListHandler(w http.ResponseWriter, r *http.Request) {
	list := make(Todos, 0)
	for _, todo := range todoMap {
		list = append(list, todo)
	}
	sort.Sort(list)
	rd.JSON(w, http.StatusOK, list) // ID로 정렬하여 전체 목록 반환
}

func PostTodoHandler(w http.ResponseWriter, r *http.Request) {
	var todo Todo
	err := json.NewDecoder(r.Body).Decode(&todo)
	if err != nil {
		log.Fatal(err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	lastID++ // 새로운 ID로 등록하고 만든 Todo 반환
	todo.ID = lastID
	todoMap[lastID] = todo
	rd.JSON(w, http.StatusCreated, todo)
}

type Success struct {
	Success bool `json:"success"`
}

func RemoveTodoHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r) // ID에 해당하는 할 일 삭제
	id, _ := strconv.Atoi(vars["id"])
	if _, ok := todoMap[id]; ok {
		delete(todoMap, id)
		rd.JSON(w, http.StatusOK, Success{true})
	} else {
		rd.JSON(w, http.StatusNotFound, Success{false})
	}
}

func UpdateTodoHandler(w http.ResponseWriter, r *http.Request) {
	var newTodo Todo // ID에 해당하는 할 일 수정
	err := json.NewDecoder(r.Body).Decode(&newTodo)
	if err != nil {
		log.Fatal(err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	vars := mux.Vars(r)
	id, _ := strconv.Atoi(vars["id"])
	if todo, ok := todoMap[id]; ok {
		todo.Name = newTodo.Name
		todo.Completed = newTodo.Completed
		rd.JSON(w, http.StatusOK, Success{true})
	} else {
		rd.JSON(w, http.StatusBadRequest, Success{false})
	}
}

func main() {
	m := MakeWebHandler()  // 기본 핸들러 (핸들러들이 등록된 mux가 반환됨)
	n := negroni.Classic() // negroni 기본 핸들러
	n.UseHandler(m)        // negroni 기본 핸들러로 만든 핸들러 MakeWebHandler 을 감싼다.
	// HTTP 요청 수신 시 negroni에서 제공하는 부가 기능 핸들러들을 수행하고 난 뒤, MakeWebHandler()를 수행한다.

	log.Println("Started App")
	err := http.ListenAndServe(":3000", n) // negroni 기본 핸들러가 동작함
	if err != nil {
		panic(err)
	}
}

 

프론트 엔드는 핵심이 아니므로 github(https://github.com/tuckersGo/musthaveGo/tree/master/ch31/ex31.1/public)의 파일을 참조해서 웹서버의 위치에 /public 폴더를 만들고 넣는다.

 

실행해보면 아래와 같이 실행된다.

 

negroni 를 사용해서 로그도 그럴싸하게 남는다.

$ go run .\\main.go
2023/11/09 22:57:37 Started App
[negroni] 2023-11-09T22:57:51+09:00 | 200 |      213.3309ms | localhost:3000 | GET /
[negroni] 2023-11-09T22:57:51+09:00 | 200 |      22.9704ms | localhost:3000 | GET /todo.css
[negroni] 2023-11-09T22:57:51+09:00 | 200 |      39.5097ms | localhost:3000 | GET /todo.js
[negroni] 2023-11-09T22:57:51+09:00 | 200 |      976.8µs | localhost:3000 | GET /todos
[negroni] 2023-11-09T22:57:51+09:00 | 404 |      257.9µs | localhost:3000 | GET /favicon.ico
[negroni] 2023-11-09T22:58:04+09:00 | 201 |      0s | localhost:3000 | POST /todos
[negroni] 2023-11-09T22:58:16+09:00 | 201 |      0s | localhost:3000 | POST /todos
[negroni] 2023-11-09T22:58:22+09:00 | 201 |      0s | localhost:3000 | POST /todos

'Book Study > Tucker의 Go Programming' 카테고리의 다른 글

Go스터디: 6주차(27~30장)  (0) 2023.11.05
Go스터디: 5주차(23~26장)  (0) 2023.10.29
Go스터디: 4주차(18~22장)  (1) 2023.10.22
Go스터디: 3주차(12~17장)  (1) 2023.10.15
Go스터디: 2주차(3~11장)  (1) 2023.10.08

+ Recent posts