이번 포스팅은 운영 중인 웹 서비스에서 처음 겪은 보안 사고의 기록입니다. 공격은 분산 스크래핑 봇넷이었습니다.

 

 사람과 멀티 AI 에이전트가 함께 공격을 감지하고, 진행 중인 공격의 패턴을 실시간으로 분석하고, 서비스가 받는 영향을 추적했습니다. 그리고 분석으로 확정한 방어 방법 하나를 단 한 번 적용해 공격을 무력화했습니다.

 

 마지막 장에서는 이 경험을 바탕으로 AI 에이전트를 통한 시스템 운영의 방향을 이야기해 보려 합니다.


1. 사건 요약

항목 출처
기간 2026.08.19 20:00 - 2026.08.20 10:00 자체 옵저버빌리티 스택
Cloudflare 수신 약 197만건(평시 대비 50배), 방문자 3.6만 Cloudflare 대시보드 및 자체 옵저버빌리티 스택
오리진 도달 14시간동안 약 77만건(직전 평시 24시간 대비 약 13배) 자체 옵저버빌리티 스택
전송량 오리진 TX 21.6GB / RX 14.1GB 자체 옵저버빌리티 스택
피크 요청 131 req/s (평시의 약 260배) 자체 옵저버빌리티 스택
서비스 영향 p95 열화 약 18분, 서비스 마비 약 23분(조치 직전 수치) 자체 옵저버빌리티 스택
조치 엣지 클라우드 WAF 설정 - 적용 6분 내 공격 97% 방어, 품질 3분내 완전 회복 Cloudflare 대시보드 및 자체 옵저버빌리티 스택
피해 데이터 손실 0, 실사용자 오검문 0, 방어 비용 0, Umami 지표 오염 자체 옵저버빌리티 스택

 

2. 사건 전개

2.1 배경

 바이브 코딩을 통해 만든 작은 웹 서비스를 하나 운영하고 있습니다. CNCF에 등재된 Graduated/Incubating 등급의 프로젝트의 새로운 릴리스 노트를 수집하여 분석해 주는 서비스입니다. 대중적인 주제는 아니어서 개설한 지 반년이 넘어가고 있는 시점이지만 사용자가 그렇게 많지는 않습니다.

 단, 최근에 Amazon Bot이나 GPT나 Google Gemini 같은 AI 크롤러들이 제 사이트를 발견하고 정보를 얻어가고 있어서 사람 사용자보다는 봇 사용자들이 조금씩 늘고있는 추세였습니다. 이에 조금 더 잘 발견되게 하기 위해 DNS for AI Discovery(DNS-AID) 설정을 하였습니다. 그리고 얼마 지나지 않아 Umami 대시보드에 방문자가 급증하는 것을 포착하게 됩니다.

2.2 인지

 그런데 이게 곧 정상적이지 않은 요청이라는 것을 알게 되었습니다. 미국, 브라질, 프랑스, 아르헨티나, 싱가폴, 파키스탄, 인도 등등... 세계 각지의 수십 개국으로부터 동일한 경로로 요청이 들어오고 있었습니다. 처음에는 앞서 설정한 DNS-AID가 작동하면서 나타난 현상이 아닌가 했는데 모든 요청들의 Visit duration이 1초인 것이 봇이라 하기에도 너무 짧았습니다.

fig.1 - 세계 각국의 봇넷 공격

2.3 관찰 및 분석

  공격이 시작된 시간은 2026년 8월 19일 20시부터 약 4시간 정도는 동시 접속 사용자가 약 20명 내외로 집계 되었으며 그 요청량도 그렇게 크지 않았습니다. 공격을 인지한 시점에 AI 에이전트를 통해 평소보다 많은 사용자 수에 대한 분석을 요청했습니다.

현재 Ratatosk에 비정상적으로 보이는 요청이 증가하고 있다.
모두 /release 페이지를 겨냥하고 있고 Visit duration도 모두 0초로 일반적인 패턴이 아니다.

공격으로 의심되는데 접속 로그를 분석해서 보고해 달라.

 AI 에이전트는 이에 지금 이 요청이 비정상적인 것은 맞다고 판단해 주었습니다. 그 근거로 동일 경로에 대한 동시 다발적인 접근, 그리고 비정상 적인 사이트 상주 시간을 근거로 들었습니다. 다만, 요청이 동시에 들어왔다가 다소 소강상태를 보이고 또 들어오는 파도와 같은 형식이고, 3~6 req/s 정도의 요청은 시스템에 부하를 일으키지 않는 수준이고 대부분은 웹 요청 처리 P95에도 이상이 없어 지켜보자는 입장이었습니다.

fig.2 - 점차 증가하는 공격 강도

 저 역시도 이 판단에 동의해습니다. 기본적인 서버의 리소스 지표인 CPU나 메모리 사용률도 평소와 같았고 웹 서비스 품질도 문제가 없다 판단했기 때문입니다. 그렇게 약 5시간 정도를 지켜보다가 자정을 넘겨서 잠이 들었습니다. 물론 잠을 자지 않는 AI 에이전트에게 감시를 맡겨둔 상태로 말입니다.

 

 하지만, 아침에 일어나보니 밤새 이 미적지근한 공격이 계속 이어지고 있었습니다. 공교롭게도 잠을 청하고 얼마 지나지 않아 트래픽양이 두배로 늘어났고 또 얼마 지나지 않아 두배로 늘어나 있는 상태였습니다. 감시를 하고 있었던 AI 에이전트에게 보고를 요청했고 그 답변은 잠들기 전과는 심각도가 달라져 있었습니다. 밤 사이 계속된 공격으로 리소스 사용량은 확실히 올라가 있었으며 웹 요청 처리 P95도 영향을 받고 있었습니다. 10~21 req/s의 요청이 지속되면서 미리 설정해 둔 웹 서버의 초당 요청량 임계값도 넘어서고 있어 메신저를 통해 알람도 오고 있었습니다. 공격이 멈출 기세가 없었기 때문에 빠르게 AI 에이전트를 통해 분석을 요청하였습니다. AI 에이전트는 수많은 로그를 신속하게 검색하여 패턴을 찾아내고 아래와 같이 보고했습니다.

특징 측정
소스 주거용 프록시망 - 수십 개국에서 고유 /24 대역 26,859개, IP 26,859개, IP당 1 ~ 4 요청 후 교체, Rate Limit 발화 우회
클리어언트 헤드리스 Chromium 팜 - JS 실행으로 Umami 방문자로 집게, Next.js RSC 요청 형식 정확
위장 패턴 UA=Chrome/118, Firefox 120/121(2023년식) vs Sec-Ch-Ua=Chromium 146(최신) - 사람이 사용하는 브라우저에서는 있을 수 없는 불일치
세션 구조 문서 로드 1회 RSC 탐색 평균 27건 내외 폐기, 트래픽 86%가 RSC fetch
표적 특정 경로 필터 조합 전수 열거. 상세 페이지 및 AI 에이전트용 앤드포인트에는 미접촉

 분석이 있고 얼마 후 프록시 메모리가 임계치를 초과했다는 알람이 디스코드로 왔고 급하게 메모리 한도를 올려서 프록시가 OOM Killed 되는 것을 막았습니다. 바로 옵저버빌리티를 담당하는 에이전트에게 분석 내용을 파일로 전달하였고, 이를 기반으로 본격적인 메트릭과 로그 분석을 시작하였습니다. 옵저버빌리티 쪽 에이전트 역시 동일한 결론에 도달하였으며 Cloudflare의 보안 규칙을 통해 이를 해결할 수 있다고 하며 방법을 제시하였습니다.

2.4 대응과 조치

 그러는 와중에도 상황은 더욱 악화일로를 걸었습니다. 사건 경과 약 13시간 만인 2026년 8월 20일 오전 9시경 요청이 38 req/s에서 57 req/s로 점프합니다. 바로 이때 P95가 92ms에서 970ms로 올라가면서 순간 영향을 받았다가 일시 소강상태를 겪습니다. 이후 재급증하면서 P95가 2.2초로 급격히 나빠졌고 9시 30분이 조금 못된 시점에 서비스 품질이 최악으로 치닫기 시작합니다.

fig.3 - 거세진 공격으로 시스템 마비 상황

 웹 요청이 100 req/s를 넘어가면서 P95가 8.9초까지 올라갔으며 P99는 116초, 5xx 응답은 10분간 약 3600건이 발생하게 됩니다. 웹의 메트릭 앤드포인트가 무응답으로 프로브가 실패하여 옵저버빌리티 측에서 타겟 다운 크리티컬 알람이 울리기 시작합니다. 웹서버의 CPU 코어를 응급 증설 하였으나 부분적으로 효과가 있었을 뿐 즉시 재포화가 되어 효과가 없었습니다. 요청수는 140req/s를 넘어가고 있었으며 Umami의 방문자 통계는 이전 시간대와는 비교가 안될 정도로 무섭게 치솟고 있었습니다. 아마 이때 Umami에 집계된 동시 접속자 수가 300명이 넘었던 것으로 기억합니다.

 

 이는 도저히 하나의 인스턴스로 버틸 수 없다고 판단, 앞서 옵저버빌리티 측 AI 에이전트가 추천한 Cloudflare의 보안 규칙을 사용하기로 합니다. Managed Challenge를 공격 대상이 되는 경로에 설정하였습니다. 단, 앞서 이야기한 정상적인 AI 크롤링 봇(GPT, Amazon 봇 등)은 예외로 허용하였습니다. 규칙을 적용하자마자 약 4.3만 개의 요청이 이 규칙에 의해 무력하 되었으며 3분 만에 서비스 품질 완전 회복, 6분 내 공격 전면 차단 및 안정 상태에 돌입할 수 있었습니다. 이로서 약 14시간 동안 이어진 공격을 완전히 차단할 수 있게 되었습니다.

3. 사건 타임라인 및 분석과 교훈

3.1 사건 타임라인

시각(KST) 사건
20:00 ~ 20:45 공격 시작 - 한번 몰아치고 소강 상태를 보이는 요청(순간 최대 18 req/s) 으로 정찰 및 캘리브레이션 추정
20:45 ~ 04: 55 파도 단계(약 8시간): 3~6 req/s로 몰려왔다 빠지기 반복, 시간당 400 ~ 800회 방문
04:55 ~ 09:05 지속 단계(약 4시간): 10~21 req/s로 쉼 없이 요청 발생 → 08:15 부터는 24~45 req/s로 계단 증가, 웹 서비스 품질은 정상 구간, RequestRate 경고 알람 발생
(07:36 ~ 07:44) 프록시 컨테이너 메모리 임계치 초과 발생메모리 증설 응급 처방으로 프록시 컨테이너 OOM Killed 방지
09:06 ~ 09:17 웹 서비스 품질 저하 시작 - 38 → 57 req/s로 점프하면서 바로 그때 P95 92ms → 970ms로 급증
09:17 ~ 09:19 일시 소강 - P95 즉시 97ms로 회복, 큐잉 특성
09:19 ~ 09:21 재급증으로 인한 P95 응답 2.2초
09:21 ~ 09:23 서비스 붕괴 - 108 req/s, P95 8.9초, P99 116초, 5xx 10분간 3,667건 발생
09:23 ~ 09:27 웹 메트릭 앤드포인트 무응답, 옵저버빌리티 측에서 타겟 다운 경보 발생
09:27 ~ 09:47 웹 컨테이너 CPU 증설하였으나 부분 효과, 즉시 재포화 Cloudflare Managed Challenge 적용
09:47 ~ 09:53 요청 수 131 → 4 req/s로 97% 감소, P95 50ms로 안정화, 3분이내 품질 완전 회복, 6분 내 안정권 진입

3.2 분석과 교훈

 이번 공격을 통해 의외의 수확을 하나 할 수 있었는데, 벤치마크 테스트가 되었다는 겁니다. 웹 서비스가 올라간 인프라가 어디까지 버틸 수 있는지 이번에 확실하게 알게 되었습니다. 한계선은 요청 50 req/s 이하이며 그 이상이 되면 P95 품질 저하, 110 req/s 부근이면 서비스 붕괴 수준이라는 점입니다.

 또한 각종 지표들의 거동도 확인할 수 있었습니다. cpu 사용량이나 메모리 사용량은 요청에 따라 비교적 선형적으로 증가하지만 P95는 특정 임계를 넘는 순간 급격히 나빠지는 것도 확인할 수 있었습니다. 이는 큐잉의 특성으로 대기열이 처리열을 넘어가는 순간 폭주하는 큐잉으로 인해 급격히 서비스 품질의 저하가 발생하게 됩니다. 따라서, P95가 여유가 있다 하더라도 이것을 '지금은 괜찮다'라고 판단하면 안 된다는 교훈을 얻을 수 있었습니다.

 그리고 수직 확장은 이런 류의 공격에는 아무런 효과가 없다는 것 역시도 웹 서버의 CPU 증설을 통해 확인할 수 있었습니다. 이번의 경우에는 공격이라 차단을 통해 막을 수 있었으나 향후 대규모 서비스를 운영할 때에는 수직 확장이 아닌 탄력적인 수평 확장과 부하분산이 중요할 것이라는 너무나도 보편적인 진리(?)를 몸으로 느낄 수 있었습니다.


4. AI 에이전트의 활약

4.1 특화된 에이전트

 이번 공격을 알아차리고 즉시 AI 에이전트와 대처할 수 있었던 이유는 바로 각 기능마다 특하된 에이전트가 있어서 가능했습니다. 거창하게 AI 에이전트를 개발한 것은 아닙니다. 다만 웹 서비스와 옵저버빌리티를 구현하면서 느낀 점은, 개발과 운영 과정에서의 여러 결정 사항들을 컨텍스트 한계가 있는 AI 에이전트가 언제든지 그 맥락을 이해하고 궤도에 오를 수 있도록 하는 것이 중요하다 것이었습니다. 그래서 프로젝트 초기에 문서를 쓰는 규칙을 설정하고 그것이 자산이 될 수 있게 계속 쌓아왔습니다.

 물론 문서를 쌓기만 해서는 안됩니다. 모델의 성능이 떨어졌다는 느낌을 받을 때면 대부분 문서에 쌓인 결정이나 지시사항이 서로 모순을 일으키거나 충돌하는 경우가 대다수라 이런 부분을 한 번씩 정리하는 시간이 필요했습니다. 이를 통해 문서는 고도화하고, 메모리는 중요한 것들만 남김으로써 AI 에이전트의 성능을 원하는 상태로 유지하면서 각 서비스에 특화시킬 수 있었습니다.

4.2 에이전트 간의 협업

 사람만 협업하는 것은 아닙니다. AI 에이전트 간에도 협업이 가능합니다. 저는 어느 순간엔가 두 서비스를 담당하는 에이전트끼리 서로를 인지하고 대화가 필요하다는 것을 느끼기 시작했습니다. 특히 옵저버빌리티 스택을 구성할 때가 그랬습니다. 웹에서 나오는 기본적인 자원 메트릭은 매우 표면적인 상태만을 알 수 있습니다. 각 컨테이너에서 나오는 그들만의 메트릭과 애플리케이션 자체에서도 메트릭을 생산할 수 있다면 이를 통해 좀 더 세밀한 관측이 가능할 것이라 생각했습니다.

 하지만, 두 에이전트는 서로를 모르고 개별적으로 작업을 진행하고 있었기 때문에 이를 연결할 필요가 있었습니다. 그래서 처음에는 사람이 두 에이전트 간의 매개자 역할을 하였습니다. 프롬프트를 입력하고 나온 출력을 다른 에이전트에게 전달해주고, 그 응답을 다시 되돌려 주는 방식으로 작업을 진행했는데 한계가 있었습니다. 그래서 생각한 것이 문서를 서로 교환하게 하면 어떨까 였습니다. 문서는 그 내용이 남으니 사람도 그것을 추적할 수 있고, 에이전트도 문서가 많아져도 검색을 통해 쉽게 맥락을 다시 끌어올 수 있을 것이라 생각했습니다.

 이 방식은 생각보다 효과가 좋았습니다. 그래서 두 에이전트가 서로를 인지하고 있던 상황에서 이번과 같은 사건이 생겼고, 서로의 특성을 알던 에이전트들이 매우 면밀하게 협업하여 문제 상황을 진단하고 합의된 해결책을 도출해 낼 수 있었습니다.

4.3 빅테크들의 움직임

 이미 이런 요구를 알고 있던 빅테크들은 이에 대해 더욱 세련된 방식을 제시하고 있습니다. 2025년에 발표된 Google의 A2A 프로토콜이 그것이며, 앤트로픽에서 최근에 발표한 Cross-session messaging도 하나의 방식입니다. 하지만 이것은 제가 사용하고 있는 파일 기반의 에이전트간 통신과는 조금 다릅니다.
 우선 A2A의 경우 운영자 경계를 넘는 에이전트간 통신을 그 범위로 하고 있습니다. A2A는 서로 모르는 에이전트끼리의 발견과 인증, 위임을 전제로 하고 있으며 제가 운영하는 범위에서는 이런 요소가 없습니다. Cross-session messaging은 같은 호스트 내 Unix 소켓을 통한 세션 간의 통신으로 메시지를 기반으로 하여 휘발성을 가지고 있습니다. 저의 경우에는 같은 호스트에서 돌아가는 서로 다른 세션은 아니라 이것을 사용할 수 없는 상태입니다.

 어쨌든, 큰 틀에서는 서로 다른 AI 에이전트들이 어떻게 서로를 알아차리고 협업해 나갈 수 있는지에 대해 고민하며 나름대로 이를 구현해가고 있다고 보입니다.


 이번 봇넷 공격 사건을 통해 운영적인 측면에서의 교훈도 얻을 수 있었지만, 그간 쌓아온 AI 에이전트 사용 방식이 문제 해결에 빛을 발하기도 하였습니다. 특히 마지막 부분에 저의 독특한 운영 방식과, 이를 발전시킬 수 있는 방향이 있는지도 한번 더 고민해 봐야겠습니다.

 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은 가장 신뢰할 수 있는 보안 파트너가 되어줄 것입니다.

+ Recent posts