VMware vSphere의 공유 데이터 스토어로는 VMware vSAN을 사용한 HCI 구성하거나 혹은 외부 스토리지를 제공하는 방식이 있습니다. 외부 스토리지는 대표적으로 SAN과 NAS가 있을 수 있는데, 이번 포스팅에서는 Synology NAS의 SAN Manager를 활용, iSCSI를 통해 제공된 VMware vSphere 공유 데이터 스토어에 대한 튜닝을 통해 얼마나 성능 향상을 이룰 수 있는지 확인해 보도록 하겠습니다.

테스트 환경

장비 사양 소프트웨어 용도 비고
Beelink SER5 MAX - CPU: AMD Ryzen 7 5800H
- RAM: 64GB DDR4 3200MHz
- Network: 1Gbps Ethernet 2EA
- Local Disk: Essencore KLEVV CRAS C710 M.2 NVMe 256GB
VMware ESXi 8.0.3 VM 호스팅용 하이퍼바이저 2개 NIC 중 1개 NIC을 iSCSI 전용 스토리지 네트워크로 사용
Synology DS1515+ - CPU: Intel Atom C2538
- RMA: 16GB DDR3 메모리
- Network: 1Gbps NIC 4EA
- Disk: Samsung SSD 870 EVO 4TB 3EA(RAID 5)
DSM 7.1.1-42962 Update 9 iSCSI LUN 제공 3개의 이더넷 인터페이스를 통해 iSCSI 타겟 제공/Thick Provisioning LUN
TP-Link SG2218 Interface: 1Gbps RJ45 16 Ports / 1Gbps SFP Slot 2EA - 스토리지 트래픽 전송 L2 네트워크에 Synology NAS와 ESXi 호스트를 위치
IOPS Test VM - CPU: 4vCore
- RAM: 4GB
- Network: 1Gbps
- Disk: 60GB
- SCSI 컨트롤러: VMware 반가상화
Ubuntu 24.03 IOPS 테스트 Thick Provisioning Disk

테스트 토폴로지

  • 스토리지 네트워크 MTU 1500: 점보 프레임 설정 시 ESXi 호스트가 먹통되는 문제 발생으로 기본값 사용
  • 동일한 L2 네트워크에 NAS와 ESXi 호스트를 두어, 스위치 레벨에서 트래픽을 처리하도록 설계
  • Synology NAS iSCSI LUN과 VM의 디스크 모두 Thick Provisioning 구성
  • 캐시 효과 배제를 위해 NAS 물리 메모리 용량의 1.5배인 24GB로 테스트 데이터 설정

테스트 시나리오

 Test VM의 블록 디스크의 위치와, ESXi의 설정을 변경해 가면서 IOPS 성능의 변화를 측정합니다.

시나리오 다중 경로 지정 정책 Synology I/O 정책 설명
[대조군] ESXi Local Disk - -  
[비교군 1] MPIO 기본 설정 가장 최근에 사용됨(VMware) 버퍼링된 I/O ESXi 호스트가 iSCSI 타겟에 대한 다중 경로만 인식하고 있는 상태 
[비교군 2] 라운드 로빈 기본 설정 라운드 로빈(VMware) 버퍼링된 I/O ESXi 호스트가 iSCSI 타겟에 대해서 1000IOPS 마다 새로운 경로를 지정하여 사용
[비교군 3] 라운드 로빈 IOPS 정책 수정 라운드 로빈(VMware)
1 IOPS
버퍼링된 I/O ESXi 호스트가 iSCSI 타겟에 대해서 1 IOPS마다 새로운 경로를 지정하여 사용
[비교군 4] 라운드 로빈 IOPS 정책 수정 + Synology NAS I/O 정책 변경 라운드 로빈(VMware)
1 IOPS
직접 I/O Synology NAS 버퍼링 해제
  • 테스트 도구: Fio
  • 방식
    • 테스트 시작 전 24GB 전체 영역에 데이터를 기록하는 과정을 선행하여 Test VM의 시스템 레벨의 할당 오버헤드 제거
    • 테스트 시 실제 데이터를 읽어 오도록 강제하여 MPIO 네트워크 대역폭과 디스크 I/O 성능을 정확히 타격하도록 설계
    • 각 테스트간 60초의 휴지 기간을 가지고 실행
  • 측정 결과 집계: 총 3회를 측정하여 평균값을 도출

테스트 데이터 생성 스크립트

fio --name=warmup_fill \
    --filename=fio_test_file.dat \
    --ioengine=libaio \
    --rw=write \
    --bs=1M \
    --direct=1 \
    --numjobs=1 \
    --size=24G \
    --group_reporting
  • 앞서 언급한 NAS의 캐시 효과 배제를 위해 NAS 물리 메모리의 1.5배인 24GB 테스트 데이터를 준비

테스트 스크립트

COUNT=3				# Test 횟수
OUTPUT_PREFIX="test name"		#fio 테스트 결과 파일 이름 prefix

echo "==== FIO 벤치마크 시작 (총 ${COUNT}회) ===="

for i in $(seq 1 $COUNT); do
    echo ""
    echo "[$(date '+%H:%M:%S')] ${i}회차 진입: 60초 대기 후 테스트 시작..."
    sleep 60

    echo "[$(date '+%H:%M:%S')] ${i}회차 측정 시작 (5분)..."
    fio --name=benchmark_mpio_test \
        --filename=fio_test_file.dat \
        --ioengine=libaio \
        --rw=randrw \
        --rwmixread=70 \
        --bs=4k \
        --direct=1 \
        --numjobs=4 \
        --size=24G \
        --runtime=300 \
        --time_based \
        --iodepth=32 \
        --group_reporting \
        --output-format=json \
        --output="${OUTPUT_PREFIX}_${i}.json"

    echo "[$(date '+%H:%M:%S')] ${i}회차 완료 -> ${OUTPUT_PREFIX}_${i}.json 저장됨"
done

iSCSI 설정 - 테스트 전 기본 설정

공통 설정 - IOPS 대기열 맞춤

ESXi의 대상 디바이스의 IOPS 대기열 크기 확인
Synology NAS의 SAN Manager > 설정 > iSCSI 서비스 > I/O 대기열 수준
iSCSI 클라이언트의 다중 경로 접근 허용

  • ESXi에서 확인되는 iSCSI 타겟 LUN의 대기열 크기와 Synology NAS의 iSCSI 서비스의 I/O 대기열 수준을 동일하게 설정

 

[비교군 1] MPIO 기본 설정

MPIO 기본값 확인 - 가장 최근에 사용됨(VMware)

  • 소프트웨어 스토리지 어댑터를 생성하고 NAS의 iSCSI 서버 IP 중 하나를 동적 검색으로 등록하면 다중 경로 설정이 자동으로 생성
  • 다중 경로 지정 정책의 기본 정책은 가장 최근에 사용됨(VMware)로 이는 NAS에서 제공하는 경로 중 가장 최근에 사용된 경로를 계속 사용하는 것으로, 해당 경로가 실패할 때 다른 경로로 변경됨(Active - Standby 설정)

[비교군 2] 라운드 로빈 기본 설정

다중 경로 지정 정책을 라운드 로빈(VMware)로 변경

  • 라운드 로빈 정책을 통해 다중 경로를 돌아가면서 사용할 수 있도록 선택
  • 라운드 로빈 기본 설정값은 1000 IOPS 마다 경로를 변경
# esxi 호스트에 shell로 접근 후 아래 명령어를 수행
# Storage device list 확인
esxcli storage nmp device list | grep -i "Device Display Name" -B 1

naa.6001405722e8deed16d2d4662d8c32df
   Device Display Name: SYNOLOGY iSCSI Disk (naa.6001405722e8deed16d2d4662d8c32df)
--
naa.60014056b78746cda247d45dbd8576de
   Device Display Name: SYNOLOGY iSCSI Disk (naa.60014056b78746cda247d45dbd8576de)
--
naa.6001405e5b0f035db767d4b26da2d1da		# 튜닝 대상 데이터 스토어
   Device Display Name: SYNOLOGY iSCSI Disk (naa.6001405e5b0f035db767d4b26da2d1da)
   
# 데이터 스토어 기본 IOPS 정책 확인
esxcli storage nmp psp roundrobin deviceconfig get -d naa.6001405e5b0f035db767d4b26da2d1da

   Byte Limit: 10485760
   Device: naa.6001405e5b0f035db767d4b26da2d1da
   IOOperation Limit: 1000			# 1000 IOPS 마다 경로 변경이 기본 설정
   Latency Evaluation Interval: 0 milliseconds
   Limit Type: Default				# 기본 라운드 로빈 정책
   Number Of Sampling IOs Per Path: 0
   Use Active Unoptimized Paths: false

[비교군 3] 라운드 로빈 IOPS 정책 수정

# 위에서 살펴본 튜닝 대상 디스크에 대해서 아래 명령어를 실행하여 IOPS 정책을 변경
esxcli storage nmp psp roundrobin deviceconfig set --type=iops --iops=1 --device=naa.6001405e5b0f035db767d4b
26da2d1da

# 정책 변경 확인
esxcli storage nmp psp roundrobin deviceconfig get -d naa.6001405e5b0f035db767d4b26da2d1da

   Byte Limit: 10485760
   Device: naa.6001405e5b0f035db767d4b26da2d1da
   IOOperation Limit: 1			# 1 IOPS 마다 경로 변경 정책 설정됨
   Latency Evaluation Interval: 0 milliseconds
   Limit Type: Iops			# IOPS 기준으로 경로 변경 정책 설정됨
   Number Of Sampling IOs Per Path: 0
   Use Active Unoptimized Paths: false
  • 1 IOPS 마다 iSCSI 경로를 변경하도록 설정하여 극한의 성능 향상을 노리는 설정

[비교군 4] 라운드 로빈 IOPS 정책 수정 + Synology NAS I/O 정책 변경

Synology NAS의 버퍼 기능 해제

  • LUN에 대한 I/O 버퍼를 해제하고 직접 디스크에 읽고 쓰는 설정을 활성화
  • 데이터가 물리 디스크에 안전하게 기록되었음을 확인할 때까지 완료 신호를 주지 않는 설정으로 안정성 향상의 목적

테스트 결과 및 심층 분석

 총 4가지 시나리오에 대한 FIO 벤치마크 결과, iSCSI MPIO 튜닝(라운드 로빈 IOPS=1)이 단일 물리 네트워크 환경에서 드라마틱한 성능 향상을 가져옴을 확인했습니다. 하지만 이면에는 운영 입장에서 반드시 알아야할 Trade-off가 숨겨져 있는것도 확인할 수 있었습니다.

테스트 결과

테스트 시나리오 Total IOPS Bandwidth(MB/s) Read Latency Avg (ms) Read Latency P99 (ms) Write Latency Avg (ms) Write Latency P99 (ms)
[대조군] ESXi Local Disk 51890 202.69 3.24 8.26 0.67 1.77
[비교군 1] MPIO 기본 설정 17558.75 68.59 7.19 14.99 7.53 19.97
[비교군 2] 라운드 로빈 기본 설정 20153.18 78.72 6.23 13.13 6.6 16.78
[비교군 3] 라운드 로빈 IOPS 정책 수정 27543.46 107.59 4.53 21.8 4.89 26.96
[비교군 4] 라운드 로빈 IOPS 정책 수정 + Synology NAS I/O 정책 변경 8172.02 31.92 1.89 5.97 47.69 67.2

 

Total IOPS 그래프
Bandwidth (MB/s) 그래프
Read/Write Latency 평균 그래프
Read/Write Latency P99 그래프

심층 분석

Insight 1. 기본값(Default)의 함정: 대역폭 낭비

 가장 먼저 눈에 띄는 것은 기본 설정인 [비교군 1] - MPIO 기본 설정[비교군 2] - 라운드 로빈 기본 설정의 비효율성 입니다.

  • 현상: [비교군 1][비교군 2]의 설정에서는 대역폭이 약 68 - 78 MB/s에 머물렀습니다. 이는 1 Gbps 네트워크의 최대 한계의 60 - 70% 수준 밖에 활용하지 못하는 수준입니다.
  • 원인: TCP 단일 세션의 한계입니다. ESXi가 단일 경로를 고집하거나[비교군 1] 경로 변경 주기가 너무 길면[비교군 2], 4K 랜덤 I/O와 같은 작은 블록 전송 시 윈도우 사이즈와 응답 대기(RTT)로 인해 물리 대역폭을 가득 채우지 못합니다. 고속도로는 뚫려 있는데 톨게이트를 하나만 열어둔 셈입니다.

Insight 2. IOPS=1 튜닝: 물리적인 최대치에 도달

 라운드 로빈 정책의 경로 변경 임계값을 1 IOPS로 수정[비교군 3]한 후, 생각보다 놀라운 성능 향상이 관찰되었습니다. 구형 NAS임에도 불구하고 예상보다 고무적인 수치가 나와주었습니다.

  • 성과: 대역폭이 107.58MB/s까지 치솟았습니다. 이는 이더넷 오버헤드를 제외하면 사실상 1Gbps 물리 회선을 95% 이상 포화 시킨 수치입니다.
  • 핵심: 물리적인 업링크는 1개뿐이지만, '다중 세션(Multi-Session)' 효과를 톡톡히 봤습니다. 1 IOPS마다 경로를 바꿔가며 NAS의 여러 포트로 세션을 분산시켰고, 이를 통해 I/O 큐(Queue)를 병렬로 처리하여 단일 세션의 병목을 소프트웨어적으로 극복해냈습니다.

Insight 3. 속도와 안정성의 Trade-off (P99 Latency & Kubernetes)

 하지만 모든 지표가 긍정적인 것만은 아닙니다. 하위 1%의 느린 응답 속도를 나타내는 P99 Latency의 변화를 주목해야 합니다. 특히 Kubernetes 관점에서 말이죠.

  • 현상: 평균 성능은 좋아졌지만, P99 Latency는 **14.99ms(MRU) → 21.8ms(Tuned)**로 오히려 악화되었습니다. 이는 대역폭 포화로 인한 네트워크 큐잉 지연(Queuing Delay) 때문입니다.
  • Kubernetes 환경에서의 의미: 이 수치는 etcd와 같은 민감한 워크로드에 치명적일 수 있습니다. etcd는 쓰기 작업마다 fsync를 수행하는데, P99 지연시간이 지속적으로 20~30ms를 넘어가면 리더 선출(Leader Election) 실패나 API 서버 응답 저하로 이어질 수 있습니다. 즉, 처리량(Throughput)을 얻는 대신 응답의 균일성(Stability)을 일부 희생한 결과입니다.

Insight 4. 벤치마크와 현실의 차이: 단일 vs 다중 워크로드

 이번 테스트 결과는 단일 VM이 스토리지를 독점할 때의 최대 성능이라는 점을 기억해야 합니다.

  • Noisy Neighbor: 실제 운영 환경에서 여러 VM이 동시에 부하를 준다면(Multi-Workload), NAS의 대기열이 가득 차면서 Latency는 이보다 훨씬 더 튀게 될 것입니다.
  • 의의: 따라서 이 결과값(27k IOPS)은 항상 보장되는 성능이 아니라, 이 인프라가 견딜 수 있는 물리적 한계점(Baseline)으로 해석하는 것이 타당합니다.

 

번외 분석: NAS 쓰기 캐시(Bufferd I/O)는 필수인가?

 마지막으로 데이터 무결성을 위해 직접 I/O를 활성화했을 때의 결과는 상당히 실망스러웠습니다.

  • 성능 붕괴: 쓰기 지연시간(Avg)이 47.69ms로 튜닝 전보다도 6배 이상 느려졌으며, 전체 대역폭은 31.92 MB/s로 곤두박질쳤습니다.
  • 교훈: SATA SSD 기반의 RAID 5 환경에서는 '패리티 연산(Read-Modify-Write)' 오버헤드가 매우 큽니다. 이를 NAS의 RAM 캐시가 받아주지 못하면, 아무리 네트워크를 튜닝해도 디스크단에서 병목이 발생함을 알 수 있습니다. 따라서 홈랩 환경에서는 성능은 버퍼(Cache)로 챙기는 것이 현실적인 정석입니다.

결론 및 제언: 성능과 안정성의 균형 찾기

 이번 실험을 통해 단일 1Gbps 망에서도 iSCSI 튜닝은 선택이 아닌 필수라는 점을 증명했습니다. 하지만 단순히 설정을 바꿨다고 끝이 아닙니다. 다음의 운영 전략이 병행되어야 합니다.

1. 최적의 튜닝 조합

 단일 1Gbps 링크의 물리적 한계를 극복하기 위해서는 라운드 로빈 (IOPS=1) 설정NAS의 쓰기 캐시(Buffered I/O 활성화가 가장 효과적인 해법임이 확인되었습니다. (대역폭 약 57% 향상)

2. 현실적인 운영 해법 (Hybrid Approach)

 하지만 앞서 분석했듯, 이 설정은 네트워크를 한계까지 밀어붙여 P99 Latency를 불안정하게 만들 수 있습니다. 따라서 Kubernetes와 같은 민감한 워크로드를 운영한다면 다음과 같은 전략을 추천합니다.

  • 기반 설정: 스토리지 성능 극대화를 위해 RR IOPS=1을 유지합니다.
  • 안정성 확보: ESXi 레벨에서 VM별 IOPS 제한(Limit)이나 SIOC(Storage I/O Control)를 적용합니다.
    • 일반 VM: IOPS를 적절히 제한하여 스토리지 큐를 독점하지 못하게 방지
    • 중요 VM (etcd/Control Plane): 제한을 풀거나 높은 우선순위(Shares)를 부여하여 Latency 보장

3. 마무리

 이번 테스트는 단순히 숫자를 높이는 튜닝이 아니라, 제한된 홈랩 자원 환경에서 성능·안정성·운영 복잡도 사이의 균형점을 어디에 두어야 하는지를 고민하는 과정이었습니다.

 스토리지 튜닝에 ‘무조건적인 정답’은 없습니다. 내 인프라의 한계를 명확히 알고, 워크로드의 성격에 맞춰 의도적으로 Trade-off를 선택하는 것이야말로 엔지니어링의 핵심임을 다시 한번 확인할 수 있었습니다.

[서론] 컨테이너가 죽지 않아!

 운영 중인 Kubernetes 노드에서 컨테이너 런타임(Containerd)을 업데이트하거나 재시작해야 할 때가 있습니다. 상식적으로 생각해 보면 부모 프로세스인 데몬이 종료되면, 그 자식 프로세스인 컨테이너들도 함께 종료되거나 고아(Orphan)가 되어 문제가 생겨야 할 것 같습니다. 하지만 실제로 그런일은 일어나지 않습니다. 서비스 중인 컨테이너(Pod)들은 아무런 영향 없이 계속 동작합니다. 어떻게 이것이 가능할까요? 도대체 리눅스 커널에서는 어떤 일이 벌어지고 있는 걸까요? 이 '죽지 않는 컨테이너'의 비밀을 풀기 위해 프로세스 트리의 족보를 파해져 보았습니다.

장비를 정지합니다. 정지하겠습니다. 어? 안 되잖아?

 이번 포스팅에는 잘 작동하고 있는 Kubernetes 클러스터의 Control plane 노드(Locky Linux 10, root 사용자 환경)에서 진행됩니다.

멀쩡히 작동하는 Kubernetes 클러스터

 위와 같이 잘 작동하고 있는 Kubernetes 클러스터가 있습니다. API 서버도 잘 작동중이고, Containerd 프로세스도 문제없이 작동 중입니다. 이제 Containerd 프로세스를 정지시켜 보겠습니다. 그리고 Kube-apiserver가 잘 응답하는지 확인해 보겠습니다.

# Conatinerd 프로세스 중지
systemctl stop containerd

# kube-apiserver Port 확인 및 kubectl 응답 확인
ss -nltp | grep 6443

kubectl get pods -n kube-system | grep api

Containerd 서비스는 중지 되었지만, kube-apiserver는 정상 작동 중

 분명 Containerd 서비스는 중지 되었지만, 여전히 6443 포트는 LISTEN 상태이고, kubectl을 통해서도 문제없이 Kubernetes 클러스터와 통신이 가능합니다. 정말 컨테이너가 죽지 않습니다. 그런데 Containerd 프로세스의 상태를 자세히 살펴보면 Cgroup 필드에 특이한 점을 발견할 수 있습니다.

  • 앞서 존재했던 1088523 containerd 프로세스(/usr/local/bin/containerd)가 보이지 않음
  • 그러나 여전히 containerd-shim-runc-v2라는 프로세스들은 여러 개가 보임

 일반적으로 프로세스가 중지되고 나면 Cgroup 필드 자체가 표시되지 않는데 Containerd는 Cgroup 필드가 살아 있습니다. 도대체 왜 이런 현상이 일어나고 있을까요?

중지된 kubelet 프로세스는 Cgroup 필드도 사라짐

[관찰] 족보가 이상한데요...? feat. pstree

 Containerd 프로세스가 중지 되었는데도 컨테이너(Pod)가 문제없이 작동한다는 것은, 아무래도 containerd와 컨테이너(Pod)가 서로 부모-자식 관계가 아닌것일지도 모르겠다는 추측을 하게 합니다. (리눅스 프로세스의 부모-자식 관계에 대한 상세한 내용은 이전 포스팅을 참고해 주세요.) 다음 명령어를 통해서 프로세스 트리를 확인해 봅시다.

 

 

Linux 프로세스 관리 - 좀비 프로세스에 관하여 [1편]

리눅스에서 지금 당장 아래 명령어를 입력해보자!$ ps aux$ pstree 아마도 리눅스를 조금 다루어 보았다면 'ps'는 상당히 익숙한 명령어일 것입니다. 잘 아시겠지만, 리눅스에서 명령어 'ps'는 현재 프

tech-recipe.tistory.com

# Conatinerd process 재시작
systemctl start containerd

# 프로세스 트리 확인
pstree

containerd와 형제 관계로 보이는 containerd-shim 프로세스들

 pstree를 통해 살펴보면 위에서 확인할 수 있듯, containerd와 containerd-shim은 부모-자식 관계가 아닌 형제 관계인것 처럼 보입니다. 예상했던 예상밖의(?) 결과입니다. 우선 여기서 확실하게 확인되는 것은 컨테이너(Pod)와 containerd 프로세스는 확실히 부모-자식 관계가 아니라는 점이고, 이 때문에 containerd 프로세스가 중지되거나 재시작되어도 컨테이너(Pod)는 아무 문제 없이 잘 작동할 수 있었던 것입니다. 그런데 containerd-shim은 도대체 무엇일까요? 왜 이 프로세스들이 컨테이너(Pod)들의 부모 프로세스의 위치를 가지고 있을까요?

[실험] 왜 컨테이너들이 containerd-shim의 자식이 되어 있을까?

커널에서는 무슨 일이 일어났던 것일까?

 strace 명령어를 사용하여 커널 레벨에서의 시스템 콜(System Call)을 추적, 컨테이너(Pod)가 생성될 때 실제로 어떤 일들이 벌어지고 있는지 확인해 봅시다. 현재 우리가 살펴보고 있는 컨트롤 플레인 노드에서 Pod를 고정적으로 생성하기 위해 Static Pod를 사용할 예정입니다. 다음과 같은 명령어를 통해 Static pod를 생성하고, 시스템 콜을 추적해 봅시다.

# containerd 프로세스의 시스템 콜을 추적
sudo strace -f -e trace=clone,execve,exit_group,prctl -p $(systemctl show containerd -p MainPID --value) 2>&1 | grep -v "SIGURG"
# 새로운 터미널 창을 열고 control plane에 접근
# nginx.yaml 파일 생성
kubectl run nginx --image=nginx --dry-run=client -o yaml > nginx.yaml

# static pod 생성
mv nginx.yaml /etc/kubernetes/manifests/

# 프로세스 트리 확인
pstree

strace와 pstree 명령어를 통한 프로세스 상태 추적

 

kubernetes-study/process-tree-and-cgruop at main · garlicKim21/kubernetes-study

Kubernetes Study의 기록입니다. Contribute to garlicKim21/kubernetes-study development by creating an account on GitHub.

github.com

containerd-shim(PID 1224703)과 pause 컨테이너(PID 1224727)

 strace와 pstree 명령어를 통해 확인할 수 있는 로그 및 프로세스 트리는 위 Github 레포 링크에서 자세하게 살펴볼 수 있습니다. 우선 pstree.txt 파일을 살펴보면 containerd의 PID는 1108358이고 nginx static pod의 부모인 containerd-shim의 PID는 1224703이며 pause 컨테이너의 PID는 1224727, nginx 컨테이너의 PID는 1224754 임을 확인할 수 있습니다. 이 프로세스들이 언제 어떻게 탄생했는지는 strace.logs 파일을 통해 확인할 수 있습니다.

# nginx Pod의 부모 프로세스 PID 1224703 생성
165 [pid 1224700] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224703 attached
166  <unfinished ...>

# PID 1224703 프로세스의 containerd-shim 실행
167 [pid 1224703] execve("/usr/local/bin/containerd-shim-runc-v2", ["/usr/local/bin/containerd-shim-r"..., "-namespace", "k8s.i    o", "-id", "c8b394369634a6c6755d4e9094907a92"..., "-address", "/run/containerd/containerd.sock"], 0xc000170900 /* 14 vars *    / <unfinished ...>
168 [pid 1224700] <... clone resumed>, parent_tid=[12]) = 1224703

# containerd-shim의 부모 프로세스였던 PID 1224700 프로세스 그룹 종료
169 [pid 1224703] <... execve resumed>)     = 0
170 [pid 1224700] exit_group(0)             = ?
171 [pid 1224701] +++ exited with 0 +++
172 [pid 1224696] +++ exited with 0 +++
173 [pid 1224699] +++ exited with 0 +++
174 [pid 1224695] +++ exited with 0 +++
175 [pid 1224700] +++ exited with 0 +++
176 [pid 1224698] +++ exited with 0 +++
177 [pid 1224697] +++ exited with 0 +++
178 [pid 1224694] +++ exited with 0 +++

 먼저 containerd-shim(PID 1142827)이 생겨나는 과정을 뜯어봅시다. 위 내용은 containerd-shim이 독립하여 systemd(PID 1)에 입양되어 containerd와 형제 프로세스가 되는 부분의 로그입니다. 중간 과정이 약간 생략되었지만, 큰 틀은 아래 그림과 같습니다.

containerd-shim이 systemd에 입양되는 과정

 contianerd로부터 생성된 프로세스 PID 1224700PID 1224703을 생성한 후 종료되면서, containerd-shim(PID 1224703)고아(Orphan) 상태가 되고 systemd(PID 1)이 이를 입양, containerd와 형제 프로세스가 되는 것입니다.

그럼 컨테이너(Pod)는 언제 생겼을까?

 다시 pstree를 상세하게 뜯어보겠습니다. containerd-shim(PID 1224703)의 자식 프로세스로 nginx(PID 1224754)pause(PID 1224727)이 보입니다. 그런데 그 어디를 살펴보아도 Pod는 보이지 않습니다. 그 이유는 Pod는 Kubernetes가 만든 논리적인 그룹일 뿐, 리눅스 커널에는 존재하지 않는 객체이기 때문입니다. 여기서 Kubernetes의 Infra 컨테이너(또는 sandbox 컨테이너)라는 개념이 등장합니다. Infra 컨테이너가 바로 pause(PID 1224727)이고 이것이 Pod의 구현체입니다. Kubernetes는 Pod라는 논리적 그룹을 구현하기 위해 리눅스 네임스페이스 기능을 사용하는데, 이 네임스페이스는 최소한 하나의 프로세스가 그 안에 살아 있어야만 유지됩니다. 이를 위해 아무 일도 하지 않지만 네임스페이스를 유지할 수 있는 프로세스인 pause를 생성하여 sandbox 환경(Pod)을 구현하게 됩니다. 그럼 이 pause 컨테이너가 어떤 과정을 통해 생성되고 있는지 strace 로그를 통해 추적해 보겠습니다.

# pause(PID 1224727) 프로세스 추적

# pause(PID 1224727)의 부모는 PID 1224726 확인
# PID 1224726의 부모는 PID 1224723 확인
# pause(PID 1224727) 프로세스 생성 후 부모 프로세스(PID 1224726), 조부모 프로세스(PID 1224723) 모두 종료
235 [pid 1224723] clone(child_stack=0x7ffec80fe4c0, flags=CLONE_PARENT|SIGCHLDstrace: Process 1224726 attached
236 ) = 1224726
237 [pid 1224726] prctl(PR_SET_NAME, "runc:[1:CHILD]") = 0
238 [pid 1224726] clone(child_stack=0x7ffec80fe4c0, flags=CLONE_PARENT|SIGCHLDstrace: Process 1224727 attached
239 ) = 1224727
240 [pid 1224727] prctl(PR_SET_NAME, "runc:[2:INIT]") = 0
241 [pid 1224726] exit_group(0)             = ?
242 [pid 1224726] +++ exited with 0 +++
243 [pid 1224714] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224726, si_uid=0, si_status=0, si_utime=0, si_stim    e=0} ---
244 [pid 1224723] exit_group(0)             = ?
245 [pid 1224723] +++ exited with 0 +++

# PID 1224723 부모는 PID 1224714
226 [pid 1224714] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224723 attached

# PID 1224714의 부모는 PID 1224711
# PID 1224711은 정확히는 프로세스가 아니라 스레드
207 [pid 1224711] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224714 attached

# 스레드 PID 1224711의 부모는 스레드 PID 1224709
197 [pid 1224709] clone(child_stack=0xc00022c000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
strace: Process 1224711 attached

# 스레드 PID 1224709의 부모는 스레드 PID 1224706
# 191 라인의 로그 뒷 부분을 분리해서 보아야 함
190 [pid 1224706] clone(child_stack=0xc00021a000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
<unfinished ...>
191 strace: Process 1224709 attached
193 [pid 1224706] <... clone resumed>, tls=0xc00005b198) = 1224709

# 스레드 PID 1224706의 부모는 PID 1224703(containerd-shim 프로세스)
184 [pid 1224703] clone(child_stack=0xc000070000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
strace: Process 1224706 attached
185 , tls=0xc000058e98) = 1224706

# 주요 포인트! - containerd-shim(PID 1224703)으로 부터 생성되는 스레드 PID 1224710의 생성
182 [pid 1224703] clone(child_stack=0xc000074000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLSstrace: Process 1224705 attached
183 , tls=0xc000058798) = 1224705

# (PR_SET_CHILD_SUBREAPER, 1) 속성을 추가하는 스레드 PID 1224710
191 [pid 1224705] clone(child_stack=0xc000216000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|    CLONE_SETTLS
192  <unfinished ...>
195 [pid 1224705] <... clone resumed>, tls=0xc000100798) = 1224710
196 [pid 1224710] prctl(PR_SET_CHILD_SUBREAPER, 1) = 0

 

[다이어그램] Pause 컨테이너 프로세스 생성 단계

 

 Pause 컨테이너 하나를 생성하는데도 이렇게 많고 복잡한 과정을 거칩니다.(심지어 이것도 많은 단계들이 생략된 것입니다.)이 복잡한 과정의 주요 내용을 요약해 보면 다음과 같습니다.

단계 내용 비고
1 단계 - containerd-shim 프로세스에서 시작하여 여러 프로세스와 스레드를 거치면서 Pause 컨테이너 프로세스를 생성
- Pause 컨테이너가 생성되고 나면 containerd-shim과 Pause 프로세스 사이의 중간 단계 프로세스와 스레드는 모두 종료하여 Pause 컨테이너 프로세스를 고아(Orphan) 상태로 만들어 입양 보낼 준비를 진행
 
2 단계 - containerd-shim의 또 다른 스레드에서 PR_SET_CHILD_SUBREAPER를 선언
- 이로인해 containerd-shim도 Subreaper 속성을 부여 받음
1단계와 거의 동시에 일어남
3 단계 - 고아가 된 Pause 컨테이너가 가장 가까운 reaper를 찾음
- Subreaper 속성이 부여된 containerd-shim 프로세스가 Pause 컨테이너 프로세스를 입양
 

하청의 하청... 저수준 컨테이너 런타임 runc의 역할

 Kubernetes에서 Pod 생성 요청이 들어오면 노드의 kubelet 데몬이 container runtime(containerd)에게 컨테이너 생성을 명령합니다. 그러면 그 명령을 받은 containerd는 containerd-shim을 통해 컨테이너 프로세스를 구동시킵니다. 그런데, 사실 containerd-shim 역시도 직접 컨테이너를 만드는 것이 아니라 runc라는 저수준 컨테이너 런타임을 통해 실제 컨테이너를 생성합니다. 정말 하청에 하청을 몇 번이나 내리는지...

 

 다시 한번 strace.logs 파일을 살펴봅시다. Pause 컨테이너와 Nginx 컨테이너가 생성되는 시점을 유심히 살펴보면서 runc가 어떤 역할을 하는지 알아봅시다. 이번에는 PID 1224714를 중심으로 살펴보겠습니다. 그 이유는 PID 1224714가 runc를 실행하기 때문입니다.

# containerd-shim의 스레드인 PID 1224711이 PID 1224714 프로세스를 생성
[pid 1224711] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224714 attached
[pid 1224714] prctl(PR_SET_PDEATHSIG, SIGKILL) = 0

# PID 1224814 프로세스가 runc 명령어를 실행하면서 runc로 변형됨 
# runc create 명령을 통해 컨테이너 격리공간(Namespace, Cgroups)만 만들고, 프로세스는 실행하지 않음
[pid 1224714] execve("/usr/local/bin/runc", ["/usr/local/bin/runc", "--root", "/run/containerd/runc/k8s.io", "--log", "/run/containerd/io.containerd.ru"..., "--log-format", "json", "--systemd-cgroup", "create", "--bundle", "/run/containerd/io.containerd.ru"..., "--pid-file", "/run/containerd/io.containerd.ru"..., "c8b394369634a6c6755d4e9094907a92"...], 0xc000283030 /* 13 vars */ <unfinished ...>
[pid 1224711] <... clone resumed>, parent_tid=[15]) = 1224714
[pid 1224714] <... execve resumed>)     = 0
[pid 1224714] prctl(PR_SET_CHILD_SUBREAPER, 1) = 0
[pid 1224714] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORKstrace: Process 1224722 attached
[pid 1224714] <... clone resumed>, parent_tid=[17]) = 1224722

# PID 1224714는 PID 1224723을 생셩
# PID 1224723이 runc init을 사용하여 컨테이너 초기화 작업을 진행
# 이후 PID 1224726 > Pause 컨테이너(PID 1224727)로 이어지는 과정
[pid 1224714] clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLDstrace: Process 1224723 attached
[pid 1224723] execve("/proc/self/fd/6", ["/usr/local/bin/runc", "init"], 0xc0001ff7c0 /* 7 vars */ <unfinished ...>
[pid 1224714] <... clone resumed>, parent_tid=[18]) = 1224723
[pid 1224723] <... execve resumed>)     = 0
[pid 1224723] prctl(PR_SET_DUMPABLE, SUID_DUMP_DISABLE) = 0
[pid 1224723] prctl(PR_SET_NAME, "runc:[0:PARENT]"...) = 0
[pid 1224723] clone(child_stack=0x7ffec80fe4c0, flags=CLONE_PARENT|SIGCHLDstrace: Process 1224726 attached
[pid 1224723] exit_group(0)             = ?
[pid 1224723] +++ exited with 0 +++
[pid 1224714] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224726, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
[pid 1224714] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224723, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
[pid 1224714] exit_group(0)             = ?
[pid 1224714] +++ exited with 0 +++
[pid 1224711] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1224714, si_uid=0, si_status=0, si_utime=0, si_stime=1 /* 0.01 s */} ---

 위 내용을 해석해 보면 다음과 같습니다.

  1. containerd-shim의 스레드인 PID 1224711이 프로세스 PID 1224714를 생성
  2. 프로세스 PID 1224714는 'runc create'로 Namespace와 Cgroups만 만들고 일시 중지
  3. PID 1224714가 PID 1224723을 생성하여 'runc init'으로 컨테이너 프로세스 초기화 진행
  4. 이후 PID 1224726 생성 → Pause 컨테이너(PID 1224727) 생성으로 이어짐

 여기서 한 가지 짚고 넘어가야 할 것이 있습니다. 바로 PID 1224723PID 122476입니다. 다른 프로세스와 다르게 이들만 특별하게 CLONE_PARENT 설정을 사용하고 있는데, 이는 runc의 특별한 역할 때문입니다. runc의 역할은 오로지 컨테이너 생성과 실행에만 있으며, 이것이 끝나면 바로 종료됩니다. 따라서 CLONE_PARENT 옵션을 통해 미리 고아가 될 컨테이너 프로세스의 부모를 더 상위 프로세스로 지정해 주어 프로세스 상속 관계의 안정성을 확보합니다.

 참고로, runc는 OCI(Open Container Initiative) 표준을 준수하는 저수준 런타임으로 containerd가 이를 사용함으로 해서 역시 OCI 표준을 만족할 수 있게 되는 것입니다.

 

 추가로 Nginx 컨테이너(PID 1224754) 역시도 그 프로세스를 추적해 보면 우리가 지금까지 Pause 컨테이너를 추적했던 것과 거의 흡사한 내용을 확인할 수 있을 것입니다.

[분석] 왜 containerd는 containerd-shim을 입양 보냈을까?

 왜 이런 복잡한 과정을 거쳐서 컨테이너를 만들까요? 이것이 바로 Daemonless Architecture라는 설계 철학의 구현이기 때문입니다. 만약 containerd-shim이 containerd의 자식 프로세스라면 container runtime 업그레이드와 같은 상황에서 모든 서비스가 중지되는 문제가 발생할 것입니다. 하지만 containerd-shim을 의도적으로 고아(Orphan)로 만들고 systemd로 입양 보내면서 container runtime과 컨테이너 프로세스 간의 결합도를 분리하고 고가용성을 확보할 수 있게 된 것입니다.

 

 또한, runc의 경우에는 의도적으로 컨테이너 프로세스의 부모 프로세스로 존재하지 않게 하기 위해 CLONE_PARENT 옵션을 주어 실행, 미리 더 상위 프로세스로 입양되게 하여 프로세스 상속 관계의 안정성을 확보하는 것은 매우 영리한 설계라 할 수 있겠습니다.

[결론 및 예고] 리소스를 통제하는 보이지 않는 손: Cgroup

 지금까지 프로세스 트리가 어떻게 '생명 주기'의 안정성을 위해 분리되었는지 확인해 보았습니다. Daemonless Architecture와 영리한 결합도 분리를 통해 시스템 안정성을 확보하기 위해 이런 복잡한 과정을 거친다는 것이... 리눅스 시스템은 파고들수록 정교하면서도 또 이해하기에 어렵다고 느껴집니다. 그런데 여기서 한 가지 의문이 듭니다.

분명 containerd와 containerd-shim은 서로 남남이 되었는데 어째서 systemctl status containerd 명령어에서 Cgroup에 함께 보일까?

 

 다음 포스팅에서는 pstree 뒤에 숨겨진 리소스를 통제하는 보이지 않는 손 Cgroup과 Systemd Slice의 이중적인 구조에 대해서 파해쳐 보도록 하겠습니다.

 

[Oracle Cloud] OCI Free Tier 소개

홈랩의 확장을 위해 여러 CSP를 찾아보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능과 사용량을 보장하기 때문

tech-recipe.tistory.com

 

 

[Oracle Cloud] OCI CLI 사용하기

[Oracle Cloud] OCI Free Tier 소개홈랩의 확장을 위해 여러 CSP를 찾아 보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능

tech-recipe.tistory.com

 지난 포스팅에서 Oracle Cloud(이하 OCI)에서 무료로 제공하는 클라우드 리소스와, OCI CLI를 설치하고 세팅하는 방법에 대해서 알아보았습니다. 이번 포스팅에서는 IPsec VPN을 사용, BGP Site-to-Site VPN을 구현하여 OCI(Cloud)와 홉랩(On-premise)을 연결함으로써 하이브리드 클라우드 환경의 기본인 사이트 간 네트워크 연동을 구성하는 방법에 대해 알아보겠습니다.


1. 하이퍼바이저를 통한 인프라 가상화에서 Kubernetes Native로: Site-to-Site VPN 도입 배경

 지금까지 운영하던 홈랩은 On-premise 데이터센터를 모방한 모습이었습니다. 물론, 데이터 센터의 발끝도 따라가지 못했지만 그래도 하이퍼바이저를 통해 인프라를 가상화 하고, OPNsense를 통해 네트워크 Segmentation을 실현하여 용도에 맞게 네트워크를 격리하여 운영할 수 있었습니다.

 그러나 얼마전, 하이퍼바이저를 통한 인프라 가상화 환경을 버리고 Kubernetes Native 환경을 구축하기로 계획을 세웠습니다. 그러면서 자연스럽게, 지금까지 매뉴얼 한 인프라 조작 방식을 IaC(Infrastructure as Code)로 전환하기로 마음먹었습니다. 이를 구현하기 위해서 Cluster API를 도입이 필요하다 판단하였고, 이를 구현하기 위해 추가적인 인프라가 필요했습니다. Cluster API의 Management 클러스터 역할을 수행할 Kubernetes 클러스터가 필요했는데 이를 클라우드를 사용해서 구성해 보면 어떨까 하는 생각을 하게 되었습니다.

 가장 비용이 저렴하면서도 적당한 성능을 제공하는 클라우드 서비스를 찾던 중 눈에 띈것이 OCI였습니다. 그렇다면 OCI 내의 인프라와 홈랩을 구성하는 리소스들 간에 통신이 필요하게 됩니다.

 물론 퍼블릭 네트워크를 통해서도 충분히 구성이 가능하겠지만, Enterprise 환경을 모사하기 위해서 Site-to-Site VPN을 도입하기로 결정합니다. 이를 통해, 클라우드 인프라와 홈랩 리소스 간에 암호화된 통신을 제공하여 보안성과 기밀성을 향상할 수 있으며 두 Site 간의 네트워크를 L3 영역에서 통합할 수 있게 되어 더욱 유연한 인프라 구성이 가능할 것이라 판단하였습니다.

2. Site-to-Site VPN 구축을 위한 준비

1) 네트워크 준비

 Site-to-Site VPN을 구축하기 위해서는 우선적으로 두 Site간에 네트워크 CIDR 설계가 필요합니다. 두 Site가 VPN을 통해 거대한 L3 네트워크로 통합되기 때문에 네트워크 CIDR이 겹치게 되면 통신에 문제가 발생할 수 있습니다. 게다가 저의 경우 Cilium BGP Controlplane을 통해서 Service와 Pod CIDR을 투명하게 물리 네트워크 레벨로 광고할 계획까지 가지고 있기 때문에 네트워크 설계에 조금 더 주의가 필요했습니다. 아래 표는 네트워크 설계의 큰 틀로 이를 기반으로 상세한 네트워크를 설계하였습니다.

용도 Client 구현 비고
일반 용도의 Home Network 데스크탑, Bastion Instance, 핸드폰, IoT 장비 등 Private C Class 서브네팅 DHCP 주소 예약 및 방화벽 설정을 통한 네트워크 접근 제어
Homelab Infra Network Kubernetes Node, NAS, Switch 등 홈랩 구성 요소 Private B Class 서브네팅 Site-to-Site VPN을 통해 연결할 네트워크
OCI VCN(Virtual Cloud Network) OCI Instance Private A Class 서브네팅  
Workload Network Kubernetes Pod, Service Private A Class 서브네팅 Cilium BPG를 활용하여 L3 네트워크에 투명하게 광고
Site-to-Site VPN 구현 VTI(Vritual Tunnel Interface) P2P (RFC 3021) BGP 피어링 용도로도 사용

2) 인프라 준비

 OCI에는 VCN(Virtual Cloud Network, AWS의 VPC와 동일한 개념)와 Public Subnet이 필요합니다. 그리고 On-premise 환경(홈랩)에는 CPE(Customer-Premises Equipment)가 될 IPsec을 지원하는 장비가 필요합니다. 이번 포스팅에서는 공인 IP를 받는 OPNsense가 CPE 역할을 할 것입니다.

3. Site-to-Site VPN 네트워크 토폴로지

Site-to-SIte VPN 토폴로지

 다소 복잡한 Site-to-Site VPN의 토폴로지입니다. 각각의 요소들과 그 역할에 대해서 하나씩 정리해 보겠습니다. 각 요소들에 대해 잘 이해한다면, OCI와 OPNsense가 아닌 환경에서도 충분히 IPsec을 활용하여 Site-to-Site VPN 구축이 가능할 것입니다.

1) OCI 측 리소스

  • DRG(Dynamic Routing Gateway) - 이름에서 알 수 있듯 VCN 내의 동적 라우팅 경로를 담당합니다. OCI VCN의 트래픽 중 인터넷을 향하는 트래픽이 아닌 Homelab을 향하는 트래픽에 대한 라우팅을 수행합니다. Site-to-Site VPN의 실제 구현을 담당하며, Oracle 측의 실제 라우터를 추상화한 요소라 보면 됩니다.
  • Public Subnet - OCI 측 Subnet으로, 이곳에 인스턴스가 위치하고, 이 네트워크에 대한 정보를 BGP를 통해 라우팅 합니다.
  • IGW(Internet Gateway) - OCI 측 인스턴스가 인터넷과 통신하기 위한 게이트웨이입니다. VPN 설정을 통해 홉랩으로 향하는 트래픽을 제외한 트래픽은 IGW를 향하게 됩니다.
  • OCI IPsec Tunnel Interface(VTI, Vritual Tunnel Interface) - Site-to-Site VPN의 핵심 컴포넌트라 할 수 있겠습니다. OCI에서는 고가용성(HA)을 위해 2개의 터널 인터페이스를 제공하며, 이를 통해 CPE(Customer-Premisese Equipment)와 VPN 연결을 맺습니다. VTI는 Public IP와 Private IP 2개를 가지는데, Public IP는 OCI에서 랜덤 하게 제공해 주며, Private IP는 사용자가 직접 지정하게 됩니다.
  • OCI BGP ASN - Homelab과 BGP 피어링을 맺을 때 사용할 AS Number입니다. BGP 피어링은 VTI의 Private IP를 통해서 이루어집니다.

2) Homelab 측 리소스

  • CPE(Customer-premisees Equipment) - OCI와 Site-to-Site VPN을 맺기 위한 라우터로, 이번 포스팅에는 OPNsense를 기준으로 설명합니다. IPsec VPN 기능을 지원하여야 하며, BGP를 통한 동적 라우팅을 제공한다면 두 Site 간 네트워크 라우팅 정보 교환을 자동으로 할 수 있게 됩니다.
  • Homlab IPsec Tunnel Interface - OCI 측의 VTI가 2개에 대응하는 Homelab 측 인터페이스로, Private IP를 가집니다. OCI 측 VTI와 쌍을 이루는 VTI는 같은 CIDR 블록(/31)에 속하게 됩니다. OCI 측과 다른 점은, OCI의 VTI의 경우 Public IP와 Private IP를 동시에 가지지만, Homelab의 VTI의 경우에는 OPNsense IPsec VPN 설정에서 생성하는 가상 인터페이스가 되며, Pirvate IP만을 가집니다. Public IP는 OPNsense가 ISP로부터 공급받는 IP 하나에 매핑됩니다.
  • FRR Plug-in - BGP 피어링을 위해 플러그인으로, 간단히 설치가 가능합니다.

4. IPsec을 통한 Site-to-Site VPN 설정

 Site-to-Site VPN의 설정은 다음과 같은 절차를 거칩니다.

  1. OCI Portal을 통한 IPsec 설정
  2. CPE 설정을 통해 OCI가 Homelab을 인식하도록 설정
  3. OPNsense(Homelab) 설정을 통해 CPE가 OCI를 인식하도록 설정
  4. Homelab 측과 OCI 측의 BGP 피어링
  5. Homelab 측 라우팅 경로 재분배 설정 및 방화벽 설정
  6. OCI 측 라우팅 테이블 추가 및 Security List 설정
  7. 두 Site 간 연결 테스트

 위 절차에 대해서 상세한 가이드는 다음과 같습니다. 처음 Site-to-Site VPN을 설정하는 분들도 그 개념을 이해하실 수 있도록 최대한 쉽게 설명해 보도록 하겠습니다.


1) OCI Portal을 통해 IPsec 설정

 먼저 OCI 측에 VCN(Virtual Cloud Network, AWS의 VPC와 동일한 개념)를 하나 생성해야 합니다. OCI 웹 콘솔에서 Networking > Virtual cloud networks > Actions > Start VCN Wizard을 선택합니다.

VCN Wizard를 통해 VCN 생성
왼쪽(Create VCN with Internet Connectivity) 선택
기본 설정값 입력

 

 VCN의 이름을 정해줍니다. CIDR값은 기본으로 입력된 값을 사용하도록 하겠습니다. 필요에 의해 변경할 수도 있습니다. 다음으로 다시 Start VCN Wizard 기능을 통해 Add Internet Connectivity and SIte-to-Site VPN to a VCN을 선택하여 Site-to-Site VPN을 구성합니다.

오른쪽(Add Internet Connectivity and Site-to-Site VPN to a VCN) 선택
VCN 선택 및 DRG 생성

 Site-to-Site VPN을 설정한 VCN을 선택(방금 만든 VCN)하고, DRG를 생성합니다. 앞에서도 설명했지만 이 DRG를 통해 Site-to-Site VPN을 구성하고, On-Premise와 Cloud 간에 BGP를 통한 라우팅 정보 교환을 할 수 있게 됩니다.

DRG와 연결될 VCN 내의 Subnet을 선택

 DRG와 연결되어 동적 라우팅 경로 광고 대상이 될 Subnet을 선택합니다. 여기에서는 Public Subnet을 선택하도록 하겠습니다.

Routing type으로 BGP를 선택

 3단계에서는 라우팅 타입 선택 및 터널 인터페이스(VTI)에 대한 설정을 하게 됩니다. 우선 Routing type에 BGP dynamic routing을 선택합니다. 그리고 아래로 화면을 드래그하면 Tunnel 1 / Tunnel 2가 보이는데 이곳에서 VTI에 대해서 설정할 수 있습니다.

 Tunnel(VTI) 설정에 총 4가지 항목을 입력하게 됩니다. 각 항목에 대한 설정값은 아래와 같이 하였습니다. 이것은 절대적인 값은 아닙니다. 상황에 따라 변경해서 사용하면 됩니다.

  IKE version Your BGP ASN IPv4 inside tunnel interface
- CPE
IPv4 inside tunnel interface
- Oracel
Tunnel 1 IKEv2 65000 10.1.0.0/31 10.1.0.1/31
Tunnle 2 IKEv2 65000 10.2.0.0/31 10.2.0.1/31
  • IKE version: Internet Key Exchange 버전으로 IKEv2가 최신 버전이고 효율성이 더 높음
  • Your BGP ASN: Homelab 측 OPNsense Router의 BGP ASN 번호
  • IPv4 inside tunnel interface - CPE: Homelab 측의 VTI의 사설 IP 주소
  • IPv4 inside tunnel interface - Oralce: 오라클 측의 VTI의 사설 IP 주소

VTI와 IPsec Tunnel 다이어그램

 위 다이어그램을 보면 각 인터페이스가 서로 어떤 역할을 하는지 좀 더 쉽게 이해할 수 있을 것입니다. VTI는 내부적으로는 /31의 CIDR을 사용하며, 외부적으로는 공인 IP를 통해 통신하게 됩니다. 그러면서 IKE를 통해 IPsec Tunnel을 생성하게 됩니다. 또한 BGP ASN 설정에서 눈치채셨겠지만 이 VTI 들을 통해 BGP 피어링을 맺고, 라우팅 정보도 교환합니다.

CPE(Homelab) 측 공인 IP 입력

 마지막으로, CPE(Homelab) 측의 IPsec Tunnel을 구성할 장비의 Public IP 주소를 입력합니다. 저의 경우에는 OPNsense의 공인 IP 주소를 입력하였습니다. 이렇게 하면 OCI가 CPE(Homelab)을 인식할 수 있도록 하는 설정은 마무리되었습니다.

 

 이번 단계에서 생성한 것들을 정리해 보면 아래와 같습니다.

  • OCI VCN, Subnet
  • Site-to-Site VPN을 위한 DRG
  • IPsec Tunnel을 위한 Tunnel Interface 2개, Shared secret, 공인 및 사설 IP 주소
  • CPE(OPNsense) 측 정보

2) CPE(OPNsense)측 설정을 통해 OPNsense 측에서 OCI를 인식하도록 설정

 앞선 설정이 잘 마무리되었다면, OCI 측에 Tunnel에 각각 Public IP 주소가 할당되며, IKE Secret이 생성됩니다. OCI의 Site-to-Site VPN 메뉴에서 Tunnels 탭을 통해 이 값들을 확인할 수 있습니다. 이 값들을 OPNsense에 설정하여 CPE 측에서 OCI 측을 인식할 수 있도록 합니다.

Tunnel의 Public IP를 확인
각 Tunnel의 IKE Secret을 확인

 위 스크린샷에서 처럼 확인된 Tunnel Interface(VTI)의 Public IP 주소와 Shared secret 정보를 OPNsense 측에 입력해 줍니다. 해당 정보들은 Site-to-Site VPN 메뉴에서 터널 인터페이스의 detail 정보를 확인하면 볼 수 있습니다.

OPNsense Pre-Shared Keys 설정

 OPNsense 설정은 위와 같습니다. OPNsense의 VPN 메뉴에서 IPsec을 선택, Pre-Shared Keys 메뉴를 통해 새로운 항목을 생성합니다. Tunnel 1과 Tunnel 2의 정보를 사용하여 2개의 항목을 생성합니다.

OPNsense VTI 생성

 이제 OPNsense 측에 VTI(Virtual Tunnel Interface)를 생성하여 OCI와 IPsec VPN 생성으로 할 수 있도록 합니다. Reqid 값이 중요한데 이 값을 통해서 VTI와 아래 Connection 설정에 나오는 Child가 서로 연결되게 됩니다. 즉 VTI와 Child의 내부 연결을 위한 고유한 식별자라고 보면 됩니다.

Connections 생성

 다음은 IPsec Connectios를 설정하는 작업입니다. advanced mode를 활성화하고, Proposals를 aes256-sha1-modp2048 [DH14]로 설정합니다. 이 옵션은 256비트 AES로 데이터를 암호화하고, SHA-1로 무결성을 검증하며, 2024 비트 Diffie-Hellman 그룹 14를 사용하여 세션키를 교환하는 보안 정책입니다. Vesion은 IKEv2로 설정하고, Local address에는 OPNsense의 Public IP 주소를, Remote addresses에는 OCI 측 Tunnel interface 1의 Pubclic IP 주소를 설정합니다. 그리고 맨 아래 Description을 작성하고 Save를 클릭합니다.

Local Authentication 생성
Remote Authentication 생성

 Save 후에 아래에 새로운 메뉴가 생깁니다. Local Authentication을 추가합니다. 특별히 설정할 것은 없고 기본 값이 채워진 상태로 Save를 누르면 됩니다. Remote Authentication 역시 마찬가지입니다.

Child 생성, 반드시 Policies 체크박스를 해제, Repid 값 주의

 다음으로 Child를 생성합니다. 여기에서 한 가지 주의할 점이 있는데, 반드시 Policies를 체크 해제 해야 합니다. 만약 이를 체크하고 만들게 되면 모든 트래픽이 하이제킹 당해 OCI 쪽으로 흘러가게 되어 네트워크가 마비될 수 있습니다. Reqid는 Local과 Remote 옵션에 모두 0.0.0.0/0을 입력합니다. Policies 체크를 반드시 해제하는 것에 주의하며, 위 과정을 한번 더 반복하여 Tunnel 2에 대해서도 설정을 생성합니다.

IPsec VPN 실행

 마지막으로 IPsec VPN 서비스를 실행하여 Site-to-Site VPN을 구현합니다. 연결이 잘 되면 다음과 같은 화면을 확인할 수 있습니다.

Status Overview에서 VTI가 모두 활성화 되어 있는것을 확인

 

OCI 측에서도 IPsec 상태 확인 가능

 

이번 단계에서 생성한 것들을 다시 정리해 보면 다음과 같습니다.

  • Pre-Shared Keys: OCI Tunnel Interface 생성하면서 함께 발급된 Secret 값 2개를 통해 생성
  • VTI 2개 생성: OPNsense 및 OCI 측 Tunnel Interface의 공인 IP 주소 및 사설 IP 주소를 설정값으로 하여 생성
  • Connection 2개 생성: Local/Remote Authentication 2개 생성, Child 설정에서 Reqid 값을 통해 VTI와 연결 

OPNsense에서 OCI의 Tunnel Interface의 Private IP로 Ping 테스트를 통해 통신을 확인

3) Homelab 측과 OCI 측의 BGP 피어링

  위 스크린샷에서 볼 수 있듯, 두 사이트 간 VTI 연결은 문제 없이 되었는데, BGP 설정이 되어 있지 않은것을 확인할 수 있습니다. 즉, 두 사이트간 VPN 터널은 생성되었으나, BGP를 통한 동적 라우팅 경로 생성은 되지 않고 있는 상태입니다. 우선 OPNsense 측 설정부터 시작하겠습니다.

BGP의 Route Redistribution 설정

 먼저 OPNsense의 Routing > BGP의 첫 화면에서 Route Redistribution을 리스트를 생성합니다. 경로 재분배 옵션을 Connected routes로 선택하여, 현재 OPNsense와 연결되어 있는 라우팅 경로를 광고 후보로 둡니다.

Prefix Lists 설정

 다음은 실제로 광고할 네트워크 주소 대역을 설정하는 Prefix Lists입니다. 가상머신의 네트워크 대역인 192.168.2.0/24를 목록에 추가합니다. Premit 옵션은 Network 옵션에 있는 네트워크를 광고하겠다는 의미입니다. Name은 이 Prefix Lists를 대표하는 그룹의 이름으로 같은 Namse을 사용하는 Prefix Lists들은 동시에 네트워크 광고 옵션에 적용됩니다. Sequence Number를 통해 여러 규칙을 하나의 Prefix Name 안에 그룹화할 수 있습니다. 저는 추가로 172.24.0.0/24도 추가해 보았습니다.

BGP Neighbor 설정

 다음 단계는 BGP neighbor를 설정하는 단계입니다. Peer-IP는 OCI 측 Tunnel Interface의 Private IP가 됩니다. Remote AS는 OCI 측의 BGP ASN인데 이것은 특별한 설정을 하지 않았으면 31898이 기본값입니다. Update-Source Interface는 OPNsense 쪽 VTI가 됩니다. 주의할 점은 Peer-IP와 VTI가 앞선 IPsec 설정에서 서로 터널을 맺은 쌍이어야 한다는 점입니다. Soft reconfiguration inbound 옵션을 활성화하여 세션을 재시작하지 않고도 정책을 변경할 수 있도록 합니다. 마지막으로 Prefix-List Out을 통해 광고할 네트워크 대역을 설정합니다. Tunnel 1과 같은 내용으로 Tunnel 2에 대한 BGP Neighbor를 설정합니다.

 

 이번 단계에서는 아래와 같은 요소들을 만들고 설정하였습니다.

  • Route Redistribution 리스트
  • Prefix List

OPNsense vtysh를 통해 BGP 상태 확인

 추가로 OPNsense에 vtysh를 통해 BGP 상태를 확인할 수 있습니다. BGP를 통해 Static 하게 설정하지 않아도 네트워크 대역을 자동으로 광고하고 또 받고 있는 것을 확인할 수 있습니다.

4) 방화벽 설정 

 방화벽은 2가지 룰을 추가합니다. 하나는 OPNsense의 Public IP가 부여되는 WAN 인터페이스, 하나는 IPsec이라는 이름을 가진 인터페이스입니다. 우선 방화벽에서 Aliase를 만들어 줍니다. OCI 측 Tunnel Interface의 Public IP 주소에 대해서 하나, 그리고 Port 번호 500과 4500에 대해서 Aliase를 만듭니다. 이를 가지고 WAN Interface에 방화벽 설정을 진행합니다.

방화벽 규칙 설정

  • WAN Interface 방화벽 규칙
Rule Name Action Direction Protocol Source Destination Destination Port
ESP Rule Pass in ESP OCI IPsec Pubelic IP Address OPNsense WAN Interface None
IKE Rule Pass in UDP OCI IPsec Pubelic IP Address OPNsense WAN Interface 500
NAT-T Rule Pass in UDP OCI IPsec Pubelic IP Address OPNsense WAN Interface 4500

 다음은 IPsec 인터페이스입니다. 이 인터페이스는 특이하게도 Interfaces 목록에는 보이지 않고, 방화벽 Rules 목록에만 존재합니다. 이 인터페이스에 대한 방화벽 설정은 매우 특이한 모습을 보이는데, Homelab에서 OCI 측으로의 통신이 아닌, OCI에서 Homelab 측으로의 통신에 영향을 주는 방화벽 룰이라는 것입니다. 따라서 OCI 측에서 Homelab으로의 접근을 제어하고 싶다면 이 방화벽 설정이 중요한 역할을 합니다. 이번 포스팅에서는 모든 소스에 대해서 모든 프로토콜에 대해 Pass 룰을 적용하겠습니다.

OCI 측으로 부터 접근을 제어하는 IPsec 방화벽 규칙

  • IPsec 인터페이스 방화벽 규칙
Rule Name Action Direction Protocol Source Destination Destination Port
All allow Pass in Any Any Any Any

5) OCI 측 Routing Tables 추가 및 Security List 설정

 BGP를 통해서 네트워크 대역이 광고되고 있긴 하지만, OCI VCN에 Routing rule을 설정해 주어야 합니다. 아래와 같이 OCI에서 Homelab의 Subnet으로 통신할 수 있도록 라우팅 경로를 추가해 줍니다.  라우팅 경로의 Target은 반드시 DRG가 되어야 다른 사이트로의 통신이 가능하다는 점에 유의해야 합니다.

Routing 경로 설정
Target이 DRG인것을 확인

 다음은 Security List로 방화벽 규칙과 같은 것을 적용해 줍니다. 이 VCN에 기본적으로 존재하는 규칙인 22번 포트에 대한 TCP 연결(SSH) 허용과, ICMP 프로토콜에 대한 차단 규칙 이외에, Homelab의 특정 네트워크에서 발생하는 ICMP 규칙도 추가합니다. 이를 통해서 Homelab 측 VM에서 OCI 측 Instance에 Ping 테스트를 할 수 있게 합니다.

Homelab에서 접근하는 ICMP 프로토콜 트래픽을 허용하는 규칙

6) 두 Site 간 연결 테스트

 이제 정말 두 사이트 간 통신이 잘 작동하는지 확인할 차례입니다. 두 사이트에 테스트에 사용할 VM과 Intance를 생성합니다.

위치 IP Address 적용 받는 규칙
Homelab 192.168.2.250 - OPNsense LAN 인터페이스 방화벽 Egress 규칙
- OCI VCN의 Default Ingress Rules
OCI 10.0.0.9 - OCI VCN의 Default Egress Rules
- OPNsense IPsec 인터페이스 방화벽 Ingress 규칙

Homelab VM → OCI Instance 통신 확인
OCI Instance → Homelab 통신 확인

  • Homelab VM(192.168.2.250)에서 OCI Instance(10.0.0.9)로 ping 테스트 및 ssh 접근 테스트 성공
  • OCI Instance(10.0.0.9)에서 Homelab VM(192.168.2.250)으로 Ping 테스트 및 ssh 접근 테스트 성공

 OCI Home Region이 싱가포르이라 Ping이 약 100ms 정도에 달하긴 하지만, 통신 품질 자체는 아주 안정적인 것을 확인할 수 있습니다.


Network Visualizer를 통해 Site-to-Site VPN의 연결 상태를 확인

 이렇게 해서 두 Site 간 호스트들이 Private Network를 통해 통신할 수 있는 Site-to-Site VPN을 IPsec Tunnel을 사용하여 구현해 보았습니다. 구축하면서 방화벽 규칙 때문에 단방향으로만 통신이 된다던가, 혹은 BGP 피어링이 잘 맺어지지 않는 상황도 있었습니다. 대부분 저의 실수였고, 실수를 통해 많이 배울 수 있었습니다. 여담이지만, 엔지니어에게 이렇게 직접 핸들링할 수 있는 홈랩이 있다는 것은 여러 기술을 직접 채득 하기에 너무나도 좋은 환경인 것 같습니다.(망가지더라도 제가 고치면 되니까, 그 부분에 대한 부담도 적어서 좋구요...)

 

 어쨌든, 이번 구축을 통해 다음 단계로 나아갈 수 있게 되었습니다. 이제는 OCI Instance에 단일 노드 Kubernetes를 구성하고 Cluster API Managmenet Cluster를 구축할 차례입니다. 그리고 이를 통해 Homelab의 MiniPC 노드들을 프로비저닝 하는 것을 목표로 또 스터디해야겠습니다. 긴 포스팅 읽어주셔서 감사합니다.

 

[Oracle Cloud] OCI Free Tier 소개

홈랩의 확장을 위해 여러 CSP를 찾아 보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능과 사용량을 보장하기 때

tech-recipe.tistory.com

 이전 포스팅에서, 오라클 클라우드의 Free Tier에서 제공하는 기능에 대해서 알아보았습니다. 이번 포스팅은 오라클 클라우드를 제어하는데 필요한 CLI 도구를 설치하고, 사용하는 방법에 대해서 알아보겠습니다.


1. OCI CLI 설치하기

 

Quickstart

This section documents how to quickly install and configure the OCI Command Line Interface (CLI).

docs.oracle.com

 이번 포스팅에는 MacOS를 기반으로한 OCI CLI 설치 방법을 설명드립니다. 다른 운영체제에서 OCI CLI를 설치하는 방법은 위 링크를 통해 확인할 수 있습니다.

Brew Install

 MacOS에서는 Homebrew를 사용합니다. 따라서 Homebrew가 먼저 설치되어 있어야 합니다.

CLI Install

brew update && brew install oci-cli

CLI Upgrade

brew update && brew upgrade oci-cli

CLI Uninstall

brew uninstall oci-cli

 

 설치가 완료되면 아래 명령어를 통해 설치를 확인할 수 있습니다.

oci -v

# 2025년 10월 18일 기준
3.68.0

2. Configuration file 설정하기

명령어를 사용한 Setup

 oci setup config 명령어를 사용하면 대화 형식을 통해 Configuration file을 설정할 수 있습니다.

oci setup config

# 아래 내용이 출력됨
    This command provides a walkthrough of creating a valid CLI config file.

    The following links explain where to find the information required by this
    script:

    User API Signing Key, OCID and Tenancy OCID:

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#Other

    Region:

        https://docs.cloud.oracle.com/Content/General/Concepts/regions.htm

    General config documentation:

        https://docs.cloud.oracle.com/Content/API/Concepts/sdkconfig.htm


Enter a location for your config [/Users/<사용자 이름>/.oci/config]: # Enter를 입력하면 기본 경로에 config 파일이 생성됨
Enter a user OCID: # <사용자 OCID> 입력
Enter a tenancy OCID: # <Tenacy OCID> 입력
Enter a region by index or name(e.g.
1: af-johannesburg-1, 2: ap-batam-1, 3: ap-chiyoda-1, 4: ap-chuncheon-1, 5: ap-chuncheon-2,
6: ap-dcc-canberra-1, 7: ap-dcc-gazipur-1, 8: ap-delhi-1, 9: ap-hyderabad-1, 10: ap-ibaraki-1,
11: ap-melbourne-1, 12: ap-mumbai-1, 13: ap-osaka-1, 14: ap-seoul-1, 15: ap-seoul-2,
16: ap-singapore-1, 17: ap-singapore-2, 18: ap-suwon-1, 19: ap-sydney-1, 20: ap-tokyo-1,
21: ca-montreal-1, 22: ca-toronto-1, 23: eu-amsterdam-1, 24: eu-budapest-1, 25: eu-crissier-1,
26: eu-dcc-dublin-1, 27: eu-dcc-dublin-2, 28: eu-dcc-milan-1, 29: eu-dcc-milan-2, 30: eu-dcc-rating-1,
31: eu-dcc-rating-2, 32: eu-dcc-zurich-1, 33: eu-frankfurt-1, 34: eu-frankfurt-2, 35: eu-jovanovac-1,
36: eu-madrid-1, 37: eu-madrid-2, 38: eu-marseille-1, 39: eu-milan-1, 40: eu-paris-1,
41: eu-stockholm-1, 42: eu-zurich-1, 43: il-jerusalem-1, 44: me-abudhabi-1, 45: me-abudhabi-2,
46: me-abudhabi-3, 47: me-abudhabi-4, 48: me-alain-1, 49: me-dcc-doha-1, 50: me-dcc-muscat-1,
51: me-dubai-1, 52: me-ibri-1, 53: me-jeddah-1, 54: me-riyadh-1, 55: mx-monterrey-1,
56: mx-queretaro-1, 57: sa-bogota-1, 58: sa-santiago-1, 59: sa-saopaulo-1, 60: sa-valparaiso-1,
61: sa-vinhedo-1, 62: uk-cardiff-1, 63: uk-gov-cardiff-1, 64: uk-gov-london-1, 65: uk-london-1,
66: us-ashburn-1, 67: us-ashburn-2, 68: us-chicago-1, 69: us-gov-ashburn-1, 70: us-gov-chicago-1,
71: us-gov-phoenix-1, 72: us-langley-1, 73: us-luke-1, 74: us-newark-1, 75: us-phoenix-1,
76: us-saltlake-2, 77: us-sanjose-1, 78: us-somerset-1, 79: us-thames-1): # <사용자 Home region 번호> 입력
Do you want to generate a new API Signing RSA key pair? (If you decline you will be asked to supply the path to an existing key.) [Y/n]: y # 'y' 입력하여 새로운 api 키 생성
Enter a directory for your keys to be created [/Users/<사용자 이름>/.oci]: # Enter를 입력하면 key가 저장될 위치로 기본 위치가 지정됨
Enter a name for your key [oci_api_key]: # OCI Key 이름 지정, Enter를 입력하면 'oci_api_key'라는 기본 이름으로 지정
Public key written to: /Users/<사용자 이름>/.oci/oci_api_key_public.pem
Enter a passphrase for your private key ("N/A" for no passphrase): # Private Key의 passphrase를 입력, 'N/A'를 입력하면 passphrase 없이 진행
Repeat for confirmation: # 위에 입력했던 passphrase를 다시 입력, 'N/A'를 입력했어도 동일하게 입력
Private key written to: /Users/<사용자 이름>/.oci/oci_api_key.pem
Fingerprint: 74:5b:fa:ec:21:22:65:d6:31:c1:7a:08:12:41:0b:67
Config written to /Users/<사용자 이름>/.oci/config


    If you haven't already uploaded your API Signing public key through the
    console, follow the instructions on the page linked below in the section
    'How to upload the public key':

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#How2

 위 대화 형식의 configuration setup을 진행에 userOCID, teancy OCID, region index 정보를 입력해야 합니다. 각 정보를 확인하는 방법은 아래와 같습니다.

userOCID

OCI Profile 메뉴

 userID는 화면 우측 상단의 사람모양 아이콘을 클릭하고, 드롭다운 메뉴의 User settings를 클릭합니다.

 화면에 userOCID값이 출력됩니다. 가급적이면 이 값은 유출되지 않도록 하는게 좋습니다.

tenacyOCID / Home region / Home region key

 TenacyOCID 정보 역시 화면 우측 상단의 사람 모양 아이콘을 클릭하고 나타나는 드롭다운 메뉴에서 Teancy를 클릭하여 확인할 수 있습니다. 이 화면에서는 또한 Home region과 그에 대한 Key 값도 확인할 수 있습니다. Home region key 값을 통해 Region Identifier를 확인할 수 있기 때문에, 대화형 configration file setup에서 region index가 불확실한 경우 꼭 참고하여야 합니다.

 

Regions and Availability Domains

Open the Help menu , go to Support and click Request service limit increase. Enter the following: Primary Contact Details: Enter the name and email address of the person making the request. Enter one email address only. A confirmation will be sent to this

docs.oracle.com

 위 링크는 Region Key와 Region Identifier를 함께 확인할 수 있는 문서로 참고하여 region index를 확인하면 됩니다.

3. API Public key 업로드

 Configration file이 생성되면 기본적으로 홈(~/) 경로에 .oci/ 디렉토리 아래에 oci_api_key_public.pem이라는 파일이 생성됩니다. 해당 파일의 내용을 읽고 이를 OCI 콘솔에 업로드 합니다.

cat ~/.oci/oci_api_key_public.pem

# 아래 공개키 전체 내용을 복사
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA78+/qFddYMqYyA3Lvd9A
<중략>
AZmsXXl3seTPo189zcy3IwEC2PYeW/WLhc1QdINL6P7wWETLzNI0aDpNbNTL+HHH
3wIDAQAB
-----END PUBLIC KEY-----

사용자 Profile > Add API Key
oci_api_key_public.pem 파일의 내용을 입력

4. OCI CLI 설정 확인

 아래와 같은 명령어를 통해 OCI CLI가 제대로 작동하는지 확인할 수 있다.

# 사용자 리스트업
oci iam user list

{
  "data": [
    {
      "capabilities": {
        "can-use-api-keys": true,
        "can-use-auth-tokens": true,
        "can-use-console-password": true,
        "can-use-customer-secret-keys": true,
        "can-use-db-credentials": true,
        "can-use-o-auth2-client-credentials": true,
        "can-use-smtp-credentials": true
      },
      "compartment-id": "<compartment OCID>",
# ... 생략

# 사용자 정보 확인
oci iam user get --user-id <사용자 OCID>
{
  "data": {
    "capabilities": {
      "can-use-api-keys": true,
      "can-use-auth-tokens": true,
      "can-use-console-password": true,
      "can-use-customer-secret-keys": true,
      "can-use-db-credentials": true,
      "can-use-o-auth2-client-credentials": true,
      "can-use-smtp-credentials": true
    },
# ... 생략
}

 위 설정을 통해 Oracle Cloud의 CLI 툴을 로컬에 설치하고, API 공개키 등록을 통해 클라우드 인프라에 명령어를 통해 제어할 수 있는 환경을 구성해 보았습니다.

 

 본격적인 홈랩 구성을 위한 준비단계가 잘 진행되고 있는것 같습니다.

 홈랩의 확장을 위해 여러 CSP를 찾아보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능과 사용량을 보장하기 때문에 1일 개발자나 테스트 용도의 인프라가 필요한 분들이라면 아주 매력적인 클라우드 서비스라 생각됩니다.


Free Tier 가입하기

 

클라우드 서비스 무료 이용

Oracle Cloud Free Tier는 기업에게 무제한으로 사용할 수 있는 상시 무료 클라우드 서비스를 제공합니다.

www.oracle.com

 오라클 클라우드 Free Tier 가입은 위 페이지를 통해 할 수 있습니다. 가입 절차는 간단한 편이고, 신용 카드 정보를 입력하면 여느 클라우드처럼 해외 결제가 이루어졌다가 취소됩니다. 저의 경우에는 홈 리전을 싱가포르로 설정하였기 때문에 1.38 싱가포르 달러가 결제되었다가 취소되었습니다.

Oracel Free Tier 가입 페이지

 최초 가입 시 홈 리전을 선택하게 되는데, 이때 선택된 홈 리전은 변경이 불가능합니다. 또한 홈 리전에서만 Free Tier 제품들을 사용할 수 있으므로 선택에 신중을 가해야 합니다. 저의 경우에는 ARM 기반의 Compute Instance 사용이 주목적이었는데, 한국 리전에는 해당 자원의 여유가 없다는 경고가 있어, 싱가포르를 홈 리전으로 선택하게 되었습니다.

Free Tier에서 제공하는 기능들

 

Access Cloud Services for Free

Oracle Cloud Free Tier provides enterprises with Always Free Cloud Services that can be used for an unlimited time.

www.oracle.com

 오라클 Free Tier는 상당히 다양하고, 넉넉한 기능과 자원을 Free Tier로 제공합니다. 위 페이지에서 다양한 Free Tier 제품군들을 검색해 볼 수 있습니다. 총 27개의 서비스를 Always Free로 제공하고 있으며, 추가로 가입한 30일 동안 300$ 상당의 크레디트를 제공하여 Free Tier가 아닌 제품도 사용해 볼 수 있습니다. 아래는 주요 리소스 카테고리 별 Free Tier 서비스에 대한 요약입니다.

1. Infrastructure

서비스 리소스 상세 내용 비고
Compute AMD 프로세서 기반 VM 인스턴스 - Shape: VM.M.Standard.E2.1.Micro
- 수량: 최대 2개
- 프로세서: 1/8 OCPU (추가 리소스 사용 가능)
- 메모리: 1GB
- 네트워킹: VNIC 1개, 공용 IP 1개, 인터넷 대역폭 최대 50Mbps (지역 내/온프레미스는 최대 480Mbps)
유휴 인스턴스 정책

7일 동안 CPU, 네트워크, 메모리(A1) 사용률이 20% 미만인 유휴 인스턴스는 Oracle에 의해 회수될 수 있음
ARM 프로세서 기반 VM 인스턴스 - Shape: VM.M.Standard.A1.Flex
- 제공량: 월 3,000 OCPU-시간 및 18,000 GB-시간 무료
(총 4 OCPU, 24GB 메모리에 해당, 자유롭게 조합 가능)
- 네트워킹: OCPU 수에 비례하여 확장
Block Volume 블록 스토리지 - 총 용량: 200 GB (부트 볼륨과 블록 볼륨 합산)
- 볼륨 백업: 5개
 
Object Storage 오브젝트 스토리지 - 총 용량: 20 GB (Standard, Infrequent Access, Archive 티어 합산)
- API 요청: 월 50,000건
 
  아카이브 스토리지 오브젝트 스토리지 20GB 용량에 포함됨  
Certificatges 인증서 - 인증 기관(CA): 5개
- 인증서: 150개
 
Valult 키 관리 - 소프트웨어 보호 키: 모두 무료
- HSM 보호 키 버전: 20개
- Vault 시크릿: 150개
 
Resource Manager 인프라 자동화 - 스택: 100개
- 동시 작업: 2개
- 프라이빗 엔드포인트: 1개
 

2. Database

서비스 리소스 상세 내용 비고
Autonomouse Database 자율형 데이터베이스 - 수량: 2개
- 프로세서: 1 OCPU (확장 불가)
- 스토리지: 20 GB (확장 불가)
- 워크로드 유형: TP, Data Warehouse, APEX, JSON 중 선택
- 최대 동시 세션: 20
 
NoSQL Database NoSQL 데이터베이스 - 테이블: 3개
- 스토리지: 테이블당 25GB
- 읽기/쓰기: 월 1억 3300만 건
 
MySQL HeatWave MySQL 데이터베이스 - DB 시스템: 1개
- 스토리지: 50GB
- 백업 스토리지: 50GB
 

3. Networking

서비스 리소스 상세 내용 비고
Load Balancing 로드 밸런서 - 수량: 1개
- Shape: Flexible (최소/최대 10 Mbps)
- 리스너/가상 호스트명/백엔드셋: 각 16개
- 백엔드 서버: 1024개
 
Network Load Balancer 네트워크 로드 밸런서 - 수량: 1개
- 리스너/백엔드셋: 각 50개
- 총 백엔드 서버: 1024개
 
Virtual Cloud Network (VCN) 가상 클라우드 네트워크 - 수량: 최대 2개  
Site-to-Site VPN 사이트 간 VPN - IPSec 연결: 최대 50개  
VCN Flow Logs VCN 흐름 로그 - 용량: 월 10GB (OCI Logging 서비스와 공유)  

4. Observability & Management

서비스 리소스 상세 내용 비고
Monitoring 모니터링 - 수집 데이터 포인트: 월 5억 건
- 조회 데이터 포인트: 월 10억 건
 
Logging 로깅 월 10GB 무료 (VCN 흐름 로그와 공유)  
Application Performance Monitoring APM - 추적 이벤트: 시간당 1,000건
- Synthetic 모니터 실행: 시간당 10회
 
Notifications 알림 - HTTPS 알림: 월 100만 건
- 이메일 알림: 월 1,000건
 
Email Delivery 이메일 전송 - 전송량: 월 3,000건  
Connector Hub 커넥트 허브 - 커넥터: 2개  
Console Dashboards 콘솔 대시보드 - 대시보드: 테넌시당 100개  
Bastion 배스천 모든 계정에 무료로 제공  

5. 추가 서비스

서비스 리소스 상세 내용 비고
Outbound Data Transfer
아웃바운드 데이터 전송 - 전송량: 월 10 TB  

 


 이 정도 수준이면, Compute Instance의 유휴 인스턴스 정책만 잘 준수한다면 소규모 서비스를 하는 데에도 부족함이 없는 수준의 강력한 기능이라 할 수 있겠습니다. 웬만한 개발 환경이나 테스트 환경은 찜 쪄먹는 수준이 아닐까 합니다.

 

 오라클 클라우드의 Free Tier를 활용해서 도전적인 홈랩 프로젝트를 시작해 보려고 합니다. 기존 On-Premise 장비와 클라우드 인프라를 엮어서 하이브리드 클라우드로 진화시켜 볼 예정입니다. 시간은 좀 걸리겠지만, 재미있는 여정이 될 것 같습니다.


 

[Oracle Cloud] OCI CLI 사용하기

[Oracle Cloud] OCI Free Tier 소개홈랩의 확장을 위해 여러 CSP를 찾아 보던 중 오라클 클라우드의 혜자(?) Free Tier 정책이 있어 소개하고자 합니다. CSP 메이저 3사는 아니지만, 랩 환경에서는 충분한 기능

tech-recipe.tistory.com

 

 

환경

  • 화이트박스에 설치된 OPNsense Version 25.7.4
  • Nginx Plugin Version 1.35

증상

  • OPNsense 재부팅 후 외부로 노출된 서비스 접근 불가
  • Nginx Plugin에 아래와 같은 에러가 발생 중

2025-10-07T19:18:54Emergencynginxnginx: configuration file /usr/local/etc/nginx/nginx.conf test failed
2025-10-07T19:18:54Emergencynginxnginx: [emerg] BIO_new_file("/usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919") failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919, r) error:10000080:BIO routines::no such file)

원인

  • 실제 해당 위치(/usr/local/opnsense/data/OPNsense/Nginx/)에 파일(dh-parameters.4096.rfc7919)이 존재하지 않음
  • 재부팅 시 파일이 어떤 이유에서인지 삭제된 것으로 추정
    • 화이트박스의 전원 버튼을 눌러 하드웨어적인 Shutdonw을 진행한 것이 원인으로 추정됨
    • 재부팅 시 다소 불안정한 상태이 로그를 확인할 수 있음
2025-10-06T17:18:45Noticekernel<118>[114] nginx not running? (check /var/run/nginx.pid).
2025-10-06T17:17:24Noticekernel<118>[33] /usr/local/etc/rc.d/nginx: WARNING: failed to start nginx
2025-10-06T17:17:24Noticekernel<118>[33] nginx: [emerg] BIO_new_file("/usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919") failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919, r) error:10000080:BIO routines::no such file)
2025-10-06T17:17:24Noticekernel<118>[33] Starting nginx.
2025-10-06T17:17:24Noticekernel<118>[33] nginx: configuration file /usr/local/etc/nginx/nginx.conf test failed
2025-10-06T17:17:24Noticekernel<118>[33] nginx: [emerg] BIO_new_file("/usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919") failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919, r) error:10000080:BIO routines::no such file)
2025-10-06T17:17:24Noticekernel<118>[33] Performing sanity check on nginx configuration:
2025-10-06T17:17:24Noticekernel<118>[33] /usr/local/etc/rc.d/nginx: WARNING: failed to setup nginx
2025-10-06T17:17:24Noticeroot/usr/local/etc/rc.d/nginx: WARNING: failed to start nginx
2025-10-06T17:17:24Noticeroot/usr/local/etc/rc.d/nginx: WARNING: failed to setup nginx

조치

  • dh-parameters.4096.rfc7919 파일 생성
  • OPNsense SSH 접속 후 아래 명령어 실행 - 파일 생성에 약 10분정도 소요
openssl dhparam -out /usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919 4096

#Generating DH parameters, 4096 bit long safe prime
#...................................................

결론

  • 하드웨어 shutdown 지양 및 정상적인 종료 진행 권장

 

Cilium Study의 긴 여정이 드디어 마지막 주제에 도달했습니다. 우리는 CNI의 기본부터 서비스 메시, 성능 튜닝에 이르기까지 Cilium이 제공하는 강력한 기능들을 단계별로 학습했습니다. 이 모든 기술의 정점에는 바로 '보안' 이 있습니다. 동적이고 예측 불가능한 클라우드 네이티브 환경에서, 어떻게 우리의 애플리케이션과 데이터를 안전하게 보호할 수 있을까요?

이번 마지막 포스팅에서는 기존의 경계 기반 보안 모델의 한계를 짚어보고, Cilium이 제로 트러스트(Zero Trust) 원칙을 어떻게 네트워크 정책에 구현하는지 살펴봅니다. 또한, 네트워크를 넘어 커널 레벨의 행위까지 감지하고 차단하는 eBPF 기반의 혁신적인 런타임 보안 솔루션, Tetragon의 세계로 깊이 들어가 보겠습니다.

1. 경계의 붕괴, 새로운 보안 패러다임의 서막

과거의 데이터 센터 보안은 '성벽과 해자(Castle-and-moat)' 모델에 비유할 수 있었습니다. 외부의 침입을 막기 위해 강력한 방화벽(경계)을 구축하고, 일단 내부망에 들어온 트래픽은 비교적 신뢰하는 방식이었습니다. 하지만 마이크로서비스 아키텍처와 컨테이너 환경은 이러한 경계를 무의미하게 만들었습니다.

  • 동적인 워크로드: 파드는 수시로 생성되고 삭제되며, 노드를 넘나들며 이동합니다. IP 주소는 더 이상 신뢰할 수 있는 식별자가 아닙니다.
  • 내부 트래픽(East-West)의 증가: 대부분의 통신이 클러스터 내부에서 서비스 간에 발생합니다. 만약 하나의 서비스가 침해당하면, 내부망 전체가 위험에 노출될 수 있습니다.

이러한 변화는 제로 트러스트(Zero Trust) 라는 새로운 보안 패러다임을 요구합니다. "Never Trust, Always Verify." 즉, 네트워크의 위치(내부/외부)와 관계없이 모든 통신 요청을 신뢰하지 않고, 반드시 검증해야 한다는 원칙입니다. Cilium은 바로 이 제로 트러스트 원칙을 쿠버네티스 환경에 가장 효과적으로 구현하는 도구 중 하나입니다.

2. Cilium 네트워크 정책, 신원 기반 보안을 구축하다

Cilium은 쿠버네티스 표준 NetworkPolicy를 완벽하게 지원하며, 한 걸음 더 나아가 CiliumNetworkPolicy(CNP)와 CiliumClusterwideNetworkPolicy(CCNP)라는 강력한 자체 CRD를 제공합니다. Cilium 정책의 핵심은 IP 주소가 아닌 신원(Identity)을 기반으로 통신을 제어한다는 점입니다.

신원(Identity)이란 무엇인가?

Cilium은 파드가 가진 레이블(Label) 의 조합을 기반으로 고유한 숫자 형태의 Security Identity를 부여합니다. 예를 들어, app=frontend, env=prod 레이블을 가진 모든 파드는 동일한 ID를 공유하게 됩니다. 이 ID는 파드의 IP 주소와 매핑되어 eBPF Map에 저장되며, 파드의 IP가 바뀌더라도 레이블이 동일하다면 같은 ID를 유지합니다.

이 신원 기반 접근 방식은 다음과 같은 강력한 이점을 제공합니다.

  • IP에 대한 비의존성: 파드의 생명주기와 관계없이 일관된 정책 적용이 가능합니다.
  • 뛰어난 확장성: 수만 개의 IP 규칙을 iptables 체인으로 관리하는 대신, 수천 개의 신원 ID만으로 정책을 효율적으로 관리할 수 있습니다.
  • 풍부한 표현력: L3(IP), L4(Port)뿐만 아니라, L7(HTTP API 경로/메서드, Kafka 토픽 등)과 DNS 이름까지 조합하여 매우 정교하고 구체적인 정책을 만들 수 있습니다.

CiliumNetworkPolicy의 다층적 방어

CiliumNetworkPolicy를 사용하면 다층적인 보안 정책을 구현할 수 있습니다.

  • L3/L4 정책: 특정 신원(예: role=frontend)을 가진 파드가 다른 신원(role=backend)의 8080 포트로만 통신하도록 제한할 수 있습니다.
  • L7 정책: 한발 더 나아가, role=frontend 파드가 role=backend/api/v1/data 엔드포인트에 GET 요청만 보내도록 제한할 수 있습니다. 만약 허용되지 않은 POST 요청이 발생하면, Cilium의 내장 Envoy 프록시는 eBPF 레벨에서 해당 요청을 즉시 차단하고, Hubble을 통해 이벤트를 실시간으로 관측할 수 있습니다.
  • DNS 기반 정책: toFQDNs 필드를 사용하면 파드가 특정 외부 도메인(예: api.github.com)으로만 나가는(egress) 통신을 하도록 제한할 수 있습니다. Cilium은 DNS 응답을 감지하여 해당 도메인에 매핑되는 IP 목록을 동적으로 eBPF Map에 업데이트함으로써 이 기능을 구현합니다. 이는 외부 서비스와의 통신을 최소한으로 제한하는 데 매우 효과적입니다.

3. Tetragon, 커널 레벨의 런타임 보안을 열다

네트워크 정책이 아무리 강력하더라도, 컨테이너 내부에서 발생하는 악의적인 행위까지 막을 수는 없습니다. 예를 들어, 침해된 파드 내에서 악성코드가 실행되거나, 민감한 파일에 접근하거나, 외부로 몰래 데이터를 유출하려는 시도 등은 네트워크 정책만으로는 감지하기 어렵습니다.

Tetragon은 바로 이 런타임 보안(Runtime Security) 문제를 해결하기 위해 등장한 Cilium의 자매 프로젝트입니다. Tetragon은 eBPF를 사용하여 커널과 애플리케이션 사이에서 발생하는 모든 시스템 콜(System Call)과 다른 커널 이벤트를 실시간으로 관찰하고, 미리 정의된 정책에 따라 의심스러운 행위를 감지하거나 차단할 수 있습니다.

Tetragon의 작동 원리

Tetragon은 데몬셋(DaemonSet)으로 각 노드에 배포되며, eBPF 프로그램을 커널의 다양한 훅 포인트(kprobes, tracepoints 등)에 부착합니다. 이를 통해 기존의 보안 에이전트들이 유발했던 성능 저하 없이, 커널 수준에서 직접 다음과 같은 다양한 활동을 감지할 수 있습니다.

  • 프로세스 실행 감지: 파드 내에서 어떤 프로세스(execve 시스템 콜)가 실행되었는지, 어떤 인자(argument)를 사용했는지 추적합니다. "내 nginx 파드에서 bash 셸이 실행되었다"와 같은 의심스러운 활동을 즉시 포착할 수 있습니다.
  • 파일 접근 모니터링: 민감한 파일(예: /etc/shadow, /var/run/secrets/...)에 대한 읽기/쓰기 시도를 감지합니다.
  • 네트워크 소켓 활동: 어떤 프로세스가 어떤 IP와 포트로 네트워크 연결을 시도하는지 감지합니다. 이는 네트워크 정책에서 놓칠 수 있는 내부 프로세스의 악의적인 통신 시도를 잡아낼 수 있습니다.
  • 권한 상승 시도: CAP_SYS_ADMIN과 같은 위험한 리눅스 캐퍼빌리티(Capability) 사용을 감지하여 컨테이너 탈출 시도를 막습니다.

TracingPolicy를 통한 실시간 탐지

Tetragon의 모든 정책은 TracingPolicy라는 CRD를 통해 정의됩니다. 예를 들어, privileged-pod-exec 정책은 권한이 있는(privileged) 파드 내부에서 셸이 실행되는 것을 감지하도록 설정할 수 있습니다.

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "detect-shell-in-privileged-pod"
spec:
  kprobes:
  - call: "security_bprm_check"
    # ... (생략) ...
    selectors:
    - matchPIDs:
      - operator: "In"
        followForks: true
        isNamespacePID: true
        values:
        - 1
      matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "/bin/bash"
        # ... (생략) ...

4. Cilium과 Tetragon이 완성하는 통합 보안

Cilium Study의 마지막 여정을 통해, 우리는 eBPF가 단순한 네트워킹 기술을 넘어 클라우드 네이티브 보안의 지형을 어떻게 바꾸고 있는지를 목격했습니다.

  • Cilium네트워크 계층(L3-L7) 에서 신원 기반의 제로 트러스트 보안을 구현하여 서비스 간의 통신을 안전하게 보호합니다.
  • Tetragon커널과 런타임 계층에서 발생하는 모든 행위를 감시하여 네트워크 정책을 우회하는 내부 위협까지 차단합니다.

이 두 가지 강력한 도구가 결합될 때, 우리는 비로소 애플리케이션의 전체 생명주기에 걸쳐 일관되고 심층적인 보안 가시성과 제어 능력을 확보할 수 있습니다. 복잡하고 동적인 쿠버네티스 환경에서, Cilium과 Tetragon은 가장 신뢰할 수 있는 보안 파트너가 되어줄 것입니다.

지금까지 우리는 Cilium의 다양한 네트워킹 및 보안 기능을 학습하며 강력한 쿠버네티스 클러스터를 구축하는 방법을 익혔습니다. 하지만 클러스터의 규모가 수백, 수천 개의 노드로 확장되고 수만 개의 파드가 동작하는 대규모 환경이 되면, 우리는 새로운 도전에 직면하게 됩니다. 바로 '성능' 입니다. 응답이 느려지는 API 서버, 원인 모를 파드 생성 실패, 간헐적인 네트워크 드롭 등은 클러스터의 안정성을 심각하게 위협할 수 있습니다.

이번 7주차 스터디에서는 대규모 클러스터에서 발생할 수 있는 다양한 성능 병목 지점을 진단하고, 이를 해결하기 위한 구체적인 튜닝 전략을 심층적으로 다루고자 합니다. Kubernetes 컨트롤 플레인의 심장인 kube-apiserveretcd의 동작 원리부터 Cilium 데이터 플레인의 핵심인 eBPF Map 관리까지, 성능 최적화의 여정을 함께 떠나보겠습니다.

이미지 출처: https://cilium.io/use-cases/host-firewall/

1. Kubernetes 성능 병목 현상 탐구: API 서버는 왜 느려지는가?

클러스터의 모든 상태 변경은 kube-apiserver를 통해 etcd에 기록됩니다. 따라서 클러스터의 규모가 커질수록 컨트롤 플레인의 부하는 기하급수적으로 증가하며, 이곳에서 가장 먼저 성능 문제가 발생합니다. 이러한 문제를 재현하고 분석하기 위해 kube-burner와 같은 부하 테스트 도구를 활용할 수 있습니다.

부하 테스트를 통해 발견된 병목 지점

kube-burner를 사용하여 수백 개의 파드를 동시에 생성하는 시나리오를 시뮬레이션하면 다음과 같은 대표적인 문제들을 마주하게 됩니다.

  1. Too many pods (노드 당 파드 개수 제한): Kubelet은 maxPods 설정값에 따라 노드 당 생성 가능한 파드의 최대 개수를 제한합니다. 이 한계를 초과하면 스케줄러는 더 이상 해당 노드에 파드를 할당하지 못하고 FailedScheduling 이벤트를 발생시킵니다.
  2. No IP addresses available (PodCIDR 고갈): 각 노드는 PodCIDR이라는 고유한 IP 대역을 할당받아 자신이 관리하는 파드들에게 IP를 순차적으로 부여합니다. 만약 maxPods를 늘리더라도 할당된 PodCIDR 대역 내의 IP를 모두 소진하면, CNI 플러그인은 FailedCreatePodSandBox 에러를 내며 파드 생성을 실패시킵니다.

이러한 현상들은 표면적인 문제일 뿐, 그 근본 원인은 대량의 리소스 생성 요청이 컨트롤 플레인에 집중될 때 발생하는 부하와 관련이 깊습니다.

근본 원인: 대규모 LIST 요청과 메모리 폭증

쿠버네티스 컨트롤러(kubelet, CNI 에이전트 등)들은 클러스터의 최신 상태를 유지하기 위해 API 서버에 주기적으로 리소스 목록을 요청(LIST API Call)합니다. 문제는 클러스터에 수만 개의 파드가 존재할 때 발생합니다.

  • 동작 방식: 클라이언트가 kubectl get pods --all-namespaces와 같은 명령을 실행하면, kube-apiserveretcd로부터 모든 파드 정보를 가져와야 합니다. 이때 etcd는 트랜잭션의 일관성을 보장하기 위해 특정 시점의 데이터를 메모리에 복제한 후 응답합니다. kube-apiserver는 이 데이터를 받아 다시 Go 구조체로 변환(deserialization)하고, 최종적으로 클라이언트가 요청한 형식(JSON, YAML 등)으로 변환(serialization)하여 전달합니다.
  • 결과: 이 과정에서 etcdkube-apiserver 양쪽 모두에서 응답 데이터 크기의 몇 배에 달하는 막대한 메모리가 일시적으로 할당됩니다. 만약 이러한 대규모 LIST 요청이 여러 클라이언트로부터 동시에 발생하면, 컨트롤 플레인 컴포넌트는 메모리 사용량이 급증하여 OOM(Out of Memory)으로 종료될 수 있습니다. 이는 클러스터 전체의 가용성을 위협하는 심각한 장애로 이어집니다.

2. Kubernetes 성능 튜닝 전략

컨트롤 플레인의 안정성을 확보하기 위해서는 API 요청을 효율적으로 관리하고 인프라 전반의 설정을 최적화하는 다각적인 접근이 필요합니다.

API 요청 관리 기법

  1. 페이지네이션 (Limit & Continue): 대규모 리소스를 조회하는 클라이언트는 반드시 limitcontinue 파라미터를 사용하여 응답을 여러 페이지로 나누어 요청해야 합니다. 이는 한 번의 요청으로 인한 메모리 부담을 줄이는 가장 기본적인 방법입니다. kubectl을 포함한 대부분의 공식 클라이언트는 이 방식을 기본적으로 사용합니다.
  2. API 서버 캐시 활용 (ResourceVersion="0"): LIST 요청 시 resourceVersion="0" 파라미터를 사용하면, kube-apiserveretcd에 직접 요청하는 대신 자신의 내부 캐시에서 데이터를 반환합니다. 이는 etcd의 부하를 극적으로 줄여주지만, 약간의 지연이 있는 데이터(eventual consistency)를 받을 수 있다는 점을 감안해야 합니다. 하지만 대부분의 컨트롤러 동기화 작업에는 이 방식으로 충분합니다.
  3. API 우선순위 및 공정성 (APF - API Priority and Fairness): Kubernetes 1.20부터 도입된 APF는 단순한 요청 속도 제한을 넘어, 요청의 중요도에 따라 우선순위를 부여하고 큐를 분리하여 관리합니다.
    • FlowSchema: 들어오는 요청을 '누가(Subject)', '무엇을(Resource)' 요청하는지에 따라 분류합니다.
    • PriorityLevelConfiguration: 각 요청 흐름에 'system-critical', 'workload-high' 등과 같은 우선순위 레벨과 동시 처리 한도를 할당합니다. 이를 통해 중요도가 낮은 컨트롤러의 과도한 요청이 노드의 keepalive와 같은 핵심 시스템 동작을 방해하는 것을 방지하고, 클러스터의 안정성을 보장합니다.

인프라 및 커널 튜닝

  • 컨트롤 플레인 고가용성(HA): 운영 환경에서는 최소 3대 이상의 컨트롤 플레인 노드를 구성하고, etcd 클러스터는 API 서버와 분리된 전용 노드에 배치하는 것이 권장됩니다. 특히, 생성과 삭제가 빈번한 Event 리소스는 별도의 etcd 클러스터에 저장하여 메인 etcd의 부하를 줄일 수 있습니다.
  • ARP 캐시 튜닝: 대규모 L2 네트워크 환경에서는 노드 간 ARP 요청이 빈번해져 커널의 ARP 캐시가 가득 차는 neighbor table overflow 문제가 발생할 수 있습니다. sysctl을 통해 net.ipv4.neigh.default.gc_thresh 관련 파라미터들을 상향 조정하여 캐시 크기를 늘려야 합니다.
  • Kubelet 동시성 제어: serializeImagePulls 플래그를 false로 설정하면 노드에서 여러 컨테이너 이미지를 병렬로 다운로드하여 파드 생성 속도를 높일 수 있습니다. 또한, Kubelet이 API 서버에 가하는 부하를 제어하기 위해 kubeAPIQPSkubeAPIBurst 값을 적절히 조정해야 합니다(1.27부터 기본값이 상향됨).

핵심 성능 지표 모니터링

Prometheus와 Grafana를 활용하여 다음과 같은 핵심 지표를 지속적으로 모니터링해야 병목 지점을 조기에 발견할 수 있습니다.

  • apiserver_request_total: API 서버의 QPS와 에러율
  • apiserver_request_duration_seconds_bucket: API 요청의 99%ile 지연 시간
  • etcd_request_duration_seconds_bucket: etcd 요청 지연 시간
  • workqueue_depth: 컨트롤러의 작업 큐 깊이 (큐가 계속 쌓이면 병목)
  • workqueue_queue_duration_seconds_bucket: 작업이 큐에서 대기한 시간

3. Cilium 성능 분석 및 최적화

Cilium은 eBPF를 통해 고성능 데이터 플레인을 제공하지만, 대규모 환경에서는 Cilium 자체의 내부 상태 관리 메커니즘이 성능에 영향을 미칠 수 있습니다.

BPF Map Pressure와 StateDB

  • BPF Map: Cilium은 서비스, 엔드포인트, 정책 등의 정보를 커널 공간의 BPF Map이라는 특수한 자료구조에 저장하여 빠르게 조회합니다. 하지만 이 Map들은 생성 시 최대 크기가 정해져 있어, 서비스나 엔드포인트의 수가 이 한계를 초과하면 더 이상 새로운 정보를 저장할 수 없습니다.
  • 장애 현상: BPF Map이 가득 차면(BPF map pressure 메트릭 증가), Cilium 에이전트는 새로운 서비스 정보를 Map에 업데이트하지 못하고 Service backend not found와 같은 사유로 패킷을 드롭(drop)하기 시작합니다.
  • Cilium의 회복성 설계: Cilium은 이러한 일시적인 실패에 대응하기 위해 StateDB라는 인메모리 데이터베이스와 Reconciler 패턴을 사용합니다. API 서버로부터 받은 모든 desired state는 먼저 StateDB에 저장됩니다. Reconciler는 주기적으로 StateDB의 상태와 실제 BPF Map의 상태를 비교하여, 실패했거나 누락된 항목을 재시도하여 최종적 일관성을 보장합니다.

Cilium 튜닝 파라미터

  • bpf.mapDynamicSizeRatio: 노드의 전체 메모리 대비 BPF Map에 할당할 메모리의 비율을 조정합니다. 서비스나 엔드포인트가 매우 많은 환경에서는 이 비율을 기본값(0.0025)보다 높여 BPF Map의 크기를 늘려야 합니다.
  • k8s.client-qps / k8s.client-burst: Cilium 에이전트가 kube-apiserver에 요청을 보내는 속도를 제어합니다. 대규모 클러스터에서는 이 값을 적절히 상향 조정하여 Cilium이 클러스터 변경 사항을 더 빠르게 동기화하도록 할 수 있습니다.

결론

대규모 쿠버네티스 클러스터의 성능 튜닝은 단일 파라미터를 조정하는 것을 넘어, 시스템 전반의 동작 원리를 깊이 이해하는 것에서 시작됩니다. 특히 컨트롤 플레인의 심장인 API 서버와 etcd의 상호작용, 그리고 각 컨트롤러의 동기화 메커니즘을 파악하는 것이 중요합니다. kube-burner와 같은 도구로 한계 상황을 시뮬레이션하고, Prometheus 메트릭을 통해 병목 지점을 정량적으로 분석하는 체계적인 접근이 필요합니다.

Cilium 환경에서는 여기에 더해, eBPF 데이터 플레인의 핵심인 BPF Map의 동작 방식과 StateDB를 통한 회복성 메커니즘을 이해하고, 관련 파라미터를 클러스터의 특성에 맞게 최적화해야 합니다. 성능 문제는 복잡하고 다층적이지만, 그 원리를 파고들면 반드시 명확한 해결책을 찾을 수 있습니다.

+ Recent posts