CloudNet@ 가시다님이 진행하는 Kubernetes Advanced Networking Study 2주차 정리입니다.

 

3. 파드 & PAUSE 컨테이너

요약 : 파드는 1개 이상의 컨테이너로 구성된 컨테이너의 집합이며, PAUSE 컨테이너가 Network/IPC/UTS 네임스페이스를 생성하고 유지/공유

 

▶ k8s CRI

  • Container Runtime : kubelet -> CRI -> High Level Runtime (continerd) <- OCI -> Low Level Runtime (Runc)
  • CRI 배경
    - 쿠버네티스 노드의 가장 낮은 계층(Low Level)에는 컨테이너를 시작하고 중지하는 것 외에도 소프트웨어가 있다. 이를 "컨테이너 런타임(Runc 등)"이라고 합니다. 가장 널리 알려진 컨테이너 런타임은 Docker이지만 유일한 것은 아닙니다. 
    "CRI"라고 하는 쿠버네티스의 컨테이너 런타임을 위한 새로운 플러그인 API를 개발해 왔습니다.
    - CRI란 무엇이고 쿠버네티스에 왜 필요한가? 표준 인터페이스를 통해서 kubelet 재컴파일 없이 다양한 컨테이너 런타임 사용 
    : 각 컨테이너 런타임에는 고유한 강점이 있으며, 많은 사용자가 Kubernetes가 더 많은 런타임을 지원하도록 요청.
  • CRI
    - kubelet은 gRPC 프레임워크를 사용하여 Unix 소켓을 통해 컨테이너 런타임(또는 런타임용 CRI shim)과 통신
    - 여기서 kubelet은 클라이언트 역할을 하고 CRI shim은 서버 역할
    - 프로토콜 버퍼 API에는 두가지 gRPC 서비스인 ImageService와 RuntimeService가 포함되어 있습니다.
    ImageService리포지토리에서 이미지를 가져오고, 이미지 검사, 제거하는 RPC 제공
    RuntimeService에는 pod와 컨테이너의 수명주기를 관리하는 RPC와 컨테이너와 상호 작용하기 위한 호출이 포함.
  • Pod는 리소스 제약이 있는 격리된 환경의 애플리케이션 컨테이너 그룹으로 구성. CRI에서 이 환경을 PodSandbox라고 합니다.
    - 우리는 의도적으로 컨테이너 런타임이 내부적으로 작동하는 방식에 따라 PodSandbox를 다르게 해석할 수 있는 여지를 남겨둡니다. 하이퍼바이저 기반 런타임의 경우 Podsandbox는 가상 머신을 나타낼 수 있습니다. Docker와 같은 다른 경우 Linux 네임스페이스일 수 있습니다. 
  • Pod를 시작하기 전에 kubelet은 Runtimeservice.RunPodSandbox를 호출하여 환경을 만듭니다. 여기에는 Pod에 대한 네트워킹 설정 (IP 할당 등)이 포함됩니다. PodSandbox가 활성화되면 개별 컨테이너를 독립적으로 생성/시작/중지/제거할 수 있습니다. Pod를 삭제하기 위해 kubelet은 PodSandbox를 중지하고 제거하기 전에 컨테이너를 중지하고 제거합니다.
  • Kubelet은 RPC를 통해 컨테이너의 수명 주기를 관리하고, 컨테이너 수명 주기 후크와 활성/준비 확인을 실행하며, pod의 재시작 정책을 준수합니다.

▶ 파드 : 컨테이너 애플리케이션의 기본 단위를 파드라고 부르며, 파드는 1개 이상의 컨테이너로 구성된 컨테이너의 집합

  • Pod는 1개 이상의 컨테이너를 가질 수 있다 ( sidecar 패턴 등)
  • Pod 내에 실행되는 컨테이너들은 반드시 동일한 노드에 할당되며 동일한 생명주기를 갖는다. -> Pod 삭제 시 , Pod 내 모든 컨테이너가 삭제
  • Pod IP - pod는 노드 IP와 별개로 클러스터 내에서 접근 가능한 IP를 할당 받으며, 다른 노드에 위치한 Pod도 NAT 없이 Pod IP로 접근 가능 -> CNI
  • IP 공유 - Pod 내에 있는 컨테이너들은 서로 IP를 공유, 컨테이너끼리는 localhost 통해 서로 접근하며 포트를 이용해 구분
    - pausse 컨테이너가 'parent'처럼 network ns를 만들어주고, 내부의 컨테이너들은 해당 net ns를 공유 
    - 쿠버네티스에서 pause 컨테이너는 pod의 모든 컨테이너에 대한 "부모 컨테이너" 역할
    - pause 컨테이너에는 두가지 핵심 책임
    : Pod에서 Linux  네임스페이스 공유의 기반 역할
    : PID(프로세스 ID) 네임스페이스 공유가 활성화 되면 각 Pod에 대한 PID1 역할을 하며 좀비 프로세스를 거둡니다.
  • volume 공유 - pod안의 컨테이너들은 동일한 볼륨과 연결이 가능하여 파일 시스템을 기반으로 서로 파일을 주고 받을 수 있음
  • pod는 리소스 제약이 있는 격리된 환경의 애플리케이션 컨테이너 그룹으로 구성. CRI에서 이 환경을 podsandbox라고 한다.
  • pod를 시작하기 전에 kubelet은 RuntimeService.RunPodSandbox를 호출하여 환경 만든다
  • Kubelet은 RPC를 통해 컨테이너의 수명 주기를 관리하고, 컨테이너 수명 주기 후크와 활성/준비 확인을 실행하며, Pod의 재시작 정책을 준수

▶ pause.c

1. 헤더파일 포함

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
  • 다양한 표준 라이브러리와 시스템 콜을 포함
  • signal.h : 신호 처리 함수와 구조체를 사용하기 위해 필요
  • stdlib.h, stdio.h, string.h : 표준 입출력, 문자열, 메모리 할당 등을 위해 필요
  • sys/types.h, sys/wait.h : 시스템 데이터 타입과 자식 프로세스의 상태를 확인하기 위한 함수(waitpid)를 사용하기 위해 필요
  • unistd.h : 유닉스 시스템 호출을 사용하기 위해 필요

2. 매크로 정의

#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)
  • STRINGIFY(x) : 매크로 x를 문자열로 변환
  • VERSION_STRING(x) : x를 문자열로 변환하여 VERSION 값을 문자열로 표현

3. VERSION 기본값 설정

#ifndef VERSION
#define VERSION HEAD
#endif
  • VERSION이 정의되어 있지 않으면 HEAD로 설정됩니다. 이는 코드의 버전 정보를 표현하는 데 사용

4. 신호 처리 핸들러 : sigdown 함수

static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}

 

  • sigdown 함수는 프로세스가 SIGINT 또는 SIGTERM 신호를 받을 때 호출됩니다. 신호를 받아 메시지를 출력하고 프로그램을 종료합니다.

5. 신호 처리 핸들러 : sigreap 함수

static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0)
    ;
}
  • sigreap 함수는 SIGCHILD 신호를 처리합니다. 자식 프로세스가 종료될 때 이 신호를 받으며, waitpid를 사용해 종료된 자식 프로세스의 상태를 수집(좀비 프로세스 방지)합니다.
  • waitpid(-1, NULL, WNOHANG) > 0 : 자식 프로세스의 종료를 비동기적으로 기다립니다. 더 이상 종료된 자식 프로세스가 없으면 루프를 종료합니다.

6. main 함수

int main(int argc, char **argv) {
  int i;
  for (i = 1; i < argc; ++i) {
    if (!strcasecmp(argv[i], "-v")) {
      printf("pause.c %s\n", VERSION_STRING(VERSION));
      return 0;
    }
  }
  • 프로그램이 시작되면 명령줄 인수를 처리합니다.
  • v 옵션이 제공되면, 프로그램의 버전 정보를 출력하고 종료합니다.

7. 프로세스 ID 확인

if (getpid() != 1)
  fprintf(stderr, "Warning: pause should be the first process\n");
  • 프로세스가 PID 1(시스템의 init 프로세스)이 아니면 경고 메시지를 출력합니다. 이 프로그램은 컨테이너에서 첫 번째 프로세스로 실행되는 것을 가정하고 있습니다.

8. 신호 핸들러 설정

if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
  return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
  return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                           .sa_flags = SA_NOCLDSTOP},
              NULL) < 0)
  return 3;
  • sigaction을 사용해 SIGINT, SIGTERM, SIGCHILD 신호에 대한 핸들러를 각각 설정합니다.
  • SIGINT, SIGTERM:sigdown 함수를 호출하도록 설정됩니다.
  • SIGCHILD : sigreap 함수를 호출하도록 설정되며, SA_NOCLDSTOP 플래그는 자식 프로세스가 일시 중지되었을 때 신호를 보내지 않도록 합니다.

9. 무한 대기 루프

for (;;)
  pause();
  • 프로그램이 종료될 때까지 무한 대기합니다. pause()는 신호가 발생할 때까지 대기하는 함수입니다.

10. 오류 메시지 출력 및 종료

fprintf(stderr, "Error: infinite loop terminated\n");
return 42;
  • 이 부분은 실행되지 않습니다. 만약 무한 루프가 끝난다면 오류로 간주하며 메시지를 출력하고 특이한 종료 코드(42)를 반환합니다.

▶ pod 배포 및 격리 확인

 

  • 신규 pod를 배포하고 확인

 

▶ myweb2 파드 정보 : myweb2 파드에 2개의 컨테이너가 동작

 

  • myweb2.yaml : containers가 복수형을 주목

▶ 배포 후 확인 => NET IPC UTS 공유 확인

 

▶ [심화] Ephemeral Containers : k8s v1.25 [stable]

  • 소개 : 현재 동작 중인 파드에 임시 Ephemeral 컨테이너를 주입 배포하여 디버깅 및 분석에 활용
    - Pods are the fundamental building block of Kubernetes applications. Since Pods are intended to be disposable and replaceable, you cannot add a container to a Pod once it has been created. Instead, you usually delete and replace Pods in a controlled fashion using deployments.
    - Sometimes it's necessary to inspect the state of an existing Pod, however, for example to troubleshoot a hard-to-reproduce bug. In these cases you can run an ephemeral container in an existing Pod to inspect its state and run arbitrary commands.
  • 제약사항
    - Ephemeral containers may not have ports, so fields such as ports, livenessProbe, readinessProbe are diallowed.
    - Pod resource allocations are immutable, so setting resources is disallowed.
    - Ephemeral containers are created using a special ephemralcontainers handler in the API rather than by adding them directly to pod.spec, so it's not possible to add an ephemeral container using kubectl edit.
    - Like regular containers, you may not change or remove an ephemeral container after you have added it to a Pod.
    - Ephemeral containers are not supported by static pods.


▶ [심화] 도커만 사용해서 pause 컨테이너 만들기 -> NET IPC PID 공유 확인

컨테이너 생성
컨테이너의 ID와 PID 변수 지정
pause 컨테이너 정보 확인
sleep-busybox 컨테이너 정보 확인

 

4. Flannel CNI

요약 : 쿠버네티스는 네트워크 모델의 요건을 만족하고 CNI 플러그인이 있고, 대표적으로 'Calico, Cilium 등'이 있습니다.

 

  • 쿠버네티스 네트워크 모델은 아래 4가지 요구사항이 있다.
    1. 파드와 파드 간 통신 시 NAT 없이 통신이 가능합니다.
    2. 노드의 에이전트는 파드와 통신이 가능합니다.
    3. 호스트 네트워크를 사용하는 파드는 NAT 없이 파드와 통신이 가능합니다.
    4. 서비스 클러스터 IP 대역과 파드가 사용하는 IP 대역은 중복되지 않아야 합니다.
  • 아래 4가지 문제 해결이 필요합니다.
    1. 파드 내 컨테이너는 루프백을 통한 통신을 할 수 있습니다.
    2. 파드 간 통신을 할 수 있습니다.
    3. 클러스터 내부에서 서비스를 통한 통신을 할 수 있습니다.
    4. 클러스터 외부에서 서비스를 통한 통신을 할 수 있습니다.
  • Flannel runs a small, single binary agent called flanneld on each host, and is responsible for allocating a subnet lease to each host out of a larger, preconfigured address space. -> 모든 노드에 flanneld가 동작
  • 네트워킹 환경 지원(Backends) : VXLAN, host-gw, UDP
    - VXLAN(권장) : Port(UDP 8472), DirectRouting 지원(같은 서브넷 노드와는 host-gw 처럼 동작)
    : L2 확장이 아니라 각 노드마다 별도의 서브넷이 있고, 해당 서브넷은 대역끼리 NAT 없이 라우팅 처리됨
    - host-gw : 일반적으로 퍼블리 클라우드 환경에서는 동작하지 않는다
    - UDP(비권장) : VXLAN 지원하지 않는 오래된 커널 사용 시, Port(UDP 8285)
  • 노드마다 flannel.1 생성 : VXLAN VTEP역할, 뒤 숫자는 VXLAN id 1에 1을 의미
  • 노드마다 cni0 생성 : bridge 역할

 

 

▶ kind & Flannel 배포

배포 확인

 

네트워크 확인
노드 확인 CRI
노드 라벨 확인
컨테이너 확인
컨테이너 내부 정보 확인

▶ bridge 실행파일 생성 후 로컬에 복사

namespace에 pod-security.kubernetes.io/enforce=privileged Label 확인

 

기본 네트워크 정보 확인

▶ Flannel 정보 확인

노드마다 할당된 dedicated subnet 확인

 

VXLAN 모드 정보와, VTEP 정보 확인

▶ 파드 2개 생성

파드 생성 확인

  • 파드 생성 후 정보 확인

브리지 정보 확인

▶ 통신 흐름 이해 : 동일 노드, 다른 노드 간

▶ 파드 shell 접속 후 확인 & 패킷 캡처

  • Wireshark에서 vxlan 기본 udp port를 4789를 사용. Flannel은 8472를 사용하니 udp port사용하여 정보 확인

[심화] 파드에 IP 할당 되기 까지(노드에 파드 IP 대역 할당 내용 포함)

1. k8s 클러스터에서 파드가 사용할 대역을 kubeadm 실행 시 설정할 수 있다 > kind시 기본값으로 pod-network-cidr=10.244.0.0/16 대역을 사용한다

helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30000 --set env.TZ="Asia/Seoul" --namespace kube-system

 

2. 워커 노드가 마스터 노드에 조인(Join) 시점에 'kube-controller-manager' 파드가 pod-network-cidr 대역 중에서 각 '워커 노드'마다 충돌되지 않는 podCIDR을 할당

# 파드 확인
kubectl get pod -n kube-system -l component=kube-controller-manager

# kube-controller-manager 파드의 CIDR allocator 동작 로그 확인
kubectl logs -n kube-system -l component=kube-controller-manager | grep CIDR
I1031 15:21:13.763667       1 range_allocator.go:116] No Secondary Service CIDR provided. Skipping filtering out secondary service addresses.
I1031 15:21:13.964982       1 range_allocator.go:172] Starting range CIDR allocator

# 워커 노드마다 할당된 dedicated subnet (podCIDR) 확인
kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}'

 

3. 이후 파드가 특정 워커 노드에 스케줄(할당) 시 파드에 IP를 할당하는 과정

# 워커노드에서 확인
docker exec -it myk8s-worker bash
---------------------------------
# 기본 flannel 플러그인 정보 확인
cat /etc/cni/net.d/10-flannel.conflist | jq
{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

# 해당 노드의 flannel 적용된 podCIDR 과 그와 network metadata 정보
cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.1.1/24
FLANNEL_MTU=65485
FLANNEL_IPMASQ=true

 

[심화] flannel이 podCIDR 라우팅 정보를 획득하는 방법
: flanneld는 etcd를 통하여 상대 워커 노드의 podCIDR 정보를 획득하고 자신의 라우팅 정보에 업데이트를 합니다.

'kubetail kube-flannel-* --since 10h | grep etcd' 로그 확인 시 flanneld가 etcd 와 통신하는 로그도 확인 가능.

  • Flannel uses either the Kubernetes API or etcd directly to store the nettwork configuration, the allocated subnets, and any auxiliary data(such as the host's public IP). > 즉 flannel은 etc에 네트워크 설정, 노드마다 할당된 서브넷 정보를 저장
  • 각 노드에서 동작하는 flanneld(flannel 파드)는 etcd 를 통해서 상대 노드의 podCIDR 정보를 획득하고 자신의 라우팅 정보에 업데이트를 한다
    Each flanneld agent provides this information to a centralized etcd store so other agents on hosts can route packets to other containers within the flannel network.

[심화] k8s 없이 CNI 동작을 구현하여 파드 생성

▶ CNI 소개

  • 리눅스 컨테이너의 네트워크 인터페이스를 설정할 수 있도록 도와주는 일련의 명세와 라이브러리로 구성
  • CNI는 오직 '컨테이너의 네트워크 연결성' 과 '컨테이너 삭제 시 관련된 네트워크 리소스 해제'에 대해서만 관여. 그 외의 구체적인 사안에 대한 제한이 없음
  • 이를 실행하는 runtime는 어떤 것이든 상관없음(k8s, podman, cloud foundry 등)

▶ CNI 명세

  • 명세에는 컨테이너가 리눅스 네트워크 namespace 안에 있다고 정의합니다. 도커와 같은 컨테이너 runtime은 매 컨테이너 실행 시, 새로운 namespace를 만들기에 네트워크 namespace에 대해 잘 알고 있어야 합니다.
  • CNI의 네트워크 정의서는 JSON 형식으로 정의됩니다
  • 네트워크 정의서는 STDIN을 통해 스트림으로 CNI plugin에 전달되어야 합니다. 네트워크 설정을 위한 파일이 따로 특정 위치에 저장되어 참조되지 않아야 합니다.
  • 다른 매개변수들은 환경변수로 plugin에 전달되어야 합니다.
  • CNI plugin은 실행파일(executable)로 구현되어야 합니다.
  • CNI plugin은 컨테이너 네트워크 연결에 책임을 가지고 있습니다. (컨테이너가 네트워크에 연결되기 위한 모든 작업에 책임을 가집니다.) 도커에서는 컨테이너의 네트워크 namespace를 호스트에 연결시키는 것까지 포함됩니다.
  • CNI plugin은 IPAM(IP 할당관리)에 책임을 가지고 있습니다. 이것은 IP 주소 할당 뿐만 아니라 적절한 라우팅 정보를 입력하는 것까지 포함됩니다.