<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Infra_Student</title>
        <link>https://velog.io/</link>
        <description>Don’t get mad at the computer.</description>
        <lastBuildDate>Wed, 15 Jul 2026 06:34:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Infra_Student</title>
            <url>https://velog.velcdn.com/images/pizza_loves_me/profile/30f05e36-24f3-44ba-a2c4-305c77314597/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Infra_Student. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/pizza_loves_me" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Project] On-Prem Multi-Node Observability System & Analyzer AI Agent - 3️⃣ 분석용 AI Agent 구축]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-On-Prem-Multi-Node-Observability-System-Analyzer-AI-Agent-3-%EB%B6%84%EC%84%9D%EC%9A%A9-AI-Agent-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@pizza_loves_me/Project-On-Prem-Multi-Node-Observability-System-Analyzer-AI-Agent-3-%EB%B6%84%EC%84%9D%EC%9A%A9-AI-Agent-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Wed, 15 Jul 2026 06:34:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>이번에는 분석용 AI Agent를 구축한 내용을 정리해보겠습니다.</p>
<h1 id="1-pipeline--architecture">1. Pipeline &amp; Architecture</h1>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/f15a09ea-85e1-448f-ac0f-c2b4163755a3/image.png" alt=""></p>
<blockquote>
</blockquote>
<p><strong>📌 주요 포인트</strong></p>
<ul>
<li>기존 파이프라인 재활용 (GitOps, Alert)</li>
<li>SSOT(Single Source Of Truth): GitHub</li>
<li>AWS ECR 사용</li>
</ul>
<p>우선 에이전트 개발을 위한 추가 파이프라인을 구축하지 않고자 했습니다.(관리 포인트를 줄이고자)
앞서 구축한 GitOps 파이프라인과 Alert 파이프라인을 재활용 하는 방법을 선택했습니다.
또 이번에도 GitHub 소스 코드를 단일 진실 공급원으로 두어 관리 포인트를 줄였습니다.
마지막으로 에이전트 개발 코드를 이미지로 관리하고자 AWS의 Elastic Container Registry(ECR)을 도입했습니다.</p>
<h2 id="⚠️-argocd-core-issue">⚠️ ArgoCD core issue</h2>
<img src="https://velog.velcdn.com/images/pizza_loves_me/post/ee2a3732-fbd8-4b3f-aa08-b91e4ba2f319/image.png" width="65%" height="65%">

<p>사진에서 볼 수 있듯이 ArgoCD core 가 ECR을 바라보지 못하는(변경사항 등을 감지하지 못하는) 문제가 있었습니다.</p>
<blockquote>
<p>이는 ArgoCD core가 Git만 보도록 의도적으로 설계 되어 있기 때문이었습니다.</p>
</blockquote>
<p>📌 참고 문서 (설계 의도)
<a href="https://github.com/argoproj/argo-cd/blob/master/docs/proposals/headless-argocd.md#provide-an-easy-way-to-deploy-argo-cd-without-apiui">https://github.com/argoproj/argo-cd/blob/master/docs/proposals/headless-argocd.md#provide-an-easy-way-to-deploy-argo-cd-without-apiui</a></p>
<p>📌 참고 문서 (매니페스트)
<a href="https://github.com/argoproj/argo-cd/blob/master/manifests/core-install.yaml">https://github.com/argoproj/argo-cd/blob/master/manifests/core-install.yaml</a>
(desired state를 넣는 통로가 Git에 연결된 Application/ApplicationSet CRD 뿐이므로 구조적으로 Git만 바라보는 형태)</p>
<h3 id="해결">해결</h3>
<blockquote>
<p>ArgoCD의 <em><strong>Image Updater</strong></em> 라는 컴포넌트를 추가 구축을 하면 이 문제를 해결할 수 있었습니다.</p>
</blockquote>
<blockquote>
<p>하지만 부트캠프 당시 프로젝트 규모 및 추가 구축에 드는 리소스를 고려해보았습니다. 
✅ 해당 컴포넌트를 추가 구축하지 않고 CI 단계에서 ArgoCD가 변경사항을 감지할 수 있도록 values.yaml 파일 내 image.tag 한 줄만 자동으로 변경해주는 자동화 봇 PR을 구축했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/9454cbc8-f347-4e4a-b575-3dcb3b404294/image.png" width="75%" height="75%"> (프로젝트 당시 비교를 위한 표)</p>
<h3 id="봇-pr-자동화">봇 PR (자동화)</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/ada7d907-afe3-44f3-afac-9d8549e23c04/image.png" alt=""></p>
<p>에이전트 개발 코드를 병합하면, CI가 돌 때 자동으로 봇 PR이 생성됩니다. 그 내용은 사진과 같이 ArgoCD가 감지할 수 있는 <em><strong>values.yaml</strong></em> 파일의 <em><strong>image.tag</strong></em> 를 자동으로 변경해줍니다. 개발자(나)는 에이전트 코드 병합과 동시에 올라오는 이미지 태그 변경 PR을 병합하기만 하면 되는 플로우로 위 문제를 해결했습니다.</p>
<h2 id="ai-agent-workflow---single-shot">AI Agent Workflow - Single Shot</h2>
<blockquote>
<p>신속한 알람(Alert)과 에이전트 분석 내용은 워크플로우를 분리</p>
</blockquote>
<ul>
<li>그 이유로는 구체적인 분석 및 해결 가이드라인 제공 또한 중요하지만, 이상 신호 감지 시 관리자에게 알람을 신속하게 보내는 것이 훨씬 중요하기에 두 워크플로우(알람 | 에이전트 분석 내용)를 분리시켰습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/22d573d7-8746-4c01-aab0-a9fe38ebdd8e/image.png" width="50%" height="50%"> Workflow를 간단하게 도식화 해보았습니다.</p>
<ul>
<li><em><strong>Flow 1: Slack 알람</strong></em><ul>
<li>Alertmanager를 통해 알람 발생 시 곧바로 관리자용 Slack 채널에 알람을 보내게 됩니다.</li>
</ul>
</li>
<li><em><strong>Flow 2: AI Agent 분석</strong></em><ul>
<li>1차 컨텍스트 수집 (비동기, 1회)<ul>
<li><code>loki.fetch()</code> : +-15m Log 수집</li>
<li><code>vm.fetch()</code> : +- 15m Metric 수집</li>
<li><code>runbook.find()</code> : 매칭되는 runbook 탐색</li>
</ul>
</li>
<li>AWS Bedrock 호출<ul>
<li>1차 컨텍스트를 종합하여 Bedrock(<del>Sonnet 4.5</del> -&gt; Nova Pro) 1회 호출</li>
<li>(Sonnet 모델은 꽤 많이 비싸더라구요..)</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="실제-alert--analzer-ai-agent">실제 Alert + Analzer AI Agent</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/fce8f928-fc2a-4815-b24d-9b9968d14818/image.png" alt=""></p>
<p>위 과정을 통해 온프레미스 노드에서 이상 신호 감지 시 즉시 알람이 발생하고 (<strong>인프라 리소스 감지 봇 - Slack 댓글</strong>), 이어서 해당 댓글의 스레드에 분석용 AI Agent(<strong>AI 분석 봇 - Slack 댓글의 스레드</strong>)가 작성한 내용이 작성됩니다.</p>
<h2 id="ai-agent-workflow---agentic-loop">AI Agent Workflow - Agentic Loop</h2>
<blockquote>
<p>🤔(나): 근데 AI한테 &quot;한 번&quot;만 물어보는걸 &#39;에이전트&#39;라고 할 수 있을까? 또 분석 내용과 해결 가이드라인을 조금 더 자세하게 알려줄 수는 없을까?</p>
</blockquote>
<ul>
<li>AI Agent가 메인인 프로젝트는 아니었지만, 다소  단조롭다는 생각에 조금 더 고도화를 시켜보고자 했습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/8f977712-56ef-4aa7-bbd6-1d8b3fb9a658/image.png" alt=""></p>
<blockquote>
<p><strong>&#39;Agentic Loop&#39;</strong> 라는 용어가 있더군요?! 용어를 정의한 곳마다 약간의(?) 의미 차이가 있었지만, 저는 _<strong>&#39;Agent가 스스로 판단하여 반복적으로 작업하고 만족하는 결과 도출 시 반복 작업 종료&#39;</strong>_를 목표로 고도화를 진행해보았습니다.</p>
</blockquote>
<h3 id="고도화-요소">고도화 요소</h3>
<ul>
<li>&#39;1차 컨텍스트 수집 + 매칭되는 runbook 탐색&#39; : 기존과 동일</li>
<li>Agent가 사용할 수 있는 _<strong>도구(Tool Set)</strong>_를 선정 및 전달<ul>
<li><strong><code>PromQL</code></strong> : VictoriaMetrics의 시계열 메트릭 데이터를 조회/집계/연산하기 위한 쿼리 언어</li>
<li><strong><code>LogQL</code></strong> : Loki에서 로그를 라벨로 필터링하고 메트릭처럼 집계할 수 있게 해주는 로그 쿼리 언어</li>
<li><strong><code>list_recent_alerts</code></strong> : Alertmanager에서 최근 발생했거나 현재 활성 상태인 alert 목록을 조회하는 함수</li>
</ul>
</li>
</ul>
<p>Bedrock 첫 호출 시 위에서 수집한 <strong>1차 컨텍스트</strong>와 <strong>Tool Set</strong>을 함께 전달합니다. 
이때 Agent가 스스로 판단하기에 답변을 할 수 있다면 즉시 답변을 보내고, 답변 결과가 부족하다고 판단된다면 Tool을 호출하여 결과를 추가하며 반복적인 작업(loop)을 하게 됩니다.</p>
<p>이렇게 개선된 답변을 Slack 댓글의 스레드에 남기게 되는 구조입니다.</p>
<h3 id="agentic-loop에-대한-안전망">Agentic Loop에 대한 안전망</h3>
<blockquote>
<p>🤔(나): 에이전트가 스스로 판단하여 반복적인 작업을 하는 것은 좋은데,, 너무 많은 권한을 주는건 아닐까? (<del>감히 AI 주제에?</del>)</p>
</blockquote>
<p>이러한 고민을 하며 <em><strong>안정망</strong></em> 을 만들어야겠다(?) 라는 생각이 들었고, 나름의 안정 장치들을 마련해보았습니다.</p>
<h3 id="안전망">안전망</h3>
<ul>
<li><strong>무한 Loop 방지</strong> : MAX_ITERATIONS=5 를 설정하여 자체적인 Loop 횟수를 5회로 제한</li>
<li><strong>중복 호출 detect</strong> : 같은 tool. args 두번째 호출 즉시 차단</li>
<li><strong>query 폭주 방지</strong> : lookbakc &lt;= 6h, limit &lt;- 500 라인 강제</li>
<li><strong>Mutating 명령 봉쇄</strong> : 제공 tool은 전부 read-only, 에이전트의 리소스 수정 / 삭제 차단</li>
<li><strong>즉시 Fallback</strong> : Agent 에러 시 Single-Shot 경로 즉시 복귀</li>
</ul>
<h3 id="최종-workflow">최종 Workflow</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/b8733a42-6f82-48d1-a7d3-4fcd719e0dc2/image.png" alt=""></p>
<ol>
<li>Alertmanager를 통해 Alert 발생</li>
<li>관리자용 Slack에 에러 내용 알람: <em><strong>인프라 리소스 감지 봇(고오스)</strong></em></li>
<li>Agentic Loop 방식을 통해 에러 내용 분석 및 해결 가이드라인을 Slack 댓글의 스레드에 제공: <em><strong>AI 분석 봇(고우스트)</strong></em></li>
<li>플랫폼 관리자가 이를 확인하여 빠른 대응 가능: <em><strong>관리자(나: 팬텀)</strong></em></li>
</ol>
<hr>
<h1 id="회고">회고</h1>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/3bde8709-25dc-4612-b70b-48a0a71b76d6/image.png" alt=""></p>
<blockquote>
<h3 id="observability-회고">Observability 회고</h3>
<p>이렇게 부트캠프의 최종 프로젝트를 잘 마무리하게 되었습니다. 애플리케이션 레벨이 아닌 온프레미스 노드의 리소스를 관측은 이번에 처음 해보았는데 고려해야하는 부분들이 기존과 달라서 학습에 시간을 꽤 많이 쏟았던 것 같습니다.</p>
</blockquote>
<p>어떻게 보면 옵저버빌리티에서 <em><strong>가장 기본적이고, 기초적인</strong></em> 내용을 구축한 것 같은데 이번 기회에 제대로 구축하길 잘한 것 같습니다.</p>
<blockquote>
</blockquote>
<p>또 처음 써보는 다양한 오픈소스들(OpenTelemetry, VictoriaMetrics, Loki, ArgoCD 등)이 있었는데 역시 <strong>클라우드 네이티브 생태계</strong>는 강력하고 재밌는 기술들이 참 많은 것 같습니다. 특히, ArgoCD와 OpenTelemetry는 평소에 관심을 갖고 있었는데 이번 기회에 나름 잘(?) 써보았기에 좋은 경험이었습니다.(이제 홈랩에 붙이면 딱일 것 같습니다.ㅎㅎ)</p>
<blockquote>
<h3 id="협업-회고">협업 회고</h3>
<p>음,, 많이 힘들었습니다. 이미 어느정도 진행이 된 프로젝트가 아닌, 아이디어 기획부터 시작하는 프로젝트이다보니 개발 외적으로 많은 에너지가 쓰인 것 같습니다. 주제 선정에만 4주가 걸렸던 것 같고(?).. 개발에 익숙하지 않은 팀원들도 있었기에 팀원 간의 작업 속도나 기술적인 소통에도 큰 어려움이 많았던 것 같습니다.</p>
</blockquote>
<p>최대한 이를 줄여보고자 팀 내 기술 세미나도 진행해보고, 팀원 간 공유를 위한 문서화에 많은 시간을 쏟았던 것 같습니다..(모두에게 도움이 되었던 순간이었으면 좋겠네요..ㅎㅎ)
새로운 협업 툴도 많이 사용하게 되었는데 대표적으로 <em><strong>Jira</strong></em> 와 <em><strong>Confluence</strong></em> 를 깊게 사용해볼 수 있어서 좋았습니다. (<del>혼자 Jira를 써보았을 때는 큰 효과를 못봤습니다.ㅋㅋ</del>) </p>
<blockquote>
</blockquote>
<p>개인적으로 시간이 더 주어졌더라면, 많은 것들을 구축해보았을텐데 프로젝트 기간이 짧았다는 점에 아쉬움이 많이 남는 것 같습니다.
마지막으로, <del>취업에 성공했음에도</del> 끝까지 프로젝트에 함께 참여해준 <em><strong>여*환</strong></em> 에게 매우매우 많이 고맙다는 말을 전하고 싶습니다.ㅎㅎ</p>
<h3 id="프로젝트-github-저장소">프로젝트 GitHub 저장소</h3>
<p><a href="https://github.com/TEAM-High-Cloud/Observability">https://github.com/TEAM-High-Cloud/Observability</a>
(ReadME 작성도 해야겠네요...)</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/ab5757cf-edf4-4c90-930a-c49c4000ad9c/image.png" alt=""> 최고의 동료 Claude.. 둘이서 많은 작업을 했습니다. ㅋㅋ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] On-Prem Multi-Node Observability System & Analyzer AI Agent - 2️⃣ Observability 구축 과정]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-On-Prem-Multi-Node-Observability-System-Analyzer-AI-Agent-2-Observability-%EA%B5%AC%EC%B6%95-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@pizza_loves_me/Project-On-Prem-Multi-Node-Observability-System-Analyzer-AI-Agent-2-Observability-%EA%B5%AC%EC%B6%95-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 10 Jul 2026 10:08:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난번 포스트에 이어서 이번에는 Observability 구축 과정을 정리해보겠습니다.
(<del>언제 다 정리하지,,</del>)</p>
</blockquote>
<h1 id="구축-과정">구축 과정</h1>
<h2 id="0-초기-상태">0. 초기 상태</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/8e9577d9-82d9-42e3-9bff-86b0a6582caa/image.png" alt=""></p>
<blockquote>
<p><strong>📌 주요 포인트</strong></p>
</blockquote>
<p>옵저버빌리티 시스템을 구축하기 전, 초기 상태입니다.</p>
<blockquote>
</blockquote>
<ul>
<li>관측 대상: OpenStack 클러스터를 구성하는 노드들</li>
<li>고려사항 1: On-Prem Node를 원격 관측</li>
<li>고려사항 2: 옵저버빌리티 인프라 구축 상태 &#39;Zero&#39;</li>
</ul>
<p>위와 같은 고려사항들이 있었습니다.</p>
<h2 id="1-provisioning---iac-ansible">1. Provisioning - IaC (Ansible)</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/c97566be-2abf-481e-9292-f3a9b1ad12dd/image.png" alt=""></p>
<blockquote>
<p><strong>📌 주요 포인트</strong></p>
</blockquote>
<ul>
<li>IaC (Infrastructure as Code)<ul>
<li>코드 기반 인프라 구축</li>
<li>Ansible 사용</li>
</ul>
</li>
<li>SSOT (Single Source Of Truth)<ul>
<li>GitHub 소스 코드를 단일 진실 공급원으로</li>
</ul>
</li>
<li>구독 대상<ul>
<li>On-Prem Node &amp; Public Cloud Instance</li>
</ul>
</li>
</ul>
<p>인프라 구축이 필요했고, <strong>IaC + GitOps</strong> 방식으로 인프라 구축을 하고자 했습니다.
제가 관리를 해야하는 인스턴스는 <strong><em>[수집기 / 메트릭 서버 / 로그 서버 / 대시보드 서버]</em></strong> 등 서로 다른 역할을 하는 다수의 서버였습니다.</p>
<p>해당 인스턴스들의 <strong>초기 구축 / 원격 구독 / 공통 작업 등</strong> 반복되는 작업들이 예상되었기에 _<strong>Ansible</strong>_을 사용하여 프로비저닝 자동화를 하고자 했습니다.</p>
<h3 id="ansible-이란">Ansible 이란?</h3>
<img src="https://velog.velcdn.com/images/pizza_loves_me/post/40e7f028-216a-42e3-a193-cd7ca6e390b0/image.png" width="75%" height="75%">

<blockquote>
<p>수십, 수백 대의 서버를 명령어 몇 줄로 자동 제어할 수 있게 도와주는 오픈소스 인프라 자동화 툴
(우리 프로젝트에서는 10대 미만의 서버를 다루지만, 그래도 반복되는 작업은 번거로우니까 IaC를 도입해봅시다..!)</p>
</blockquote>
<p>Ansible은 저도 이번 프로젝트에서 처음 사용해보게 되었는데, _<strong>주요 특징</strong>_으로는</p>
<ol>
<li>Agentless<ul>
<li>다른 도구들은 제어 대상 서버마다 에이전트 프로그램을 깔아야 하고, 이들이 자원을 소모하고, 관리가 번거롭다</li>
<li>앤서블은 오직 SSH 통신만 사용함.</li>
</ul>
</li>
<li>Idempotence (멱등성)<ul>
<li>연산을 여러 번 적용하더라도 결과가 달라지지 않음</li>
<li>똑같은 앤서블 코드를 여러 번 돌려도 서버는 항상 내가 선언한 정상 상태를 유지함</li>
</ul>
</li>
<li>YAML 문법</li>
</ol>
<p>이러한 것들이 있습니다.</p>
<p>_<strong>Ansible 운영의 핵심 요소</strong>_로는</p>
<ol>
<li>Control Node &amp; Target Node<ul>
<li>Control: 앤서블 프로그램이 깔려 있고, 명령을 내리는 주체</li>
<li>Target: 명령을 받아서 수행하는 대상 서버</li>
</ul>
</li>
<li>Inventory<ul>
<li>앤서블이 제어할 서버들의 주소록</li>
</ul>
</li>
<li>Playbook<ul>
<li>앤서블이 서버에서 실행해야 할 작업 명세서</li>
</ul>
</li>
</ol>
<p>위와 같은 것들이 있습니다.</p>
<blockquote>
<h3 id="작업-흐름">작업 흐름</h3>
</blockquote>
<ol>
<li>Control Node 에서 Playbook과 Inventory 를 준비</li>
<li>Ansible 동작 명령 실행 (예시: <code>ansible-playbook -i inventory.ini setup.yml</code>)</li>
<li>Ansible이 Inventory에 적힌 구독 서버들로 SSH 접속을 시도</li>
<li>접속이 성공하며 Playbook에 적힌 명령들을 파이프라인처럼 순서대로 실행하여 서버 세팅</li>
</ol>
<hr>
<p>기본적인 내용은 이정도이고, 파악했으니 만들어봅시다!</p>
<blockquote>
<h3 id="1-control-node-등록">1. Control Node 등록</h3>
<p>프로젝트 규모나 전체 맥락을 보았을 때, 컨트롤 노드를 또 준비하는 것보다 현재 개발 중인 로컬을 컨트롤 노드로 등록하는 것이 관리하기 편하다고 생각을 했습니다.
-&gt; 그래서 제 맥북을 컨트롤 노드로 두고 작업을 이어나갔습니다.</p>
</blockquote>
<blockquote>
<h3 id="2-inventory-생성하기">2. Inventory 생성하기</h3>
<p>앞서 설명했듯이 Ansible Inventory는 <strong>&quot;앤서블이 제어할 서버들의 주소록&quot;</strong> 입니다.
사전에 생성만 하고 VPN으로 묶어두었던 여러 인스턴스들을 등록해주었습니다.</p>
</blockquote>
<pre><code># inventory.ini

[argocd_server]
argocd ansible_host=100.91.4.60

[monitoring_servers]
metric ansible_host=100.87.44.83
log    ansible_host=100.68.2.91
dashboard   ansible_host=100.73.160.40

[onprem_pcs]
onprem-pc1   ansible_host=100.65.153.106 ansible_user=highcloud
onprem-pc2   ansible_host=100.123.230.70 ansible_user=highcloud

[all:vars]
ansible_user=ubuntu

ansible_ssh_private_key_file=~/.ssh/observability.pem</code></pre><p>(⚠️ 프로젝트가 종료되었고, 모든 인스턴스가 내려갔기 때문에 굳이 IP를 가리지 않았습니다. 실제 운영 환경 및 GitHub에는 절대로 IP를 그대로 올리지 마시길...)</p>
<blockquote>
<h3 id="3-playbook-작성---k3s-일괄-설치-명세">3. Playbook 작성 - (k3s 일괄 설치 명세)</h3>
<p>Ansible Playbook은 <strong>&quot;앤서블이 서버에서 실행해야 할 작업 명세서&quot;</strong> 라고 했습니다. GitOps 방식을 채택했고, (추후) ArgoCD를 도입할 것이기에 ArgoCD의 명령을 받으면서 서버 리소스는 너무 잡아 먹지 않는 경량 쿠버네티스를 설치하는 작업이 필요했습니다.</p>
</blockquote>
<pre><code># setup-k3s.yaml

---
- name: Bootstrap
  hosts: all 
  become: true
  gather_facts: yes

  tasks:
    # -----------------------------------------------------------------
    # Task 1: k3s 바이너리 설치
    # -----------------------------------------------------------------
    - name: 1. k3s 초경량 핵심 엔진 설치 (불필요한 컴포넌트 비활성화 및 TLS 설정)
      shell: |
        curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=&quot;--disable traefik --disable metrics-server --disable local-storage --tls-san {{ ansible_host }} --node-name {{ inventory_hostname }}&quot; sh -
      args:
        creates: /usr/local/bin/k3s

    # -----------------------------------------------------------------
    # Task 2: 일반 유저 환경 세팅
    # -----------------------------------------------------------------
    - name: 2. 현재 접속 유저의 홈 디렉토리에 .kube 폴더 생성
      file:
        path: &quot;/home/{{ ansible_user }}/.kube&quot;
        state: directory
        owner: &quot;{{ ansible_user }}&quot;
        group: &quot;{{ ansible_user }}&quot;
        mode: &#39;0755&#39;

    - name: 3. 쿠버네티스 인증 설정 파일(k3s.yaml)을 유저 폴더로 복사
      copy:
        src: /etc/rancher/k3s/k3s.yaml
        dest: &quot;/home/{{ ansible_user }}/.kube/config&quot;
        remote_src: true
        owner: &quot;{{ ansible_user }}&quot;
        group: &quot;{{ ansible_user }}&quot;
        mode: &#39;0600&#39;
    # -----------------------------------------------------------------
    # ★ 추가된 구간: 외부 원격 접속 및 타임아웃 방지를 위한 주소 자동 치환(05.19 18:38 추가)
    # -----------------------------------------------------------------
    - name: &quot;3-1. [Fix] 복사된 config 파일 내의 접속 주소를 각 서버의 실제 IP로 자동 치환&quot;
      replace:
        path: &quot;/home/{{ ansible_user }}/.kube/config&quot;
        regexp: &#39;https://127.0.0.1:6443&#39;
        replace: &quot;https://{{ ansible_host }}:6443&quot;
    # -----------------------------------------------------------------
    # Task 3: k3s 서비스 정상 기동 확인
    # -----------------------------------------------------------------
    - name: 4. k3s 서비스가 정상적으로 활성화 및 동작 중인지 상태 검증
      systemd:
        name: k3s
        state: started
        enabled: true</code></pre><p>Ansible은 구독한 서버에 SSH 접속이 되면, 위 Playbook에 작성한 명세를 순차적으로 실행합니다.</p>
<blockquote>
<h3 id="4-group_vars-설정">4. group_vars 설정</h3>
<p>Ansible이 원격 구독 서버에 SSH 접속을 하기 위해서는 당연히 SSH 비밀번호 혹은 키가 필요합니다. 이에 대한 등록/설정이 group_vars 입니다.</p>
</blockquote>
<pre><code># infrastructure/all.yaml

---
# .pem 키 사용 시
ansible_ssh_private_key_file: &quot;/Users/shinji/.ssh/observability.pem&quot;

---
# 비밀번호 사용 시
ansible_ssh_private_key_file: &quot;&quot;
ansible_ssh_pass: &quot;1025&quot;
</code></pre><blockquote>
<h3 id="5-ansible-명령-실행하기">5. Ansible 명령 실행하기</h3>
<p>앞선 작업들로 <strong>Control Node 설정 -&gt; inventory 생성 -&gt; playbook 작성 -&gt; group_vars 설정</strong> 을 완료하였습니다. 이제 Control Node 에서 실행 명령을 입력하면 됩니다.</p>
</blockquote>
<pre><code>ansible-playbook -i inventory.ini setup-k3s.yaml</code></pre><p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/6287a9b4-1de0-403f-b674-74873d5b7cc8/image.png" alt=""></p>
<p>-&gt; k3s 설치를 위한 Playbook이 모든 구독 서버에 대해 동작을 완료했습니다.</p>
<hr>
<h2 id="2-gitops---중앙-관리형-argocd">2. GitOps - 중앙 관리형 ArgoCD</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/40e76454-b25e-49ef-9a57-84fc92a52284/image.png" alt=""></p>
<blockquote>
<p><strong>📌 주요 포인트</strong></p>
</blockquote>
<ul>
<li>IaC Pipeline 활용<ul>
<li>k3s 공통 설치</li>
<li>각 서버의 역할에 맞추어 구축</li>
</ul>
</li>
<li>ArgoCD 도입<ul>
<li>개별 ArgoCD 구축 -&gt; 중앙 관리형 ArgoCD 구축</li>
<li>App of Apps Pattern</li>
</ul>
</li>
</ul>
<p>앞서 구축한 IaC 파이프라인을 활용하여 각 서버의 역할에 맞게</p>
<ul>
<li>(공통) k3s</li>
<li>OpenTelemetry Collector</li>
<li>VictoriaMetrics</li>
<li>Loki</li>
<li>Grafana</li>
<li>ArgoCD</li>
</ul>
<p>를 설치 및 구축했습니다.</p>
<p>초기 구축 시 ArgoCD를 각각의 인스턴스에 구축했는데, 관리포인트가 많다는 문제점을 느껴 싹 다 밀고 중앙 관리형 ArgoCD 서버 한 대만 두기로 하였습니다.
실제로 이러한 상황에서 중앙 관리형 ArgoCD 도입이 적절한지 조사를 해보았는데, Best Practice 중 <strong>&quot;App of Apps Pattern&quot;</strong> 이 있더군요. 말 그대로 Application들의 Application을 두는 패턴입니다.</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/76084b2a-87b3-417b-a200-551ca59b41dd/image.png" alt=""></p>
<p>(중앙 관리형 ArgoCD가 관리하고 있는 Application들 입니다.)</p>
<hr>
<h2 id="3-basic-flow">3. Basic Flow</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/522434c2-03b3-4214-96d7-14f884de1c66/image.png" alt=""></p>
<blockquote>
<p>앞선 과정들을 통해 기본적인 옵저버빌리티 흐름을 만들었습니다.</p>
</blockquote>
<ul>
<li><strong>온프레미스 노드</strong>에 DaemonSet 형식으로 배포한 <strong>OTel Collector</strong>는 <strong>Metric/Log 시그널을 수집</strong>하여 적절한 서버에 보냄. (Metric Server / Log Server)</li>
<li>이를 전달 받은 <strong>Metric/Log Server</strong>는 각각의 <strong>보관 정책</strong>에 알맞게 Signal을 저장 및 관리<ul>
<li><strong>EBS(Elastic Block Storage)</strong>: Metric(hot) 저장</li>
<li><strong>S3(Simple Storage Service)</strong>: Metric(cold), Log 저장</li>
</ul>
</li>
<li><strong>Grafana</strong> 는 주기적으로 두 서버에 query를 보내어 <strong>대시보드</strong> 형성</li>
<li>일정 시간 이상 &#39;이상 신호&#39; 감지 시 <strong>Alertmanager를</strong> 통해 관리자용 Slack으로 <strong>Alert</strong> 발송</li>
</ul>
<p><em><strong>&quot;수집 -&gt; 저장 및 쿼리 -&gt; 관측 -&gt; 알람&quot;</strong></em> 까지 동작하는 기본적이면서도 필수적인 흐름입니다.</p>
<h3 id="📌-signal-store">📌 Signal Store</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/82d070e2-1ed4-4a96-8626-75380fa689d7/image.png" width="50%" height="50"> Signal 저장 부분을 확대해서 가져왔습니다.</p>
<blockquote>
<h3 id="🤔-왜-ebss3-와-같은-외장-스토리지에-저장을-했나요">🤔 왜 EBS/S3 와 같은 외장 스토리지에 저장을 했나요?</h3>
<p>우선 각 인스턴스의 <strong>로컬 디스크</strong>에 시그널을 저장하지 않고, 외장 스토리지를 도입한 이유는</p>
</blockquote>
<ul>
<li>EC2 종료/장애 시 즉시 소실</li>
<li>인스턴스 디스크 용량 한계</li>
<li>인스턴스 비용 추가 청구</li>
<li>백업/복구 직접 구현 필요
등의 이유 때문입니다.<blockquote>
</blockquote>
</li>
<li><em>외장 스토리지*</em>를 연결하면 위 문제점들에 대해</li>
<li>인스턴스와 별개로 시그널 보존</li>
<li>대용량 확장 가능</li>
<li>저장 비용 저렴 (EBS, S3 &gt;&gt; EC2)</li>
<li>Snapshot 기반 백업, AZ/region 단위 내구성, 라이프사이클 자동화
등의 장점을 확보할 수 있었습니다.</li>
</ul>
<blockquote>
<h3 id="🤔-그렇다면-왜-ebs-s3-두-개를-썼나요">🤔 그렇다면 왜 EBS, S3 두 개를 썼나요?</h3>
<p>위 사진에서 볼 수 있듯이 외장 스토리지로 사용하는 스토리지는 EBS와 S3 두 개 입니다.</p>
</blockquote>
<p>이러한 구조는 초기 설계 단계에서 구상하진 못했습니다. <strong>VictoriaMetrics</strong> 에 대해 공부해보니, VictoriaMetrics는 로컬 블록 스토리지를 기본 스토리지 드라이버로 사용한다고 합니다. 추가적으로 VictoriaMetrics는 S3 Native 모드 자체가 없습니다..!
그렇기에 운영을 위한 스토리지로 Metric은 EBS에 저장하는 방식을 택했고(<del>택할 수 밖에 없었고</del>) 장기 보관은 보관 비용 절약 및 보관 용량을 고려하여 S3에 저장하는 방식을 택했습니다.</p>
<blockquote>
</blockquote>
<p><strong>hot - 운영
cold - 장기 보관</strong></p>
<p>(보관 정책은 별다른 기준이 없었기에 적절히 지정했습니다.)</p>
<table>
<thead>
<tr>
<th></th>
<th align="left">Metric(V.M)</th>
<th align="center">Log(Loki)</th>
</tr>
</thead>
<tbody><tr>
<td>1차 저장</td>
<td align="left">EBS</td>
<td align="center">S3</td>
</tr>
<tr>
<td>쿼리 가능 기간</td>
<td align="left">15d</td>
<td align="center">7d</td>
</tr>
<tr>
<td>백업/안전망 위치</td>
<td align="left">S3(별도 버킷)</td>
<td align="center">S3(같은 버킷, lifecycle)</td>
</tr>
<tr>
<td>백업/안전망 활성화</td>
<td align="left">매일 자동</td>
<td align="center">compactor 실패 한정</td>
</tr>
<tr>
<td>장기 보관 - Archive</td>
<td align="left">Glacier IR(90~365d)</td>
<td align="center">Glacier Flexible(14~365d)</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-alert">4. Alert</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/6910c53a-6c28-41e6-be70-390ca7bb59bd/image.png" alt=""></p>
<blockquote>
<h3 id="alert">Alert?</h3>
<p>임계치를 넘는 이상 신호 발견 시 관리자용 Slack으로 알람을 보내는 기능입니다.</p>
</blockquote>
<blockquote>
<h3 id="동작-방식">동작 방식</h3>
<p>각각의 Metric / Log Server에는 임계치를 설정하고 감지하는 ruler 라는 컴포넌트가 있습니다. VictoriaMetrics의 경우 vmalert이고, Loki의 경우 그냥 Ruler 입니다..ㅋㅋ</p>
</blockquote>
<p>각각의 ruler는 이상 신호의 기준이 되는 임계치 정보를 갖고 있습니다. (&lt;- 임계치는 관리자가 커스텀 가능합니다.)
ruler는 Metric/Log Server에 일정 간격으로 query를 보내고, 일정 시간 이상 임계치가 넘는 이상 신호를 감지하면 Alertmanager에게 Firing Signal을 보내게 됩니다.
최종적으로 Alertmanager는 설정해둔 관리자용 Slack 채널로 알람을 보내게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/2f73947a-ea77-4b44-a12d-5ce734ad3851/image.png" alt=""> (vmalert, Ruler에 설정한 임계치 정보입니다.)</p>
<blockquote>
<h3 id="실제로-관측한-이상-신호가-있었나요">실제로 관측한 이상 신호가 있었나요?</h3>
<p>-&gt; 넵!
가장 크리티컬(?) 했던 알람으로는 NodeDown과 HighFileSystem이 있었습니다.</p>
</blockquote>
<hr>
<blockquote>
</blockquote>
<p><strong>1️⃣ NodeDown</strong></p>
<ul>
<li>발생 일시: 06월 06일</li>
<li>증상: PC VPN 접속 불가</li>
<li>원인: 부트캠프 입주 건물 시설 점검으로 인한 정전...</li>
<li>해결: 출근 후 PC 재시작 및 VPN 정상화<blockquote>
</blockquote>
</li>
<li><em>2️⃣ HighFileSystem*</em></li>
<li>발생 일시: 06월 18일</li>
<li>분석: /var/lib/docker(24 GiB), /var/log/journal(4.1GiB)</li>
<li>해결: Docker 미사용 리소스 정리, Docker 로그 드라이버 제한</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/41489be5-ee22-496d-99d5-2283ab17e8c5/image.png" width="50%" height="50"> (<del>이왜진ㅋㅋㅋㅋㅋㅋㅋ 옵저버빌리티 일 잘하네ㅋㅋㅋㅋㅋㅋㅋ</del>)</p>
<hr>
<h1 id="정리">정리</h1>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/8cd37a4a-e266-47f4-a19e-d638cc939908/image.png" alt=""></p>
<blockquote>
<p>짜잔</p>
</blockquote>
<blockquote>
<p>여러 과정을 거쳐 Observability 구축이 완료되었습니다. 이전 백엔드 프로젝트 때는 Prometheus + Grafana 구축만 해두고 &quot;옵저버 간단하네&quot; 라고 했던 것 같은데 (<del>늘 겸손하기..</del>),,,
제대로 구축해보려고 하니 복잡한 내용도 많았고 재밌는 내용이 정말 많았던 것 같습니다.</p>
</blockquote>
<p>여기까지 구축하니 &quot;<strong>관측도 잘 되고, 알람도 잘 가고 좋네&quot;</strong> 라는 생각과 함께 <em><strong>&quot;장애 원인 분석과 해결을 해주는 녀석이 있으면 좋겠는데?&quot;</strong></em> 라는 생각이 들었습니다. (트러블슈팅은 항상 어렵기 때문에,,)</p>
<blockquote>
<p>다음은 이러한 생각에서 시작된 &quot;분석용 AI Agent 구축 과정&quot;을 정리해보겠습니다.
감사합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] On-Prem Multi-Node Observability System & Analyzer AI Agent - 1️⃣ 프로젝트 개요 및 기술 스택 선정]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-On-Prem-Multi-Node-Observability-System-Analyzer-AI-Agent</link>
            <guid>https://velog.io/@pizza_loves_me/Project-On-Prem-Multi-Node-Observability-System-Analyzer-AI-Agent</guid>
            <pubDate>Fri, 10 Jul 2026 07:36:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번 상반기에 수료한 <em><strong>부트캠프(현대오토에버 모빌리티 SW스쿨 - 클라우드)</strong></em> 에서 진행한 최종 프로젝트의 구축 과정을 적어보고자 합니다.
(정신이 매우 없었기에 최종 결과물을 만든 후에 구축 과정기를 적게 되네요..ㅜ)</p>
</blockquote>
<hr>
<h1 id="개요">개요</h1>
<blockquote>
</blockquote>
<ul>
<li>프로젝트 기간: 2026년 4월 13일 ~ 6월 29일</li>
<li>프로젝트 주제: 오픈스택을 활용한 클라우드 제공 플랫폼 구축</li>
<li>팀 구성: 5인</li>
<li>맡은 역할: 팀장, Observability, Analyzer AI Agent</li>
</ul>
<p>부트캠프의 꽃(?) _<strong>최종 프로젝트</strong>_를 무사히 마쳤습니다. 프로젝트 기간이 짧으면서 길었던 것 같습니다. 기깔나는 인프라 프로젝트를 해보고 싶은 욕심에 정말 많은 노력을 들였던 프로젝트였던 것 같습니다..하하</p>
<p>주제 선정부터 정말 많은 공을 들였습니다. 동아리 활동과 오픈소스 활동을 하며 인연을 맺은 10명의 현직자분들에게 실제 현업에서 겪는 문제점이나, 중요하게 생각하는 부분을 많이 여쭤보았습니다. (백엔드, 시스템 엔지니어, 데브옵스, SRE 등 다양한 직군의 현직자분들에게 각 분야의 이야기를 들어보고 공통점을 찾고자 했습니다!)</p>
<p>각 분야에서 중요하게 다루는 부분과 실제로 겪으신 문제점(장애)들을 공유 받았고, 서로 다른 분야임에도 공통적인 요소로 <strong><em>&quot;HA, DR</em>&quot;</strong> 을 언급해주셨습니다.</p>
<h2 id="🤔-ha-dr이-뭔가요">🤔 HA, DR이 뭔가요..?</h2>
<p><strong>HA(High Available), DR(Disaster Recovery)</strong> 의 약자로 각각 <strong>고가용성, 재해 복구</strong>를 의미합니다.</p>
<p>(인프라 분야를 공부하면서 종종 봤던 용어들인데, 이번 기회에 각각의 용어가 무엇을 의미하는지 자세히 알아보았습니다.)</p>
<blockquote>
</blockquote>
<h3 id="hahigh-available---고가용성">HA(High Available) - 고가용성</h3>
<ul>
<li>시스템이 &quot;중단 없이(또는 아주 잠깐의 중단만으로) 계속 서비스를 제공할 수 있는 상태&quot;</li>
<li>핵심 원리: 이중화(Redundancy)와 자동 이관(Failover)<h3 id="drdisaster-recovery---재해-복구">DR(Disaster Recovery) - 재해 복구</h3>
</li>
<li>지진, 화재, 대규모 정전, 전쟁, 또는 데이터 센터 전체가 마비되는 &quot;거대한 재해 상황에서 시스템과 데이터를 다시 살려내는 것&quot;</li>
<li>핵심 원리: 원격지 백업과 복구 시나리오</li>
</ul>
<p>간단하게 SW / HW 장애가 발생했을 때 시스템이 중단되지 않고 계속 서비스를 제공할 수 있는 능력이라고 이해를 했습니다.</p>
<p>이를 잘 보여주기 위해서 온프렘 노드(부트캠프 제공 데스크탑) 이중화, 하이브리드 클라우드, 장애 시나리오 시연, 관측 시스템 등의 도입을 논의하고 주제를 구체화했습니다.</p>
<p>이를 바탕으로 다양한 아이디어가 나왔고, 수차례의 회의와 수차례의 멘토링을 통해 <strong>&quot;OpenStack 기반 클라우드 제공 플랫폼 구축&quot;</strong> 이라는 주제를 선정하게 되었습니다. 구현 과정과 동작 방식은 다르지만, 이미 홈랩 클러스터로 사용자에게 인스턴스를 제공하는 프로젝트를 진행하고 있었기에 OpenStack 구축보다 _<strong>Observability 구축</strong>_에 집중하고자 하였습니다.</p>
<hr>
<h1 id="🔭-observability">🔭 Observability</h1>
<blockquote>
<p><em><strong>기술 스택 선정 이유 -&gt; 구축 과정 -&gt; Alert -&gt; 분석 AI Agent</strong></em> 순으로 정리를 해보겠습니다.</p>
</blockquote>
<h2 id="기술-스택-선정-이유">기술 스택 선정 이유</h2>
<blockquote>
</blockquote>
<ul>
<li>Collector: OpenTelemetry Collector</li>
<li>Metric Server: VictoriaMetrics</li>
<li>Log Server: Grafana Loki</li>
<li>Dashboard: Grafana</li>
</ul>
<h3 id="⚙️-collector-prometheus-vs-opentelemetry-collector">⚙️ Collector: Prometheus vs OpenTelemetry Collector</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/739dd267-81b0-4a83-89b0-dcf30ced8b99/image.png" alt=""></p>
<blockquote>
<p>관측 대상(온프렘 노드)의 Metric과 Log를 수집하는 수집기의 스택 선정을 먼저 진행 했습니다.</p>
</blockquote>
<p>다른 프로젝트에서 많이 사용했던 <strong>Prometheus</strong>와 클라우드 네이티브 진영에서 핫한(?) <strong>OpenTelemetry Collector</strong>를 고민했습니다. 
(OpenTelemetry를 한 번쯤은 꼭 사용해보고 싶었지만, 기술 스택 선정 이유의 타당성은 필수이기에 조사를 통해 두 기술 스택을 비교해보았습니다.ㅋㅋㅋ)</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/45611143-1d26-4b71-882a-1b15b5c787c2/image.png" alt=""></p>
<p>(더 많은 이유가 있겠지만) 비교 및 조사를 해보니 왜 요즘 OpenTelemetry가 각광 받는지 알 것 같습니다.</p>
<p>우선 <strong>원격 관측 방식</strong>으로 옵저버빌리티 시스템을 구축하고자 했기에 <strong>원격 관측 - Push Protocol 지원 여부</strong>가 중요했습니다. 또 초기 설계 당시 Metric과 Log 두 시그널을 수집하고자 하였는데 <strong>OTel Collector</strong>의 경우 <strong><em>단 한 개의 Agent</em></strong> 만으로 세 시그널(Metric, Log, Trace)를 수집할 수 있다는 장점이 있었습니다!</p>
<blockquote>
</blockquote>
<h3 id="결론-opentelemetry-collector-선정">결론: OpenTelemetry Collector 선정!</h3>
<ul>
<li>이유 1: OTLP(OpenTelemetry Protocol) 규격, 단 한 대의 Agent 만으로 Log/Metric/Trace 모두 수집 가능</li>
<li>이유 2: Push Protocol 지원 (OTLP - gRPC/HTTP 지원)</li>
<li>이유 3: 써보고 싶었음.ㅋ</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/533ad288-f2eb-4153-9765-4fa42d60f420/image.png" width="50%" height="50%"> (노트북 한 켠에 붙여있는 OpenTelemtry를 드디어 사용해보네요.ㅎㅎ)</p>
<h3 id="⚙️-metric-server-prometheus-vs-victoriametrics">⚙️ Metric Server: Prometheus vs VictoriaMetrics</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/c95061f6-919d-4ece-b5e6-bbaaa93c533e/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>이번에는 Metric Server 입니다. 메트릭 서버 또한 표준처럼 많이들 사용하는 Prometheus 와 VictoriaMetrics 스택을 비교했습니다.
VictoriaMetrics는 홈랩의 옵저버빌리티 스택을 조사하던 중 알게 된 오픈소스 기술입니다. 원격 관측에 유리하고, 리소스 비용이 상대적으로 저렴하다는 특징 정도만 알고 있었는데, 이번 기회에 Prometheus와의 비교를 위해 조사를 해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/82deadfc-d103-4ba5-b196-eee46505980a/image.png" alt=""></p>
<p>앞선 Collector 채택 시 &quot;원격 관측&quot;을 목표로 하였기에 Push Protocol 호환과 OTel Collector 와의 호환이 중요했습니다. VictoriaMetrics 설계 철학이 &#39;원격 관측&#39;이라고 하더군요.😳
이와 더불어 OTel Collector와의 호환성 또한 뛰어났고, 가장 중요한 _<strong>&quot;압축률 &amp; 메모리/디스크 효율&quot;</strong>_이 높다는 장점이 있었습니다. 원격 관측을 위해 퍼블릭 클라우드(AWS)에 메트릭 서버 등을 띄울 예정이었기에 저장 효율이 좋다는 점이 크게 다가왔습니다.</p>
<blockquote>
</blockquote>
<h3 id="결론-victoriametrics-선정">결론: VictoriaMetrics 선정!</h3>
<ul>
<li>이유 1: 다양한 Push Protocol과 호환 (우리는 OTLP 사용)</li>
<li>이유 2: OTel Collector 와의 호환성 높음</li>
<li>이유 3: 압출률, 메모리/디스크 효율 뛰어남</li>
</ul>
<p>.
.
.</p>
<p>계속 언급하는 &quot;원격 관측&quot;에 집착하는 이유,,</p>
<blockquote>
<p>예전에 백엔드로 참여한 프로젝트에서 내장 관측 형태로 Prometheus-Grafana 를 구축한 적이 있었습니다. 인스턴스 비용이 매우 부족했기 때문에 하나의 EC2에 &#39;백엔드 서버&#39;와 &#39;모니터링 서버&#39;를 같이 띄우는...ㅎ</p>
</blockquote>
<p>평상 시에는 문제 없이 관측을 잘 했지만, 인스턴스 자체가 다운되는 장애가 발생했을 때 (당연하게도) 모니터링 서버 또한 다운되어 장애 감지도 못하고, 메트릭도 날아가버린 아찔한 경험이 있었습니다.</p>
<blockquote>
</blockquote>
<p>이때 이후로 &quot;내장 관측&quot;을 다시는 하지 않는 사람이 되었답니다..😂</p>
<hr>
<h3 id="⚙️-log-server-elasticsearch-vs-grafana-loki">⚙️ Log Server: ElasticSearch vs Grafana Loki</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/d335cbd8-6a88-47f4-9a9a-60e6287ec229/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>마지막으로 Log Server 입니다. Elasticsearch를 현업에서 많이 사용한다고 하던데,, 비용 감당이 안 될 것 같으니 취업하고 써보는 걸로 하겠습니다..
(<del>그래도 스택 선정을 위한 비교 및 조사는 해보겠습니다</del>)</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/fa5a851e-15e3-44a6-a880-bf80c42d2011/image.png" alt=""></p>
<p>아무래도 Full-Text indexing을 하는 ElasticSearch의 검색력이 뛰어남을 보여주는 것 같습니다. 하지만 현재 구축한 플랫폼의 규모와 목적을 보았을 때 굳이 큰 비용을 들이면서까지 로그 검색 품질을 높일 필요는 없다고 판단했습니다.
또 로그 서버 또한 원격 관측에 맞추어 퍼블릭 클라우드(AWS)에 띄울 예정이었기에 리소스 자체의 비용이 저렴한(가벼운) Grafana Loki 가 적절하다고 생각했습니다.
(결정적으로 우리가 쓸 Log Server의 기능은 Loki에서는 모두 무료였습니다.)</p>
<blockquote>
<h3 id="opensearch">OpenSearch</h3>
<p>ElasticSearch는 비용 및 프로젝트 기간 등을 고려하여 선택하지 않았습니다만, 이를 대체할 수 있는 OpenSearch 라는 프로젝트가 있었습니다. 
(작년 OSS Korea 2025 행사장에서 봤던 기억이 있네요!)
<a href="https://opensearch.org/">https://opensearch.org/</a></p>
</blockquote>
<p>&quot;엘라스틱서치(Elasticsearch) 7.10.2 버전을 기반으로 포크(Fork)하여 개발된 커뮤니티 주도의 100% 오픈소스 검색 및 분석 엔진&quot; 이라고 합니다.</p>
<blockquote>
</blockquote>
<p>프로젝트 기간 및 쌓여있는 Epic/Task 등을 고려하여 이번에는 사용하지 않았는데, 추후 기회가 된다면 OpenSearch를 사용해보면 좋을 것 같습니다.</p>
<hr>
<blockquote>
<p>양이 너무 많을 것 같아서 이번 포스트에서는 &quot;프로젝트 개요 / 기술 스택 선정&quot;까지만 정리르르 해보겠습니다. 다음 포스트에서는 Observability 구축 과정에 대해서 다뤄보겠습니다. 감사합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[2026년 상반기 회고]]></title>
            <link>https://velog.io/@pizza_loves_me/2026%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@pizza_loves_me/2026%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 07 Jul 2026 18:00:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>한동안 블로그 작성을 못했습니다. 마지막으로 작성한 블로그 글이 3월 17일이네요,,, 다시 꾸준히 블로그를 써야겠습니다.</p>
</blockquote>
<p>일단 올해 상반기는 너무 바쁘고 정신이 없었습니다. 이번에는 상반기에 <em><strong>나는 무엇을 했는지</strong></em> 회고해보고 <em><strong>앞으로 어떻게 나아가야 할지</strong></em> 생각 정리 겸 글을 적어보겠습니다.</p>
<h2 id="1-홈랩-프로젝트-2512--현재">[1] 홈랩 프로젝트 (25.12 ~ 현재)</h2>
<p>블로그 시작의 계기가 되었던 라즈베리파이를 활용한 홈랩 구축 프로젝트입니다. 작년 종강 바로 다음 날부터 부트캠프 시작과 함께 매일 밤 홈랩 구축에 전념했습니다. 구축을 하며 &#39;가정용 네트워크&#39; 라는 한계에 머리가 많이 아팠지만, 정말 많은 경험을 했고 인프라 분야에 대한 지식이 많이 늘었습니다. </p>
<p>마지막 블로그 글(3월 17일)을 작성했을 때는 라즈베리파이 노드 3대를 사용한 클러스터였습니다. </p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/6a052aa5-1e2a-46bc-aa1d-11f5f34470a9/image.png" alt=""></p>
<p>부트캠프를 진행하며 너무 바빴기에 블로그 정리를 못했는데, 결과적으로 현재 홈랩은
.
.
.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/pizza_loves_me/post/252a0c96-aefb-4ebf-af11-fb11ad31c8f1/image.jpeg" alt=""> 로드밸런서 도입 (라즈베리파이)</th>
<th><img src="https://velog.velcdn.com/images/pizza_loves_me/post/aa917acc-1ff7-48de-b4dd-937a91e34c2a/image.jpeg" alt=""> 8-포트 스위치로 업그레이드</th>
</tr>
</thead>
<tbody><tr>
<td>를 거쳐서,,</td>
<td></td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/bd39d97f-0a2b-4690-8361-d82a1d59554c/image.jpeg" alt="">
x86 PC 1대(워커노드 전용)를 추가한 엄청난(?) 홈랩 클러스터가 되었습니다.
(<del>PC가 너무 비싸길래,, 동생 군입대날에 잠깐 빌렸습니다.ㅎㅎ</del>)</p>
<p>부트캠프 후반부에는 프로젝트 진행과 함께 시간이 턱없이 부족해서 방치되어 있었는데, 다시 홈랩 되살리기 프로젝트를 해야겠습니다.
시간이 날 때 &#39;로드밸런서 추가 / 도메인 기반 인스턴스 제공 / GitOps 도입&#39;에 대한 내용을 정리해서 올려보겠습니다.</p>
<hr>
<h2 id="2-부트캠프-2512--2606">[2] 부트캠프 (25.12 ~ 26.06)</h2>
<p>많이 성장하고 꽤 많이 힘들었던 과정이었습니다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/pizza_loves_me/post/d767df91-9b2d-43e6-a791-6d6f6e21c766/image.jpeg" alt=""> 우수 프로젝트상도 받았고</th>
<th><img src="https://velog.velcdn.com/images/pizza_loves_me/post/ce5c0dbc-d14d-431d-9b3a-2f2c13e8c5b7/image.jpeg" alt=""> 최종 발표도 무사히 끝마쳤습니다</th>
</tr>
</thead>
</table>
<blockquote>
<p>최종 프로젝트에서는 <code>[OpenStack 기반 온프레미스 멀티 노드를 관측하는 Observability 시스템과 분석용 AI agent 구축]</code> 을 맡아서 하게 되었는데, 자세한 구축 과정은 다음 포스트에 자세히 작성해보겠습니다.</p>
</blockquote>
<p>처음에 부트캠프에 지원하고 참여했을 때는 지식 습득과 기술적인 경험 획득만을 기대했는데, 돌이켜보니 취준 시기에 좋은 영향을 준 고마운 인연들을 많이 만나게 된 것 같습니다. </p>
<p>3학년 2학기를 막 끝마친 입장에서 <code>취업 준비</code> 는 막연하게 큰 벽이라고 생각을 했는데, 다양한 인사이트를 제공해준 친구들 덕분에 막학기를 어떻게 꾸며나갈지 방향을 잡게 된 것 같습니다. (마지막까지 함께 프로젝트를 진행하고, 많은 조언을 해 준 지환이에게 정말 고마웠다는 말을 전하고 싶네요..ㅎㅎ😊)</p>
<hr>
<h2 id="3-초록-밋업-발표--동아리-운영진-졸업--4학년-1학기">[3] 초록 밋업 발표 &amp; 동아리 운영진 졸업 &amp; 4학년 1학기</h2>
<p><em><strong>[초록 밋업 발표]</strong></em>
생각해보니 올해 1월쯤 초록 스터디 밋업에 참석하여 발표 세션을 맡기도 했습니다.
<a href="https://velog.io/@pizza_loves_me/%EC%B4%88%EB%A1%9D-%EB%B0%8B%EC%97%85-%ED%9A%8C%EA%B3%A02026.01.17">초록 밋업 회고글</a>
발표를 하기에는 부족한 점이 더 많았던 때이지만 <strong>&quot;경험 공유&quot;</strong> 를 꼭 하고 싶었기에 도전을 해보았던 발표였습니다. 다음에는 더 유익한 경험을 많은 분들에게 공유할 수 있는 사람으로 성장하여 발표에 참여하고 싶습니다! ㅎㅎ</p>
<p><em><strong>[그리디 운영진 졸업]</strong></em>
올해 3월을 기점으로 운영진으로 활동하던 교내 개발 동아리 그리디(GREEDY)를 졸업했습니다. 24년 9월 전자과에서 컴퓨터공학으로 진로를 변경하며 처음 개발을 배운 곳이고, 훌륭한 친구들에게 좋은 영향을 받으며 성장을 한 곳입니다. 졸업 후에 저도 좋은 영향을 줄 수 있는 멋진 선배가 되어서 그리디 부원들에 많은 도움을 주고 싶습니다.ㅎㅎ</p>
<p><em><strong>[4학년 1학기]</strong></em>
휴학을 하기 싫었기에 싸강 및 레포트 수업 위주로 4학년 1학기를 부트캠프와 병행했습니다. 작년까지 수업을 많이 들어놨던 덕분에 3과목 정도만 수강을 하면 되었고, 학점은...ㅎㅎ 시간을 쪼개어 최대한 열심히 했습니다.</p>
<blockquote>
<p>하계 계절학기와 막학기만 들으면 벌써 졸업입니다. 코로나로 인해 1<del>2학년 때 많은 활동을 못했던만큼 3, 4학년에는 많은 추억, 경험을 쌓고 가는 것 같습니다. 여전히 빨리 졸업하고 사회로 나가고 싶지만 마음 한켠에는 아쉬운 마음도 남는 것 같습니다. (</del>그치만 빨리 졸업하고 돈 벌고 싶네요. ㅎㅎ~~)</p>
</blockquote>
<hr>
<h2 id="4-외부-커뮤니티-활동-오픈인프라--파이콘-준비위원회">[4] 외부 커뮤니티 활동: 오픈인프라 / 파이콘 준비위원회</h2>
<blockquote>
<p>올해 3월 <code>교내 개발 동아리 그리디(GREEDY) 운영진</code> 을 졸업하며, <code>파이콘 한국 준비위원회(파준위)</code> 에 합류하게 되었습니다. 이와 함께 <code>OpenInfra Korea User Group 운영진</code> 은 작년 9월부터 꾸준히 하고 있습니다.</p>
</blockquote>
<p>다양한 사람들을 만나고, 공통의 관심사를 중심으로 사람들을 이어주는 과정에 큰 보람을 느끼기도 하고, 재밌어서 커뮤니티 운영진 활동을 계속 하는 것 같습니다.</p>
<p>또 커뮤니티 활동을 하며 <code>협업</code> 에 대해서 많이 배우게 되고, <em><strong>&quot;나는 감자였구나&quot; 🙃</strong></em> 라는 생각을 참 많이 하게 되는 것 같습니다..ㅎㅎㅋㅋ 힘든 부분도 많고 시간을 많이 쏟은 활동들이지만 그만큼 얻어가고 배운 것들도 많은 것 같습니다.</p>
<h4 id="두-커뮤니티-모두-올해-큰-행사를-앞두고-있습니다">두 커뮤니티 모두 올해 큰 행사를 앞두고 있습니다!</h4>
<p><code>파준위</code> : <em><strong><a href="https://2026.pycon.kr/">PyCon Korea 2026 (8/15~8/17)</a></strong></em>
<code>OpenInfra Korea UG</code> : <em><strong><a href="https://community2.cncf.io/events/details/cncf-kcd-south-korea-presents-kcd-x-ceph-x-openinfra-day-korea-2026/">KCD x Ceph x OpenInfra Day Korea 2026 (9/1)</a></strong></em></p>
<p>두 행사 모두 커뮤니티 운영진분들과 함께 열심히 준비하고 있으니 많은 관심과 참여 부탁드립니다. :)</p>
<hr>
<h2 id="5-kubestronaut-도전-cka-ckad-kcna-취득">[5] Kubestronaut 도전 (CKA, CKAD, KCNA 취득!)</h2>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/pizza_loves_me/post/02f3dc38-34d1-4d04-bf6d-eaa11883cdd9/image.png" alt=""> CKA</th>
<th><img src="https://velog.velcdn.com/images/pizza_loves_me/post/80225532-5e83-4f49-a827-256e019d6474/image.png" alt=""> CKAD</th>
<th><img src="https://velog.velcdn.com/images/pizza_loves_me/post/b88d7428-e2b7-4ce8-8d3d-ceb0e6b712b5/image.png" alt=""> KCNA</th>
</tr>
</thead>
</table>
<blockquote>
<p>하하하하하하 🎉🎉🎉🎉</p>
</blockquote>
<p>졸업 전 버킷리스트인 <strong>&quot;Kubestronaut 달성&quot;</strong> 을 위한 도전을 하고 있습니다. 이번 상반기에</p>
<ul>
<li><code>CKA(Certified Kubernetes Administrator)</code></li>
<li><code>CKAD(Certified Kubernetes Application Developer)</code></li>
<li><code>KCNA(Kubernetes and Cloud Native Associate)</code></li>
</ul>
<p>위 3가지 자격증을 모두 취득했습니다.</p>
<p>쿠버네티스 호소인이었지만, 쿠버네티스를 잘 몰랐던 사람이었기에 첫 자격증이었던 CKA를 위한 학습 시간에 몇 달을 쏟았던 것 같습니다. 초반에 많은 시간을 들여서 쿠버네티스 전반에 대한 학습을 깊게 가져가다 보니 이후의 CKAD, KCNA 는 상대적으로 수월하게 취득한 것 같습니다.
쿠버네티스 자격증을 3개나 취득했으니 저도 이제 쿠버네티스 전문가라고 해도 될까요?? ㅎㅎ
.
.
.
.
.</p>
<blockquote>
<h4 id="그럴리가요">그럴리가요^^</h4>
</blockquote>
<p>자격증을 취득하고, 쿠버네티스를 공부할수록 <em><strong>&quot;학습만으로, 자격증만으로는 쿠버네티스를 잘 알고 있어요! 라고 말할 수 있을까?&quot;</strong></em> 라는 물음을 스스로에게 계속 던지게 되는 것 같습니다.</p>
<p>쿠버네티스,, 정말 재밌고 복잡하고 멋진 기술이라고 생각합니다. 그만큼 어렵고, 어렵고, 어려운 것 같습니다.. (단일 클러스터 환경일 때의 운영 난이도와 멀티 클러스터일 때의 운영 난이도 또한 천지차이인 것 같구요..ㅜ)</p>
<p>또, 이것저것 만들고 놀 수 있는 홈랩 클러스터가 있음에도 개인용 홈랩의 한계도 명확한 것 같습니다. 
그렇기에 (<del>뽑아만 주신다면</del>) 실제 현업에서 돌아가는 쿠버네티스 / 인프라 환경을 경험해보고 싶다는 생각이 많이 드는 것 같습니다.</p>
<h3 id="🙋🏻♂️-그래도-cka-ckad-등-쿠버네티스-자격증-추천하나요">🙋🏻‍♂️ 그래도 CKA, CKAD 등 쿠버네티스 자격증 추천하나요?</h3>
<p>-&gt; 넵넵넵
저처럼 _<strong>쿠버네티스를 잘 모르시거나 학습을 막 시작하시는 분들</strong>_은 쿠버네티스라는 기술의 전반적인 흐름을 공부하기에 자격증이 많이 도움이 된다고 생각합니다. 
다만,, 현재 높이 치솟고 있는 환율을 고려했을 때 <strong>[자격증 취득이 아닌, 자격증 취득을 목표로 학습&#39;만&#39; 하셔도 충분히 도움이 될 것 같습니다.]</strong> (<del>자격증 취득은 취업 후 회사 돈으로 하는게 가장 좋은 것 같습니다..^^</del>)</p>
<p>이제 목표까지 두 개 남았습니다..! (CKS, KCSA ..) 2학기 개강 전에 두 개 모두 취득하는 것을 목표로 다시 달려보겠습니다...!!</p>
<hr>
<h2 id="6-kubernetes-sig-docs-approver-ko">[6] Kubernetes SIG-Docs Approver (ko)</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/68ed0dea-a50c-4095-bd91-73fa1addd430/image.png" alt=""></p>
<p>작년 <em><strong>오픈소스 컨트리뷰션 아카데미 (OSSCA) Kubernetes/OpenStack L10N, I18N</strong></em> 를 시작으로 쿠버네티스 한글화팀에서 한글화 기여를 꾸준히 하고 있습니다. 한글화를 하며 저도 쿠버네티스에 대해 더 자세히 알아보게 되고, 오픈소스 생태계에 적극적으로 기여하는 경험을 쌓는 등 많은 경험을 쌓고 있는 활동입니다.</p>
<p>지속적인 기여 활동과 함께 하는 멘토, 멘티님들의 도움으로 올해 상반기에 쿠버네티스 Docs 공식 승인자 (SIG-Docs Approver)가 되었습니다.🎉
부족한 점도 많고, 바쁜 시기에는 꾸준한 기여를 못하기도 하는데,, 올해 OSSCA에 한글화팀이 나오기도 했으니 더 열심히 기여 활동을 이어 나가겠습니다!!</p>
<hr>
<h2 id="🤔-회고-및-요즘-드는-생각">🤔 회고 및 요즘 드는 생각</h2>
<p><strong>[2026년 1월 1일 ~ 2026년 6월 30일]</strong> 기간 동안 다양한 곳에서 정말 많은 경험을 했습니다.</p>
<ul>
<li>홈랩 프로젝트 (2025.12 ~ 진행 중)</li>
<li>부트캠프: 현대 오토에버 모빌리티 SW스쿨 - 클라우드 (2025.12 ~ 2026.06)</li>
<li>초록 밋업 발표 (2026.01)</li>
<li>그리디 운영진 (~ 2026.03)</li>
<li>4학년 1학기 (2026.03 ~ 2026.06)</li>
<li>외부 커뮤니티 활동<ul>
<li>OpenInfra Korea User Group Organizer (2025.09 ~ 진행 중)</li>
<li>PyCon Korea Organizer (2026.03 ~ 진행 중)</li>
</ul>
</li>
<li>쿠버네티스 자격증 취득: CKA, CKAD, KCNA</li>
<li>Kubernetes 한글화 기여 및 SIG-Docs Approver 승급</li>
</ul>
<blockquote>
<p>적어놓고 보니 꽤 많은 일들을 한 것 같습니다. </p>
</blockquote>
<p>음...작년부터 많은 것을 하고자 욕심을 내기도 했고 그만큼 많이 재밌었습니다. 바쁜 일들이 몰리는 시기를 예상하면서도 <em><strong>&quot;덜 쉬고, 덜 놀고, 시분할 하듯이(ㅋㅋㅋ) 다양한 일들을 하나씩 차근차근 하면 되지 않을까?&quot;</strong></em> 라는 생각으로 1년 넘게 버티고 많이 달려온 것 같습니다. </p>
<p>중간중간 힘에 부치는 날도 많았고, <em><strong>&quot;나 지금 뭐하는거지?&quot;</strong></em> 라는 다소 회의적인 물음을 스스로에게 던진 적도 많았던 것 같습니다. </p>
<p>최근 부트캠프가 끝나가며, <em><strong>&quot;이제 막학기이고 진짜 취준을 해야겠구나&quot;</strong></em> 라는 생각이 드니 무수히 많이 쌓여있는 할 일들이 스트레스로 다가오기도 하고 _<strong>많은 일들을 하는 것에 대한 이유를 스스로 잃어버린 것 같다</strong>_는 생각이 들었던 것 같습니다.</p>
<p>아무래도 취준의 압박이 오기 시작하니 내면의 여유가 많이 줄어들고 있는 것 같습니다..
그리고 결정적으로 심적 여유가 없어지며 스트레스를 너무 많이 받고 있습니다. (건강이 무조건 최우선입니다 여러분)</p>
<p>2주 정도 이에 대해서 계속 고민해보고 주변에 조언도 구해보고 글도 많이 써보면서 생각 정리를 해보았고, 끝내 내린 결론은..!</p>
<blockquote>
<p><em><strong>&quot;건강과 해야할 일을 우선적으로 챙기자!&quot;</strong></em></p>
</blockquote>
<p>입니다.ㅎㅎ</p>
<p>취준 시기를 맞이하며 어쩔 수 없이 조바심도 생기고 압박감도 많이 느끼는 요즘.. <code>좋아하는 것</code> , <code>하고 싶은 것</code> , <code>해야하는 것</code> , <code>건강</code> 등 모든 것을 챙기기는 참 어려운 것 같습니다.
(<del>제가 조금 더 강인한 사람이었다면 모든 것을 다 챙길 수 있었을까요??ㅎㅎ.. 😂😂</del>)</p>
<p>어차피 해야하는 취준, 이번 하반기에 깔끔하게 끝내고 다시 좋아하는 것과 하고 싶은 것을 마음껏 할 수 있는 시기가 왔으면 좋겠습니다. 🙏🏻🙏🏻</p>
<hr>
<h2 id="하반기-계획">하반기 계획!</h2>
<p>파워 J이기 때문에 하반기 계획 또한 미리 세워야 속이 편합니다.ㅋㅋㅋ
언제나 그랬듯이 계획은 계속 수정하기 위해서 세우기 때문에 이번에도 큰 틀에서만 계획을 세워봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/1f018bfa-103a-4fe2-b833-b104e8f2b788/image.png" alt=""></p>
<blockquote>
<p>계획을 굉장히 대충 세운 것처럼 보이지만, 정말 필요한 것들만 함축해서 적어놓은 마스터 플랜입니다.</p>
</blockquote>
<p>다른건 몰라도 <em><strong>&quot;멘탈 케어 - 의도적 휴식&quot;</strong></em> &lt;-- 이건 정말 엄격하게 지키고자 합니다.🤗
(<del>의도적으로 쉬겠다는 계획을 안 세우면 계속 일만 하더라구요..</del>)</p>
<hr>
<p>이번 하반기에는 <strong>반려 서버 키우기</strong>와 함께 <strong>취준 과정</strong>을 잘 정리해보고자 합니다. 많이 응원해주시면 감사하겠습니다. 모두들 파이팅입니다!!!🔥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (15) Retrospect (Sprint 1)]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-%ED%9A%8C%EA%B3%A01</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-%ED%9A%8C%EA%B3%A01</guid>
            <pubDate>Tue, 17 Mar 2026 07:16:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://velog.io/@pizza_loves_me/series/Homelab">Multi-Tenant K8s Cluster on ARM64</a> 의 첫 스프린트(?)였던 <code>프로젝트 구축 및 교내 개발 동아리(GREEDY) 프로젝트 지원</code>이 지난주 토요일에 종료되었습니다. 첫 스프린트 기간을 회고해보겠습니다.</p>
</blockquote>
<h2 id="프로젝트는-왜-시작했는가">프로젝트는 왜 시작했는가?</h2>
<p>작년 여름부터 인프라 분야에 많은 관심을 갖게 되었습니다. 이론 공부를 할수록 직접 만들어보고 싶다는 생각이 강해졌던 것 같습니다. 특히 CSP 쪽 직군에 관심이 많이 생겼고, &quot;나도 클라우드 서비스를 직접 구축하고 운영해보고 싶다&quot; 라는 생각이 많이 들었습니다. <del>미친듯이 치솟는 RAM 가격과 가난한 대학생이었기에</del> 여름부터 12월까지 알바를 하며 돈을 모았습니다.(홈랩 구축 비용, Kubestronaut(졸업 전 버킷리스트) 번들 비용, 맥북 교체 비용...)</p>
<p><a href="https://velog.io/@pizza_loves_me/series/Project-Build-Kubernetes-Cluster-Homelab-on-Bare-metal-Raspberry-Pi-3">📌 홈랩 구축기 - 블로그</a>
.
.
.</p>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/1fd4ba81-3e90-46f0-9739-2dd29a1925d9/image.png" alt=""></p>
<p>매우 많은 시행착오들을 겪으며 살아숨쉬는 제 <code>반려 홈랩</code> 이 탄생했습니다. 🥹</p>
<p>홈랩에서 쿠버네티스 설치 및 클러스터 구축을 완료하고 <em><strong>&quot;다음은 뭘 할까?&quot;</strong></em> 라는 고민을 했습니다. CSP 와 같이 사용자에게 서버를 제공하고 관리하는 역할을 해보고 싶었기에 주위에 서버가 필요한 개발자를 찾아보았습니다. </p>
<p>때마침 운영진(메인테이너)으로 활동 중인 교내 개발 동아리 그리디(GREEDY)에서 3기 멤버들이 곧 프로젝트 시작을 할 때였습니다. 저 또한 그리디에서 BE, FE 를 배웠고, 프로젝트 과정 중 느꼈던 아쉬웠던 점이 있었는지 생각해보았습니다. 가장 생각이 많이 났던 부분은 <code>학생이라 비용 부담 때문에 프리티어 계정만 사용이 가능하고, 이마저도 단일 서버로만 운영을 해야한다</code> 는 점이었습니다. </p>
<p>단일 서버로 프로젝트를 진행했기 때문에 미션 요구사항 중 <code>무중단 배포</code> 와 <code>모니터링 대시보드 구축</code> 모두 아쉬움이 많이 남았던 기억이 있습니다.(ec2  t2.micro 를 반으로 쪼개어 블루-그린 배포를 진행하고, 하나의 인스턴스에 모니터링 서버와 백엔드 서버를 함께 띄우는 구조적 모순까지,,,ㅜㅜ)</p>
<p>멤버들에게 평생 서버를 제공해줄 수는 없겠지만,, 적어도 동아리 프로젝트 기간(약 2달) 동안은 멤버들이 다중 서버 운영도 해보고, &#39;진짜&#39; 무중단 배포도 경험해보고, 아키텍처 고민도 해보는 시간이 되었으면 했습니다.</p>
<p>최종 데모데이 요구사항(무중단 배포 적용, 모니터링 대시보드 구축 등) 공개일이 많이 남지 않아서 빠르게 멀티-테넌트 프로젝트를 진행했습니다.</p>
<p><a href="https://velog.io/@pizza_loves_me/series/Homelab">📌 멀티 테넌트 제공 프로젝트 구축기 - 블로그</a>
<del>마감기간 직전에는 밤을 샌 날이 참 많았던 것 같습니다..허허</del>
다행히 (가벼운)테스트까지 끝마치고 멤버들에게 테넌트 서버 제공이 가능했고, 두 팀에게 각각 <code>테넌트 서버 5대씩 총 10대</code> 를 제공해주었습니다.</p>
<h2 id="멀티-테넌트-프로젝트-운영-과정">멀티-테넌트 프로젝트 &#39;운영&#39; 과정</h2>
<h3 id="1-테넌트-서버-1차-안내">1. 테넌트 서버 1차 안내</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/fc87511c-466d-428e-a30a-23b6d5216f7a/image.png" alt=""></p>
<blockquote>
<ul>
<li>동아리의 주된 소통 채널이 <code>디스코드</code> 이기 때문에 테넌트 서버 안내사항을 전달했습니다.</li>
</ul>
</blockquote>
<ul>
<li>서버 리소스 확보를 위해 Alpine 버전의 운영체제를 사용했는데, 많은 멤버들이 <code>apk</code> 사용이 처음일 것 같아서 추가 안내 또한 진행했습니다.</li>
</ul>
<h3 id="2-사용자-피드백-정리-및-대응-관리">2. 사용자 피드백 정리 및 대응 관리</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/4782ee5f-2b1c-4943-a960-f225ff7bb340/image.png" alt=""></p>
<blockquote>
<ul>
<li>제공한 테넌트 서버는 총 10대, 이를 사용하는 백엔드 멤버 총 6명.</li>
</ul>
</blockquote>
<ul>
<li>많은 인원이 적지 않은 수의 테넌트 서버를 사용하기 때문에 기술적 문제에 대한 피드백 또한 많을 것 같았습니다. 이를 관리하고 전체 사용자들에게 업데이트 안내를 하고자 정리하고 관리했습니다.</li>
</ul>
<h3 id="3-피드백---문제-파악-및-해결---업데이트-안내-및-패치-노트-작성">3. 피드백 -&gt; 문제 파악 및 해결 -&gt; 업데이트 안내 및 패치 노트 작성</h3>
<blockquote>
<h3 id="case-1">Case 1)</h3>
<p>⚠️ 사용자 피드백 중 테넌트 서버 재시작 시 기존 파일이 저장되지 않는다는 피드백이 있었습니다.</p>
</blockquote>
<h3 id="1️⃣-문제-파악-및-해결">1️⃣ 문제 파악 및 해결</h3>
<p><a href="https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-10-Refactor-Building-a-Truly-Stateful-Storage">📌 &#39;파드 재시작 시 데이터 휘발&#39; 에 대한 정리 - 블로그</a></p>
<blockquote>
</blockquote>
<h3 id="2️⃣-업데이트-안내">2️⃣ 업데이트 안내</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/cc5503a5-7881-4e9d-a57d-cbfaa887be1c/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="3️⃣-패치-노트-작성">3️⃣ 패치 노트 작성</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/017a3545-7c82-433b-a4b0-99379c74964a/image.png" alt=""></p>
<blockquote>
</blockquote>
<p><a href="https://github.com/developowl/multi-tenant-server/blob/main/Patch-Note/26.03.05_%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%98%81%EC%86%8D%EC%84%B1.md">📌 패치 노트 - Github</a></p>
<blockquote>
<h3 id="case-2">Case 2)</h3>
<p>⚠️ 또 다른 피드백 중 사전에 제공해준 포트를 사용하여 웹 접속이 안 된다는 피드백이 있었습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/cdec60cb-e481-4526-88ac-fb0184d33ac3/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="1️⃣-문제-파악-및-해결-1">1️⃣ 문제 파악 및 해결</h3>
<p><a href="https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-13-Refactor-Add-web-external-port">📌 &#39;외부에서 브라우저 접속 시 응답 없음&#39; 에 대한 정리 - 블로그</a></p>
<blockquote>
</blockquote>
<h3 id="2️⃣-업데이트-안내-1">2️⃣ 업데이트 안내</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/3ab3ba1e-f162-4829-9a01-238710f22b25/image.png" alt=""></p>
<blockquote>
</blockquote>
<h3 id="3️⃣-패치-노트-작성-1">3️⃣ 패치 노트 작성</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/dd32dd53-0298-488c-96ed-91efebf04ce3/image.png" alt=""></p>
<blockquote>
</blockquote>
<p><a href="https://github.com/developowl/multi-tenant-server/blob/main/Patch-Note/26.03.10_%EC%9B%B9_%EC%A0%84%EC%9A%A9_%ED%8F%AC%ED%8A%B8_%EC%B6%94%EA%B0%80.md">📌 패치 노트 - Github</a></p>
<h2 id="🗣️-멀티-테넌트-서버-첫-사용자들의-피드백-구글폼">🗣️ 멀티 테넌트 서버 &#39;첫&#39; 사용자들의 피드백 (구글폼)</h2>
<blockquote>
<p>멀티 테넌트 서버를 제공한 당일부터 예상하지 못했던 문제점과 피드백이 많은 도움이 되었습니다. 또, 이번 첫 사용자들의 피드백을 참고하여 이후 <code>고도화 작업</code> 및 <code>오프라인 인프라 핸즈온 세미나</code> 를 진행하고자 했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/fdbeaffc-42ce-4ec8-9c68-1a27cf5867c9/image.png" alt=""><img src="https://velog.velcdn.com/images/pizza_loves_me/post/c333c11f-e839-47ed-828a-38d14586d2f1/image.png" alt=""></p>
<blockquote>
<p>감사하게도 여러 부원들이 피드백을 남겨주었고, 다음 고도화 방향과 세미나 주제를 선정하는데에 큰 도움이 되었습니다. 🙏🏻</p>
</blockquote>
<h2 id="🪧-next-step">🪧 Next Step!</h2>
<blockquote>
<p>일단 해보고 싶은 작업이 너무 많습니다ㅎㅎ. 그중에서 사용자 피드백을 우선 반영하고 고도화를 진행해볼까 합니다. </p>
</blockquote>
<h3 id="📝-list">📝 List</h3>
<ul>
<li>테넌트 생성 및 접속 시 <code>Public Key</code> 만을 사용하여 접속하도록 리팩토링 (비밀번호 입력이 번거롭다는 피드백 &amp; AWS - ec2의 경우 인스턴스 생성 시 퍼블릭 키 제공 참고)</li>
<li><code>스토리지 전용 테넌트 모델</code> 생성 (현재 제공 중인 tenant-basic, standard, pro 모두 서비스 특화 모델)</li>
<li>테넌트 서버 생성 및 관리 방식 고도화 및 자동화</li>
<li>⭐️ <code>모니터링 시스템 고도화</code> (현재는 <code>Node-exporter + Prometheus + Grafana</code> 를 사용하고 있는데, 파드 레벨의 매트릭 도입 및 다양한 패턴 적용해보기)</li>
<li><code>GitOps</code> 도입 (GitHub + Helm + ArgoCD)</li>
<li>다른 컨테이너/가상화 솔루션 도입(<code>Kata Container, KubeVirt</code> 등)</li>
<li>로드밸런서 구축 및 도입 (남는 라즈베리파이 사용)</li>
<li>실제 입주민 모집 (동아리 및 주변 지인들의 프로젝트를 홈랩에 입주)</li>
</ul>
<h3 id="마무리">마무리</h3>
<blockquote>
<p>처음 홈랩 클러스터 부품들(<del>약 80만원,,,,,😭</del>)을 주문할 때가 12월 중순이었습니다. 베어메탈 단계부터 직접 구축하고, 테넌트 서버 프로젝트 구축 &amp; 운영을 하다보니 정말 많은 것들을 배웠고, 값진 경험을 한 것 같습니다. 실제 클라우드 기업이 구현하고 운영하는 것과는 많은 차이가 있겠지만...^^ 비슷하게나마 따라서 만들고 운영 해보길 잘한 것 같습니다. </p>
</blockquote>
<p>현재는 1차 스프린트였던 그리디 3기 프로젝트가 종료되었지만, 바로 2차 스프린트 계획을 잡을까 합니다. 그리디 부원들이 만든 꽤나 성숙한 프로젝트들이 많기에 각 프로젝트팀에게 입주 제안을 하려고 합니다. 1차 스프린트 때는 <code>미션(요구사항)</code> 이라는 강제성(?)이 있었지만, 이번에는 <code>사용자(프로젝트 팀) - 클라우드 기업(나)</code> 의 관계로 견고한 제품을 팔아본다는 마음을 가져볼까 합니다.ㅎㅎ</p>
<blockquote>
</blockquote>
<p>감사합니다. :D</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (14) Network Flow]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-14-Network-Flow</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-14-Network-Flow</guid>
            <pubDate>Fri, 13 Mar 2026 17:51:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>현재까지 구축한 멀티-테넌트 프로젝트의 네트워크 흐름에 대해 정리해보고 도식화 해보았습니다.</p>
</blockquote>
<h1 id="아키텍처">아키텍처</h1>
<blockquote>
<p>현재 구축한 멀티-테넌트 프로젝트의 아키텍처 입니다. Users(클라이언트)가 테넌트 서버로 네트워크 통신을 보냈을 때 네트워크의 흐름을 패킷의 목적지 정보와 함께 정리해보겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/2a1a2b09-922f-4065-936f-9c873a400120/image.png" alt=""></p>
<h1 id="전체-네트워크-흐름도">전체 네트워크 흐름도</h1>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/bc8d48ef-e785-465b-8374-48a620438748/image.png" alt=""></p>
<h2 id="순차적-설명">순차적 설명</h2>
<ol>
<li><code>&lt;Public IP&gt;:&lt;외부 포트&gt;</code> 주소를 가지고 가정용 모뎀에 접근</li>
<li>가정용 모뎀은 이를 <code>&lt;홈랩 내부 IP&gt;:&lt;로컬포트&gt;</code> 로 포트포워딩</li>
<li>직후 패킷이 들고 있는 목적지 정보는 <code>&lt;홈랩 내부 IP&gt;:&lt;로컬 포트&gt;</code></li>
<li>홈랩(마스터 노드) 진입 즉시 iptables 규칙에 따라 <code>&lt;홈랩 내부 IP&gt;:&lt;nodePort&gt;</code> 에서 <code>&lt;파드 고유 IP&gt;:&lt;targetPort&gt;</code> 로 변환</li>
<li>직후 패킷이 들고 있는 목적지 정보는 <code>&lt;파드 고유 IP&gt;:&lt;targetPort&gt;</code></li>
<li>정해진 길(iptables 명시)을 따라 워커노드에 도착<strong>+</strong>) <strong>CNI 라우팅(라우팅 테이블/터널/encap) + conntrack + iptables 체인</strong>이 협업</li>
<li>워커 노드 진입 이후 패킷이 들고 있는 목적지 정보는 <code>&lt;파드 고유 IP&gt;:&lt;targetPort&gt;</code></li>
<li>워커 노드는 추가적인 서비스 선택/재-DNAT 없이 커널 경로(CNI/conntrack)를 거쳐 Pod로 직접 전달</li>
</ol>
<h3 id="2번-항목-가정용-모뎀의-포트포워딩">2번 항목: 가정용 모뎀의 포트포워딩</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/27bd2cce-7aa2-4d29-aa42-7d993b316f90/image.png" alt=""></p>
<ul>
<li>포트포워딩 설정을 하기 위해 접속한 <code>네트워크 관리자 페이지</code> 내의 <code>포트포워딩 항목</code>입니다.</li>
</ul>
<h3 id="마무리">마무리</h3>
<p>지난번 포스팅에서는 아키텍처만 보았을 때 네트워크 흐름이 간단할 줄 알았습니다만,, 실제로 패킷이 어떠한 목적지 정보를 가지고 테넌트 서버(파드)로 도달하는지를 고민해보니 헷갈리는 점이 너무나 많았던 것 같습니다. 이론으로 네트워크의 흐름을 알게 되었으니, 조만간 아래의 명령어로 iptables 를 분석해보는 시간을 갖겠습니다.</p>
<pre><code class="language-bash">sudo iptables -t nat -L -n | &lt;포트 번호&gt;</code></pre>
<ul>
<li>핵심 정보<ul>
<li><code>DNAT (Destination NAT) 기록</code><ul>
<li>패킷이 노드에 도착했을 때 원래 목적지(Node IP: 30100)를 진짜 목적지(Pod IP: 40100)로 바꾸는 명령줄을 확인</li>
</ul>
</li>
<li><code>KUBE-SERVICES &amp; KUBE-NODEPORTS 체인</code><ul>
<li>쿠버네티스가 관리하는 특별한 규칙들<ul>
<li><code>KUBE-SERVICES</code>: Cluster IP 로 들어오는 내부 트래픽을 처리하는 규칙들</li>
<li><code>KUBE-NODEPORTS</code>: 외부에서 <code>nodePort</code> 로 들어오는 트래픽을 낚아채는 규칙들</li>
</ul>
</li>
</ul>
</li>
<li><code>Load Balancing 로직</code><ul>
<li>만약 파드가 여러 개라면, <code>iptables</code> 는 패킷을 어느 파드로 보낼지 결정함</li>
<li><code>statistic mode random probability 0.50000</code> 같은 문구가 있다면, “패킷을 50% 확률로 이 파드에, 나머지 50%는 저 파드에 보내라” 라는 설정을 의미함</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (13) [Refactor] Add web external port]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-13-Refactor-Add-web-external-port</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-13-Refactor-Add-web-external-port</guid>
            <pubDate>Wed, 11 Mar 2026 02:59:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>현재 테넌트 서버에 접속 가능한 포트는 SSH 접속을 위한 포트뿐입니다. 테넌트 서버에 백엔드 서비스 혹은 모니터링 대시보드 등을 띄웠을 경우 외부 접속 포트가 없고, SSH 접속을 위한 포트로 접속 시 브라우저(HTTP)가 기대하는 응답 웹 프로토콜과 실제 응답으로 나가는 SSH 프로토콜이 충돌하는 상황 발생.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/22905f68-c7a2-4b3e-b1f9-08cd0f4ef56d/image.png" alt=""></p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<ul>
<li>웹 서비스용 외부 개방 포트 부재</li>
<li>기존에 테넌트 서버당 하나씩 개방한 포트는 SSH 전용 포트이기에 요청-응답 프로토콜 간 충돌 발생</li>
</ul>
<h3 id="현재-테넌트-서버의-nodeport-관련-설정">현재 테넌트 서버의 NodePort 관련 설정</h3>
<pre><code class="language-yaml">  ports:
  - port: 2222           # 서비스 자체의 포트
    targetPort: 2222     # 파드 내부 컨테이너 포트
    nodePort: $NODEPORT  # 외부에서 노드로 들어오는 포트</code></pre>
<blockquote>
</blockquote>
<h2 id="📌-port-targetport-nodeport-란">📌 port, targetPort, nodePort 란?</h2>
<blockquote>
</blockquote>
<h3 id="port---서비스-자체의-포트">port - 서비스 자체의 포트</h3>
<blockquote>
</blockquote>
<ul>
<li>역할: 쿠버네티스 클러스터 내부에서 서비스 객체가 노출하는 포트</li>
<li>설명: 클러스터 안의 다른 파드들이 이 서비스를 호출할 때 사용하는 포트. 즉, 서비스의 DNS 이름이나 ClusterIP를 통해 통신할 때 기준이 됨<blockquote>
</blockquote>
<h3 id="targetport---파드-내부-컨테이너-포트">targetPort - 파드 내부 컨테이너 포트</h3>
<blockquote>
</blockquote>
</li>
<li>역할: 트래픽이 최종적으로 도달하는 파드(애플리케이션) 내부의 포트</li>
<li>설명: 실제 애플리케이션 코드가 수신 대기(Listen)하고 있는 포트 번호와 일치해야 함. <code>selector</code> 를 통해 연결된 파드로 트래픽을 전달하는 최종 목적지<blockquote>
</blockquote>
<h3 id="nodeport---외부에서-노드로-들어오는-포트">nodePort - 외부에서 노드로 들어오는 포트</h3>
<blockquote>
</blockquote>
</li>
<li>역할: 클러스터 외부에서 각 워커 노드의 IP를 통해 접근할 때 사용하는 포트</li>
<li>설명: 클러스터 외부의 사용자가 <code>노드IP:nodePort</code> 로 접속하면 트래픽이 쿠버네티스 내부로 유입</li>
<li>범위: 기본적으로 <code>30000~32767</code> 사이의 포트번호 사용 (지정하지 않으면 범위 내에서 랜덤하게 할당됨)</li>
</ul>
<h2 id="해결-방안">해결 방안</h2>
<ul>
<li>기존에 미리 선점(포트포워딩)해두었던 여분의 포트를 사용하여 각 테넌트 서버의 웹 서비스용 포트를 추가 지급</li>
<li>환경변수를 사용하여 <code>SSH_NODEPORT</code> 와 <code>WEB_EXTERNAL_PORT</code> 를 관리</li>
</ul>
<pre><code class="language-yaml">spec:
  type: NodePort
  selector:
    app: &lt;app 이름&gt;
  ports:
  # SSH 접속 관련
  - name: ssh-access
    port: 2222
    targetPort: 2222
    nodePort: $SSH_NODEPORT # SSH 접속용 NodePort
    # 웹 접속 관련
  - name: web
    port: $WEB_EXTERNAL_PORT
    targetPort: $WEB_EXTERNAL_PORT
    nodePort: $WEB_NODEPORT</code></pre>
<h3 id="테스트">테스트</h3>
<blockquote>
<p>같은 스펙을 가진 테스트용 테넌트 서버에 간단한 nc 명령어로 접속 테스트</p>
</blockquote>
<pre><code class="language-bash">echo -e &quot;Welcome to Test Tenant Server!&quot; | nc -l -p &lt;테넌트 서버의 웹 서비스용 포트&gt;</code></pre>
<ul>
<li><code>http://&lt;Public IP&gt;:&lt;웹 서비스용 포트&gt;</code> 로 접속</li>
<li>결과 1 <img src="https://velog.velcdn.com/images/pizza_loves_me/post/87609180-aeef-443b-bf9e-4b4c6a965b96/image.png" alt="">
(웹 서비스 전용 포트로 접속이 가능!)</li>
</ul>
<ul>
<li>결과 2 (실제 테넌트 사용자)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/0464169a-8c51-4b7f-bf3b-93361dd9729e/image.png" alt=""></p>
<ul>
<li>문제 상황을 제시해준 테넌트 사용자의 초기 목적대로 해당 팀의 Grafana 대시보드가 성공적으로 띄워졌음을 확인할 수 있습니다.</li>
</ul>
<h3 id="마무리">마무리</h3>
<p>하나의 테넌트 서버에 용도가 다른 두 포트를 개방하며, 외부에서 트래픽이 들어왔을 때 내부에서 그 흐름을 파악할 수 있게 된 리팩토링이었던 것 같습니다. 네트워크 흐름에 대한 정리는 다음 포스팅에서 간단한 아키텍처와 함께 정리해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (12) Using Master Node Resource]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-12-Using-Master-Node-Resource</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-12-Using-Master-Node-Resource</guid>
            <pubDate>Mon, 09 Mar 2026 08:58:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>마스터 노드의 리소스 구성은 <code>CPU - 4.0 Core</code> / <code>RAM - 8GB</code> / <code>Storage - 256GB</code> 입니다. 여기서 실제 마스터 노드 운영에 필요한 리소스를 제외하면 놀고 있는 리소스가 상당할 것으로 예상이 됩니다. 그래서 더 많은 리소스를 멀티 테넌트에 사용하고자 합니다.</p>
</blockquote>
<h2 id="1-마스터-노드-작동에-필요한-리소스-견적">1. 마스터 노드 작동에 필요한 리소스 견적</h2>
<ul>
<li><code>CPU: 1.0 Core</code><ul>
<li>etcd, kube-apiserver 는 테넌트 파드가 많아질수록 데이터 통신량과 연산량이 급증.</li>
<li>물리적인 1코어를 시스템 전용으로 박제해둠으로써, 테넌트 파드가 CPU를 100% 점유하려는 폭주 상황에서도 제어 신호가 밀리지 않고 클러스터 업무 처리가 가능</li>
</ul>
</li>
<li><code>Memory: 2.0Gi ~ 2.5Gi</code><ul>
<li>etcd 메모리 캐싱과 클러스터 상태 감시 프로세스가 차지하는 비중</li>
<li>메모리 부족 시 지연이나 노드 이탈 현상 발생</li>
</ul>
</li>
<li><code>Ephemeral Storage: 5Gi 이상</code><ul>
<li>로그와 컨테이너 이미지 레이어들이 쌓이는 공간</li>
<li>여기가 꽉 차면 마스터 노드 자체가 DiskPressure 상태에 빠져 멈춤</li>
</ul>
</li>
</ul>
<blockquote>
<p>일반적으로 마스터 노드는 최소 사양 2코어 이상을 권장하지만,, 리소스 제약(홈랩)을 고려하여 <strong>1.0 Core</strong> 를 물리적으로 <strong>예약(Reserved)</strong> 하는 방식을 채택했습니다!</p>
</blockquote>
<h2 id="2-마스터-노드에-걸린-제한taint-해제하기">2. 마스터 노드에 걸린 제한(Taint) 해제하기</h2>
<ul>
<li>현재 마스터 노드에는 <code>Taint</code>가 붙어 있기 때문에 일반 파드가 배포되지 않는다.</li>
</ul>
<pre><code class="language-bash"># 마스터 노드의 NoSchedule 설정 제거 (명령어 끝의 &#39;-&#39;가 제거를 의미합니다)
kubectl taint nodes master node-role.kubernetes.io/control-plane:NoSchedule-</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/f043cee4-bad1-464e-aa5a-2fdd23d60fbb/image.png" alt=""></p>
<blockquote>
<p>⚠️ 주의사항: <code>NoSchedule-</code> 을 사용하여 마스터 노드를 개방했습니다. 나중에 테넌트 파드가 배포될 때, 테넌트 파드가 CPU 스케줄링을 두고 경쟁을 하게 되고 이때 마스터 노드의 작업에 영향을 끼칠 수도 있습니다.
이에 대한 명시적 조치로 <code>PriorityClass</code> 와 <code>NodeAffinity</code> 라는 설정이 있습니다.</p>
</blockquote>
<blockquote>
</blockquote>
<h2 id="📌-priorityclass--nodeaffinity">📌 PriorityClass &amp; NodeAffinity</h2>
<blockquote>
</blockquote>
<h3 id="priorityclass--누구를-먼저-내보낼-것인가"><code>PriorityClass</code> == “누구를 먼저 내보낼 것인가?”</h3>
<blockquote>
</blockquote>
<p>쿠버네티스 노드에 자원이 부족해지면 커널은 어떤 파드를 죽여서 노드를 살릴 것인지 결정을 합니다. 이때 <code>PriorityClass</code> 는 파드에게 붙여주는 계급장 역할을 합니다.</p>
<blockquote>
</blockquote>
<ul>
<li>파드마다 숫자(value)를 부여하고, 숫자가 높은 파드일수록 제거 후순위가 됩니다.</li>
<li>마스터 노드에서의 활용<ul>
<li>시스템 파드 (apiserver, etcd 등): 매우 높은 점수 (기본값으로 이미 높음)</li>
<li>테넌트 파드: 낮은 점수</li>
</ul>
</li>
<li>기대 효과<ul>
<li>마스터 노드의 메모리가 꽉 차면, 쿠버네티스는 점수가 낮은 테넌트 파드를 먼저 kill 시켜서 마스터 노드의 생존을 최우선으로 보장한다<blockquote>
</blockquote>
<h3 id="nodeaffinity--어디에-위치하게-할-것인가"><code>NodeAffinity</code> == “어디에 위치하게 할 것인가?”</h3>
<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>파드가 배포될 때 라벨에 따라 선호/비선호를 지정하는 기능</li>
<li>마스터 노드에서의 활용<ul>
<li>테넌트 파드들에게 약한 거부감(Preferred Scheduling)을 부여</li>
<li>워커 노드에 공간이 있다면 워커 노드로 먼저 가고, 워커 노드가 꽉 찼을 때만 마스터 노드의 남는 자리를 쓰도록 유도</li>
</ul>
</li>
</ul>
<h3 id="priorityclass-리스트-확인하기">PriorityClass 리스트 확인하기</h3>
<pre><code class="language-bash">kubectl get priorityclasses

or

kubectl get pc</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/5e6fcd83-c413-4c65-876f-f810d2294ed2/image.png" alt=""></p>
<ul>
<li>현재 쿠버네티스 시스템의 <code>priorityclass</code> 만 값이 20억점대로 지정이 되어 있음을 확인할 수 있습니다.</li>
<li>테넌트 파드들의 <code>priorityclass</code> 를 따로 지정하지 않았기 때문에 0점(Default)으로 작동 중입니다.</li>
<li>→ 마스터 노드의 생존은 어느 정도 보장이 된 상태!</li>
</ul>
<h3 id="각-파드들의-pc-값을-한-번에-조회하기">각 파드들의 PC 값을 한 번에 조회하기</h3>
<pre><code class="language-bash">kubectl get pods -A -o custom-columns=&quot;NAMESPACE:.metadata.namespace,NAME:.metadata.name,PRIORITY-CLASS:.spec.priorityClassName,VALUE:.spec.priority&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/f23038fb-591b-4347-b8a7-18684a080fcb/image.png" alt=""></p>
<ul>
<li>시스템 설정 관련 파드들은 모두 20억대의 값(value)을 갖고 있고, 일반 파드들은 기본값인 0을 부여받았음을 확인할 수 있습니다.</li>
</ul>
<h2 id="3-리소스-예약-설정마스터-노드-보호-설정">3. 리소스 예약 설정(마스터 노드 보호 설정)</h2>
<ul>
<li>테넌트 파드가 마스터 노드의 자원을 다 사용하지 못하도록 <code>Kubelet</code> 설정 파일을 수정하여 관리자 전용 자원을 강제로 묶음</li>
</ul>
<h3 id="3-1-설정-파일-열기">3-1. 설정 파일 열기</h3>
<pre><code class="language-bash">sudo nano /var/lib/kubelet/config.yaml</code></pre>
<h3 id="3-2-리소스-예약-값-추가">3-2. 리소스 예약 값 추가</h3>
<ul>
<li><code>kubeReserved</code> (쿠버네티스 관리용) / <code>systemReserved</code> (OS 구동용)</li>
<li><code>kubelet/config.yaml</code> 하단에 설정 추가</li>
</ul>
<pre><code class="language-bash"># 마스터 노드 보호를 위한 리소스 예약 설정
systemReserved:
  cpu: &quot;300m&quot;
  memory: &quot;500Mi&quot;
kubeReserved:
  cpu: &quot;700m&quot;      # 시스템과 합쳐서 총 1.0 Core 예약
  memory: &quot;2000Mi&quot;  # 시스템과 합쳐서 총 2.5Gi 예약
enforceNodeAllocatable: [&quot;pods&quot;]
evictionHard:
  memory.available: &quot;500Mi&quot; # 여유 메모리 500Mi 미만 시 테넌트 파드 자동 퇴거
  nodefs.available: &quot;10%&quot;   # 디스크 여유 10% 미만 시 경고 및 제한</code></pre>
<ul>
<li><code>systemReserved</code><ul>
<li>리눅스 운영체제 자체가 동작할 공간을 미리 떼어두는 설정</li>
</ul>
</li>
<li><code>kubeReserved</code><ul>
<li>쿠버네티스 관리 객체(kubelet, container runtime 등)를 위한 전용 자원 설정</li>
</ul>
</li>
<li><code>enforceNodeAllocatable</code><ul>
<li>노드에서 파드에게 실제로 나누어 줄 수 있는 자원(Allocatable)을 계산할 때, 위에서 예약한 자원들을 강제로 제외하겠다는 선언</li>
<li>보통 <code>[”pods”]</code> 로 설정하며, 스케줄러는 이 계산된 값을 보고 파드를 배치함</li>
</ul>
</li>
<li><code>evictionHard</code><ul>
<li>노드의 자원이 한계치에 도달했을 때, 파드를 강제로 즉시 종료 시켜 노드를 살리는 임계값</li>
</ul>
</li>
</ul>
<h2 id="4-마무리-및-설정-반영">4. 마무리 및 설정 반영</h2>
<h3 id="4-1-kubelet-재시작-필수">4-1. Kubelet 재시작 (필수)</h3>
<pre><code class="language-bash">sudo systemctl restart kubelet</code></pre>
<h3 id="4-2-마스터-노드-상태-및-자원-할당량-확인">4-2. 마스터 노드 상태 및 자원 할당량 확인</h3>
<pre><code class="language-bash">kubectl describe node master | grep -A 5 &quot;Allocatable:&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/9d5baaec-524f-41be-8c9b-61c5e7f22db3/image.png" alt=""></p>
<blockquote>
<p>설정이 완료된 후 <code>Allocatable</code> 수치를 확인해 보면, 마스터 노드 예약 자원을 제외한 <code>“실제 가용 범위&quot;</code> 를 확인할 수 있습니다.</p>
</blockquote>
<ul>
<li><code>cpu: 3</code><ul>
<li>4코어 중 시스템용으로 1코어를 예약했기 때문에 <code>cpu - 3</code> 가 출력</li>
</ul>
</li>
<li><code>ephemeral-storage: 22536xxxxx</code><ul>
<li>256GB 스토리지 중 시스템 영역을 제외하고 파드들이 사용 가능한 저장 공간</li>
</ul>
</li>
<li><code>memory: 581xxxxxKi</code><ul>
<li>전체 RAM 8GB 중, systemReserved + kubeReserved 로 설정한 2.5Gi 와 evictionHard 로 설정한 500Mi 등으로 제외한 수치</li>
</ul>
</li>
<li><code>pods: 110</code><ul>
<li>해당 노드에 최대로 올릴 수 있는 파드의 개수 (쿠버네티스 기본값 == 110)</li>
</ul>
</li>
</ul>
<p>→ 마스터 노드 예약 리소스 설정 완료!😋</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (11)  Monitoring Dashboard]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-11-Monitoring-Dashboard</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-11-Monitoring-Dashboard</guid>
            <pubDate>Mon, 09 Mar 2026 03:38:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번에는 홈랩의 모니터링 서버를 구축해보려고 합니다. 고도화 해볼 요소가 많은 부분인데, 우선 간단하게 <code>Node-Exporter/kube-state-metrics + Prometheus + Grafana</code> 를 사용하겠습니다.</p>
</blockquote>
<h2 id="아키텍처-후보">아키텍처 후보</h2>
<blockquote>
<p>우선 공통적으로 모니터링 서버를 클러스터와 격리시키면 좋겠다고 생각했습니다. 외부 클라우드 서비스에 모니터링 서버를 띄우는게 괜찮은지는 모르겠다만,, <code>‘클러스터와 모니터링 서버의 격리</code>’에 초점을 두기로 했습니다.</p>
</blockquote>
<h3 id="1안---각-노드에-모니터링용-포트를-개방하여-외부-모니터링-서버와-연결하기-포트포워딩">1안) - 각 노드에 모니터링용 포트를 개방하여 외부 모니터링 서버와 연결하기 (포트포워딩)</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/22f9a92d-ff84-4626-bfa7-a7cedc372bfc/image.png" alt=""></p>
<ul>
<li><code>장점</code><ul>
<li>구축이 비교적 간단하다. (모든 노드에 대한 포트를 열고 연결하면 되므로)</li>
</ul>
</li>
<li><code>단점</code><ul>
<li>현재 클러스터에서는 Bastion 방식으로 워커 노드를 숨기고 있는데 이 구조가 깨지게 됨</li>
<li>매트릭이 오고 가는 통로가 외부에 노출되므로 보안상 위험할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="2안---마스터-노드와-모니터링-서버만-연결하고-이때-연결-방식은-vpn-방식을-사용하기-vpn--bastion-구조">2안) - 마스터 노드와 모니터링 서버만 연결하고, 이때 연결 방식은 VPN 방식을 사용하기 (VPN + Bastion 구조)</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/e8c8fc30-a6da-4607-9c99-9c9187c87b92/image.png" alt=""></p>
<ul>
<li><code>장점</code><ul>
<li>기존 클러스터 구축 방식과 동일하게 Bastion 구조로 워커 노드를 외부로 노출시키지 않음</li>
<li>마스터 노드와 모니터링 서버만 연결을 하고, 이를 VPN 방식으로 연결했기 때문에 매트릭이 지나는 통로를 숨길 수 있음.<ul>
<li>공인 IP 노출 없이 NAT 환경(가정용 공유기)을 통과하여 양방향 통신을 함. - <code>Hole Punching</code></li>
</ul>
</li>
</ul>
</li>
<li><code>단점</code><ul>
<li>마스터 노드를 <code>Metric Gateway</code> 로도 사용하게 됨.(트래픽 집중 현상이 발생하지 않을까..? 하는 생각이 듭니다.)</li>
<li>구축이 비교적 복잡하다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>클러스터 아키텍처상 Bastion 방식을 채택하고 있으므로 이를 유지하고자 <code>2안</code> 의 방식으로 구축을 진행하겠습니다.</p>
</blockquote>
<hr>
<h2 id="전체-아키텍처">전체 아키텍처</h2>
<ul>
<li>라즈베리파이 클러스터 (데이터 생성)<ul>
<li><code>Node Exporter</code> &amp; <code>Kube-state-metrics</code> 가 각각 하드웨어 지표와 쿠버네티스 상태 정보를 생성</li>
</ul>
</li>
<li>OCI 인스턴스 (수집 및 시각화)<ul>
<li><code>Prometheus</code> 가 Tailscale을 통해 라즈베리파이의 데이터를 Scraping 하여 저장하고, <code>Grafana</code> 가 이를 시각화</li>
</ul>
</li>
<li>보안 게이트웨이 설계<ul>
<li>보안을 위해 워커 노드를 외부에 노출하지 않고, 마스터 노드를 통해서만 지표가 나가도록 설계</li>
</ul>
</li>
</ul>
<h2 id="1단계-oci-인스턴스-보안-설정-ingress">1단계: OCI 인스턴스 보안 설정 (Ingress)</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/6c9bbdb1-cf37-4667-9e58-3d03f523a59f/image.png" alt=""></p>
<ul>
<li>Security List 규칙 추가<ul>
<li>3000 (Grafana 웹 대시보드 접속용)</li>
<li>9090 (Prometheus 타겟 상태 확인 및 디버깅 확인용)</li>
</ul>
</li>
<li>OS 방화벽(iptables/firewalld) 설정<ul>
<li>OCI 보안 리스트뿐만 아니라 인스턴스 내부 방화벽에서도 해당 포트를 <code>ACCEPT</code> 하도록 설정</li>
</ul>
</li>
</ul>
<h2 id="2단계-네트워크-연결-tailscale-vpn">2단계: 네트워크 연결 (Tailscale VPN)</h2>
<ul>
<li>공인 IP 노출 없이 안전하게 데이터를 전송하기 위한 연결</li>
<li>Tailscale 설치<ul>
<li>OCI 인스턴스와 라즈베리파이 마스터 노드에 각각 설치하여 사설 IP로 통신하도록 설정</li>
</ul>
</li>
<li>MTU 최적화<ul>
<li>대량의 메트릭 데이터(특히 <code>kube-state-metrics</code>) 전송 시 패킷 유실을 방지하기 위해 Tailscale 인터페이스의 <code>MTU</code>를 <code>1280</code>으로 조정<ul>
<li>⚠️ 현재 구축 환경에서는 kube-state-metrics 가 수집한 매트릭이 유실되거나, OOM이 발생하여 인스턴스가 다운되는 상황이 자주 발생.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="3단계-라즈베리파이-에이전트-배치">3단계: 라즈베리파이 에이전트 배치</h2>
<ul>
<li><code>Node Exporter</code><ul>
<li>각 노드의 CPU, 메모리, 온도 등의 지표 수집</li>
</ul>
</li>
<li><code>kube-state-metrics</code><ul>
<li>쿠버네티스 API 를 통해 파드, 노드, 배포 상태 등을 수집</li>
<li>NodePort 설정: OCI에서 접근할 수 있도록 30080 포트 개방</li>
<li>리소스 최적화: 타임아웃 방지를 위해 CPU/Memory Request 를 명시적으로 할당</li>
</ul>
</li>
</ul>
<h2 id="4단계-oci-prometheus--grafana-구축">4단계: OCI Prometheus &amp; Grafana 구축</h2>
<ul>
<li><code>Docker Compose 구성</code><ul>
<li>두 서비스를 컨테이너로 띄워 관리 효율성 높임</li>
</ul>
</li>
<li><code>Prometheus 네트워크 모드 (network_mode: host)</code><ul>
<li>컨테이너 내부가 아닌 호스트의 Tailscale 네트워크를 직접 사용하도록 설정하여 통신 장애를 해결</li>
</ul>
</li>
<li><code>prometheus.yml 설정</code><ul>
<li>scrape_interval 과 scrape_timeout 을 조정하여 지연 시간 극복</li>
<li>대시보드 필터링을 위해 <code>cluster: ‘raspberry-pi-cluster’</code> 라벨을 강제로 부여</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h2 id="⚠️-문제-발생">⚠️ 문제 발생</h2>
<blockquote>
</blockquote>
<ul>
<li>현재 모니터링 서버는 OCI(Oracle Cloud Architecture) 프리티어 계정으로 받은 인스턴스에 띄워져 있습니다.</li>
<li>Node-Exporter가 수집하는 <code>매트릭 + Refresh 1m</code> 는 문제가 없는데, <code>kube-state-metrics + Refresh 1m</code> 조합은 인스턴스가 터지는 상황이 계속 발생했습니다.</li>
<li>너무 많은 양의 매트릭 + 전송량 + Refresh 주기 등의 문제로 OOM이 발생하여 인스턴스가 다운되는 것 같음.<blockquote>
</blockquote>
→ 우선, 모니터링 대시보드 구축이 첫 목표였기에 Node-Exporter 의 매트릭을 사용하여 모니터링 대시보드를 구축하겠습니다.<blockquote>
</blockquote>
→ Relabeling 기능 사용, 인스턴스 사양 업그레이드, gRPC 도입 등으로 추후 해결해보겠습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/e8f1523e-9be6-4412-b200-8e2b91c8f3bb/image.png" alt=""></p>
<ul>
<li><code>Prometheus</code> 는 정상적으로 연결이 되어 있는 상태이므로,,, 추후 모니터링 고도화 작업을 진행하며 <code>kube-state-metrics</code> 또한 살려보겠습니다..!</li>
</ul>
<h2 id="5단계-grafana-시각화">5단계: Grafana 시각화</h2>
<ul>
<li>데이터 소스 연동<ul>
<li>특정 URL로 Prometheus 연결</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/debda536-6cad-49bb-9a5f-8c9ea77f90af/image.png" alt=""></p>
<blockquote>
</blockquote>
<h2 id="📌-모니터링-방법론-use-red-fore-golden-signal">📌 모니터링 방법론 (USE, RED, Fore golden signal)</h2>
<blockquote>
</blockquote>
<ul>
<li>이번 포스팅에서 자세히 다룰 것은 아니지만, 다양한 방법론이 있기에 이들을 학습해보고 적절히 도입을 해보면 좋을 것 같습니다.<blockquote>
</blockquote>
<h3 id="use-method---인프라-시스템-중심--node-exporter">USE Method - 인프라 시스템 중심 / Node-Exporter</h3>
<blockquote>
</blockquote>
하드웨어 자원의 상태를 확인하기 위한 방법론<blockquote>
</blockquote>
</li>
<li><code>Utilization (사용률)</code><ul>
<li>자원이 얼마나 사용되었는가?<ul>
<li>ex: node_cpu_seconds_total<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><code>Saturation (포화도)</code><ul>
<li>자원이 얼마나 줄을 서서 기다리고 있는가?<ul>
<li>ex: node_load1<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><code>Errors (에러)</code><ul>
<li>하드웨어/드라이버 레벨의 에러 발생 횟수<ul>
<li>ex: node_net_errs_total<blockquote>
</blockquote>
<h3 id="red-method---서비스-중심--app-메트릭-kube-state-metrics">RED Method - 서비스 중심 / App 메트릭, kube-state-metrics</h3>
<blockquote>
</blockquote>
사용자가 느끼는 서비스의 품질을 측정하기 위한 방법론<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><code>Rate (TPS 등)</code><ul>
<li>초당 요청 수<ul>
<li>ex: http_request_total<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><code>Errors (에러)</code><ul>
<li>실패한 요청 수<ul>
<li>ex: HTTP 500 에러 비율<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><code>Durations (응답 시간)</code><ul>
<li>요청을 처리하는 데 걸린 시간<ul>
<li>ex: http_request_duration_seconds_bucket<blockquote>
</blockquote>
<h3 id="four-golden-signal--구글-sre-팀의-핵심-지표">Four golden signal / 구글 SRE 팀의 핵심 지표</h3>
<blockquote>
</blockquote>
구글에서 정의한 가장 유의미한 4가지 지표. RED 메서드의 확장판..?<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><code>Latency (지연 시간)</code><ul>
<li>서비스 응답에 걸리는 시간<blockquote>
</blockquote>
</li>
</ul>
</li>
<li><code>Traffic (트래픽)</code><ul>
<li>서비스에 대한 수요<blockquote>
</blockquote>
</li>
</ul>
</li>
<li><code>Errors (에러)</code><ul>
<li>명시적에러(500), 암시적 에러(성공했지만 응답 내용이 잘못됨) 등<blockquote>
</blockquote>
</li>
</ul>
</li>
<li><code>Saturation (포화도)</code><ul>
<li>서비스가 얼마나 꽉 찼는지.</li>
<li>CPU 뿐만 아니라 큐의 길이나 메모리 잔량 포함<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
</blockquote>
<p>USE - 인프라(Hardware)</p>
<blockquote>
</blockquote>
<p>RED - 서비스(Software)</p>
<blockquote>
</blockquote>
<p>Golden Signals - 사용자 경험(SRE)</p>
<h3 id="마무리">마무리</h3>
<p>모니터링 시스템 고도화와 로깅 시스템 고도화에 재밌는 작업들이 많을 것으로 예상됩니다. 현재는 <code>Node-Exporter + Prometheus + Granfana</code> 조합만을 사용하고 있지만, 고도화 과정에서</p>
<ul>
<li>kube-state-metrics 사용</li>
<li>Loki 도입</li>
<li>⭐️ Opentelemetry + gRPC + Grafana 구축</li>
</ul>
<p>등을 해보면 좋을 것 같습니다. (gRPC로 로그나 매트릭을 보내게 된다면 통신 부담이 조금은 덜 하지 않을까 싶습니다..)</p>
<p>그리고 매번 모니터링 시스템을 ‘구축’ 만 해보았는데, 이번 기회에 다양한 방법론도 도입해보고 ‘어떻게 유의미한 데이터를 만들어낼 수 있을지’ 에 대해서도 고민해보면 좋을 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (10) [Refactor] Building a Truly Stateful (Storage)]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-10-Refactor-Building-a-Truly-Stateful-Storage</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-10-Refactor-Building-a-Truly-Stateful-Storage</guid>
            <pubDate>Mon, 09 Mar 2026 01:39:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>현재 입주해 있는 테넌트 중 한 분이 “인스턴스가 재시작 되면 일부 코드가 날라가는 것 같아요” 라는 피드백을 주셨습니다. <a href="https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-3-Storage-Quota-Isolation">이전에 진행한 스토리지 설정 과정</a> 에서 스토리지 자원 관리(Quota)와 데이터 격리(Isolation)만 설정을 했고, 영속성에 대한 생각을 못했던 것이 문제가 되었습니다.
치명적인 문제가 발생했기에,, 빠르게 작업을 해주었습니다..</p>
</blockquote>
<blockquote>
</blockquote>
<h2 id="들어가기-전-개념-정리-pv--pvc">들어가기 전 개념 정리 (PV &amp; PVC)</h2>
<blockquote>
</blockquote>
<ul>
<li>쿠버네티스에서 스토리지는 크게 두 단계로 관리됩니다.<blockquote>
</blockquote>
<h3 id="📌-pv-persistentvolume">📌 PV (PersistentVolume)</h3>
<blockquote>
</blockquote>
</li>
<li>실제 물리적 스토리지(SSD)의 일부분을 쿠버네티스가 인식할 수 있는 자원 형태로 등록해둔 것 → <code>관리자 영역</code><blockquote>
</blockquote>
<h3 id="📌-pvc-persistentvolumeclaim">📌 PVC (PersistentVolumeClaim)</h3>
<blockquote>
</blockquote>
</li>
<li>사용자가 특정 용량과 접근 권한을 명시하여 스토리지를 할당받으려고 제출하는 요청 → <code>테넌트 영역</code><blockquote>
</blockquote>
테넌트가 PVC를 생성하면, 쿠버네티스는 테넌트의 요구사항(용량, 읽기/쓰기 모드)에 맞는 PV를 찾아서 서로 연결(<code>Binding</code>) 해줍니다.</li>
</ul>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<ul>
<li><code>/config</code> 내에 생성한 데이터, 파일들은 영속성이 보장되지만, 그 밖에 생성한 코드의 경우 영속성 유지가 안 됨.</li>
<li>이는 파드가 죽을 경우 해당 디렉토리 밖에 있는 폴더나 설정들이 모두 사라지게 됨</li>
</ul>
<h2 id="해결-방안-영속성-범위를-홈-디렉토리까지-확장">해결 방안: 영속성 범위를 홈 디렉토리까지 확장</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/440e2929-eec1-4c13-acb7-032ca346474f/image.png" alt=""></p>
<ul>
<li><p>현재 <code>v1-basic-setup.sh</code> 내에 있는 volumeMounts의 경로는 /config</p>
</li>
<li><p>→ 이를 유지하며 <code>subPath</code> 기능을 사용해 영역 추가하기</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/63715f89-2875-4377-82c5-8736600040a1/image.png" alt=""></p>
<ul>
<li><code>name: tenant-storage</code><ul>
<li>역할: 연결할 PVC의 이름을 지정</li>
<li>기능: 하단 volumes 섹션에 정의된 <code>tenant-storage</code> 라는 이름의 PVC를 찾아서 가져옴. 이전에 설정한 8Gi 짜리 디스크 공간을 가리킴</li>
</ul>
</li>
<li><code>mountPath</code><ul>
<li>역할: 파드 내부에서 데이터가 보일 가상 경로</li>
<li>기능:<ul>
<li><code>/config</code> : SSH 서버가 부팅될 때 필요한 호스트 키, 설정 파일들을 읽어가는 장소</li>
<li><code>/home/tenant-user</code> : 테넌트가 접속하자마자 마주하는 홈 디렉토리</li>
</ul>
</li>
<li>효과: 이 경로에 파일을 저장하면 파드 내부의 휘발성 메모리가 아닌, 실제 외부 디스크(PVC)에 기록됨</li>
</ul>
</li>
<li><code>subPath</code><ul>
<li>역할: 디스크 내 격리된 저장 구역 생성</li>
<li>기능: 하나의 PVC 내부에 <code>system-config</code>, <code>tenant-home-data</code> 라는 별도의 디렉토리를 자동 생성하여 관리<ul>
<li><code>system-config</code> : PVC 내부에 <code>system-config</code> 라는 폴더를 만들어 SSH 시스템 파일만 따로 모음</li>
<li><code>tenant-home-data</code> : PVC 내부에 <code>tenant-home-data</code> 폴더를 만들어 유저가 생성한 폴더나 파일들만 따로 모음</li>
</ul>
</li>
<li>도입 이유<ul>
<li><code>subPath</code> 없이 마운트하면 볼륨의 루트(Root)가 해당 경로를 완전히 덮어씌우는 상황 발생.</li>
<li><code>subPath</code> 기능을 통해 시스템 설정 파일과 사용자 데이터를 물리적으로 분리 &amp; 관리포인트(PVC)는 하나로 유지</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="현재-홈랩-물리-디스크의-구조">현재 홈랩 물리 디스크의 구조</h3>
<ul>
<li><code>tenant-storage</code><ul>
<li>8Gi 용량의 실제 물리 디스크 (PVC)</li>
</ul>
</li>
<li><code>system-config</code><ul>
<li>SSH 구동을 위한 필수 설정 저장소 (/config)</li>
</ul>
</li>
<li><code>tenant-home-data</code><ul>
<li>유저의 실제 데이터 저장소 (/home/tenant-user)</li>
</ul>
</li>
</ul>
<h3 id="before--after">Before &amp; After</h3>
<ul>
<li><code>Before</code><ul>
<li>파드가 재시작 되면 SSH 설정은 남지만, 테넌트가 작성한 코드 등의 데이터는 사라짐(휘발성)</li>
</ul>
</li>
<li><code>After</code><ul>
<li>파드가 재시작 되어도 설정과 테넌트가 저장한 데이터 모두 남아 있음(영속성)</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (9) Network Trouble Shooting Report]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-9-%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%ED%86%B5%ED%95%A9-%EB%A6%AC%ED%8F%AC%ED%8A%B8</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-9-%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%ED%86%B5%ED%95%A9-%EB%A6%AC%ED%8F%AC%ED%8A%B8</guid>
            <pubDate>Mon, 09 Mar 2026 01:09:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>홈랩 구축 및 멀티 테넌트 프로젝트를 진행하며 압도적으로 고생을 많이 했던 네트워크 설정 과정에서 경험한 트러블 슈팅 내용을 정리해보았습니다. 가정용 네트워크 환경에서 구축한 홈랩이다보니 문제 원인을 찾는 것도 오래 걸리고,, 파악하는 것도 정말 오래 걸렸던 것 같습니다..(<del>덕분에 네트워크 공부가 아주 많이 되었네용</del>)</p>
</blockquote>
<hr>
<h2 id="1-배경---csp-모델-구현을-위한-프로젝트">1. 배경 - CSP 모델 구현을 위한 프로젝트</h2>
<ul>
<li>목표: 라즈베리파이로 구축한 홈랩 클러스터를 활용해 외부 테넌트에게 <code>공인 IP + SSH</code> 기반의 독립 인스턴스 환경을 제공하기</li>
<li>핵심 도구: Calico(CNI), MetalLB(L2 Mode), SSH</li>
</ul>
<h2 id="2-phase-1-calico와-커널의-충돌-초기-장애">2. Phase 1: Calico와 커널의 충돌 (초기 장애)</h2>
<blockquote>
<p>가장 먼저 마주한 충돌 요소는 채택한 <code>CNI 방식</code>(Flannel → Calico)과 라즈베리파이 <code>커널의 충돌</code></p>
</blockquote>
<ul>
<li><p>현상: 노드는 <code>Ready</code> 지만 파드 간 통신 두절, 패킷 유실 발생</p>
</li>
<li><p>원인 분석:</p>
<ul>
<li>원인 1) <code>Tailscale 간섭</code><ul>
<li>초기 구축 과정 중(포트포워딩 전) 집 밖(외부)에서도 작업을 하기 위해 VPN(Tailscale)을 사용</li>
<li>VPN 인터페이스와 VXLAN 경로가 충돌하여 패킷이 길을 잃음<ul>
<li>→ <a href="%5Bhttps://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-6-CNI-%EC%84%A4%EC%A0%95-Calico-%EC%82%AC%EC%9A%A9%5D(https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-6-CNI-%EC%84%A4%EC%A0%95-Calico-%EC%82%AC%EC%9A%A9)">CNI 설정</a> 에서 가정용 네트워크 환경과 <code>설정 편의성</code> 을 고려하여 VXLAN 모드를 채택했습니다.</li>
</ul>
</li>
</ul>
</li>
<li>원인 2) <code>체크섬 오프로드 오류</code><ul>
<li>라즈베리파이 랜카드 하드웨어 가속 기능이 패킷을 변조된 것으로 오인하여 드랍하는 상황 발생</li>
</ul>
</li>
<li>원인 3) <code>커널 설정 미비</code><ul>
<li><code>ip_forward</code> 비활성화 및 <code>iptables</code> 포워딩 정책 차단</li>
</ul>
</li>
</ul>
</li>
<li><p>💡 <strong>해결 !</strong></p>
<ul>
<li>인터페이스 정리, <code>ethtool</code> 로 체크섬 오프로드 비활성화, 커널 포워딩 활성화</li>
</ul>
</li>
</ul>
<pre><code class="language-bash">ethtool -K eth0 tx off rx off</code></pre>
<h2 id="3-phase-2-metallb-l2-모드-문제">3. Phase 2: MetalLB L2 모드 문제</h2>
<blockquote>
<p>커널 문제를 해결했으나 L2 계층(ARP)의 한계가 드러남</p>
</blockquote>
<ul>
<li><p>현상: <code>type: LoadBalancer</code> 로 IP(192.168.x.x)는 할당이 되지만, 외부 접속 시 <code>TIMEOUT</code> 발생</p>
</li>
<li><p><code>디버깅 (tcpdump/log)</code> : MetalLB Speaker 로그에 메시지가 뜨지 않음</p>
</li>
<li><p>원인 분석:</p>
<ul>
<li><code>(가정용) 공유기 환경의 한계</code><ul>
<li>가정용 공유기가 MetalLB의 ARP 광고(Announce)를 ARP 스푸핑으로 간주해 차단하거나 무시하는 것으로 추정됨..🤬</li>
<li>+) 가정용 공유기의 경우 보안을 위해 ARP 스푸핑 방지 기능이 기본 탑재된 경우가 많다고 합니다. 저희 집 네트워크 관리자 페이지에서는 해당 기능을 설정할 수 있는 항목이 없었기에,,, 선택의 여지가 없었네요..</li>
</ul>
</li>
<li><code>CNI 충돌</code><ul>
<li>Calico 와 MetalLB 가 동일한 인터페이스를 제어하려다 발생하는 논리적 충돌도 가능성이 높았음</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>해결 방안..!</strong></p>
<ul>
<li>불안정한 Calico를 버리고, eBPF 기반의 <code>Cilium</code> 으로 마이그레이션 결정<ul>
<li>초기 선택지였던 <code>Flannel, Calico, Cilium</code> 중 남은 CNI 툴은 Cilium 뿐이었음..</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="4-phase-3-cilium-도입과-nodeport-우회-최종-해결-방법">4. Phase 3: Cilium 도입과 NodePort 우회 (최종 해결 방법)</h2>
<blockquote>
<p>하나 밖에 남지 않은 선택지 &amp; 추후 도입(고도화) 하려고 했던 <code>Cilium</code> 을 도입하고, 불확실한 L2 광고(Announce) 대신 확실한 경로를 선택하도록 변경</p>
</blockquote>
<ul>
<li><p>전략: MetalLB의 L2 ARP 방식이 (가정용) 공유기 환경에서 신뢰도가 낮음을 확인. NodePort 기반의 직결 경로로 방식 변경</p>
</li>
<li><p>최종 아키텍처</p>
<ol>
<li><code>Service</code> : LoadBalancer → NodePort 로 전환</li>
<li><code>공유기</code> : 공인 IP:2001 → 노드IP:Port 방식으로 포트포워딩</li>
<li><code>흐름</code> : 외부 요청이 실존하는 노드 IP를 타고 파드로 즉시 전달됨</li>
</ol>
</li>
<li><p>결과: 네트워크 추상화 레이어를 한 단계 낮춤으로써 안정적인 SSH 접속 성공!!</p>
</li>
</ul>
<h2 id="포트-관리-체계최최최종">포트 관리 체계(최최최종)</h2>
<table>
<thead>
<tr>
<th>계층</th>
<th>구분</th>
<th>예시</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>1단계 (외부)</td>
<td>공용 포트 (Public)</td>
<td>50111</td>
<td>사용자가 외부 인터넷에서 실제 접속 시 사용하는 번호</td>
</tr>
<tr>
<td>2단계 (공유기)</td>
<td>노드 포트 (NodePort)</td>
<td>30111</td>
<td>공유기 포트포워딩의 목적지이자, K8s 노드가 열고 있는 포트</td>
</tr>
<tr>
<td>3단계 (내부)</td>
<td>타겟 포트 (Target)</td>
<td>2222</td>
<td>실제 파드 내부에서 SSH 데몬이 떠 있는 포트</td>
</tr>
</tbody></table>
<blockquote>
<p>라즈베리파이 본체의 SSH 접속 포트(22)와 파드의 SSH 접속 포트 간의 충돌을 방지하고자 파드 내부의 SSH 포트를 2222 로 설정했습니다.</p>
</blockquote>
<h2 id="📌-네트워크-포트-범위">📌 네트워크 포트 범위</h2>
<table>
<thead>
<tr>
<th>포트 범위</th>
<th>명칭</th>
<th>특징 및 활용 예시</th>
</tr>
</thead>
<tbody><tr>
<td>0 ~ 1023</td>
<td>Well-Known Ports (특권 포트)</td>
<td>시스템의 공식 창구(SSH-22, HTTP-80 등). 일반 사용자는 함부로 열 수 없고 <code>root</code> 권한이 필요</td>
</tr>
<tr>
<td>1024 ~ 49151</td>
<td>Registered Ports (등록 포트)</td>
<td>특정 애플리케이션이나 서비스가 등록해서 사용하는 구간. 30000 ~ 32767 구간이 쿠버네티스의 NodePort 기본 범위.</td>
</tr>
<tr>
<td>49152 ~ 65535</td>
<td>Dynamic/Private Ports (사설 포트)</td>
<td>누구나 자유롭게 사용하고, 임시로 쓰고 버리는 구간. 현재: 외부로 노출시키는 공용 포트(Public)는 5만번대를 사용</td>
</tr>
</tbody></table>
<blockquote>
</blockquote>
<h2 id="❗️-우리집-가정용-공유기에서-calico는-안-되었고-cilium-은-되었던-이유">❗️ (우리집) 가정용 공유기에서 Calico는 안 되었고, Cilium 은 되었던 이유</h2>
<blockquote>
</blockquote>
<h3 id="1-calico의-실패---vxlan--iptables">1. Calico의 실패 - (VXLAN + Iptables)</h3>
<blockquote>
</blockquote>
<ul>
<li><code>공유기 입장</code><ul>
<li>패킷 안에 또 패킷이 들어 있으니 구조가 복잡함(VXLAN)</li>
<li>라즈베리파이의 랜카드(NIC)가 이 복잡한 패킷의 체크섬(오류검사)을 계산하다가 실수를 하면, 공유기는 이를 변조된 패킷 혹은 ARP 스푸핑 공격으로 오해해서 버려버림</li>
</ul>
</li>
<li><code>자원 소모</code><ul>
<li>Calico 는 iptables 라는 규칙 수 천개를 일일이 검사함</li>
<li>저사양 공유기와 라즈베리파이 입장에서 이 과정에서 발생하는 Latency 가 통신 두절로 이어짐<blockquote>
</blockquote>
<h3 id="2-cilium의-성공---ebpf-direct-routing">2. Cilium의 성공 - (eBPF Direct Routing)</h3>
<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>Cilium은 ‘지능형 고속도로’와 같음</li>
<li><code>커널 직접 처리</code><ul>
<li>Cilium 은 패킷이 커널에 들어오자마자 eBPF 프로그램을 통해 목적지 지정을 즉시 판별함. 복잡한 iptables를 거치지 않으니 패킷이 깨끗하고 가벼운 장점이 있음.</li>
</ul>
</li>
<li><code>Native Routing</code><ul>
<li>Cilium 은 설정을 통해 VXLAN 같은 무거운 포장 없이 물리망(L2/L3)에 직접 패킷을 태울 수 있음.</li>
<li>공유기 입장에서는 복잡한 캡슐화가 없는 정상적이고 가벼운 패킷으로 보이기 때문에 ARP 스푸핑 방지 로직에 걸리지 않고 통과됨</li>
</ul>
</li>
</ul>
<h2 id="📌-정리">📌 정리</h2>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/6282576f-9ac0-4c6c-bc5b-dada20ed7ae1/image.jpeg" alt=""></p>
<h3 id="calico">Calico</h3>
<ul>
<li>L3 초입에 위치하는 소프트웨어이며, iptables(2계층 이후 3~4계층에 걸쳐 위치) 까지 도착한 패킷을 검사함.</li>
<li>수천개의 iptable의 규칙을 조회하므로 많은 통신이 발생.</li>
<li>추가적으로 VXLAN, BGP 등의 통신 방법(주머니로 감쌈)을 사용하기 때문에 이 포장지를 벗기는 데에도 연산이 필요.</li>
</ul>
<h3 id="cilium">Cilium</h3>
<ul>
<li>L2 초입(XDP, TC)에 eBPF 프로그램으로 작성한 샌드박스 내에 위치한 소프트웨어.</li>
<li>eBPF 내에서 패킷을 가로채서 분석하고, 이때 3계층에서 쓰이는 ip와 4계층에서 쓰이는 port를 먼저 들여다보고 정상적인 패킷이라면 이후 계층에서 복잡한 검사를 하지 않고 바로 통과시킴(지능형 고속도로 역할).<ul>
<li>→ <code>Socket Redirection</code></li>
</ul>
</li>
</ul>
<h3 id="metallb">MetalLB</h3>
<ul>
<li>L2 레벨에서 ARP 수행.</li>
<li>CSP에서 제공하는 로드밸런서 기능을 L2 단계에서 제공함으로써 쿠버네티스 내에서 로드밸런서 기능을 지원하게 해줌.</li>
</ul>
<h3 id="트러블-슈팅의-원인과-해결">트러블 슈팅의 원인과 해결</h3>
<ul>
<li><p><code>원인 1)</code> Calico의 VXLAN 해석과 Iptables의  내의 수천개의 규칙 조회에 시간도 오래 걸리고 라즈베리파이 NIC에서 자체적으로 진행하는 체크섬 과정 중 오류가 발생하여 변조된 패킷 혹은 스푸핑 공격으로 오해함. 이때 가정용 공유기는 해당 통신을 사전에 정의된 L2 레벨에 적용되는 ARP 스푸핑 로직으로 걸러버림.</p>
</li>
<li><p><code>원인 2)</code> MetalLB의 ARP 광고는 L2 레벨에서 광고를 하는데, 이때 무분별한 광고를 공유기는 ARP 스푸핑으로 간주하기도 함. 그래서 세부 조정이 안되는 가정용 공유기는 MetalLB의 L2 단계에서의 로드밸런싱을 스푸핑으로 인식하고 패킷을 drop 함.</p>
</li>
<li><p><code>해결 1)</code> VXLAN, BGP 사용하지 않음.</p>
</li>
<li><p><code>해결 2)</code> Cilium은 L2 단계의 최전방(NIC 을 통과한 직후 XDP/TC)에 위치하며, 이곳에서는 흔한 개발 언어로는 프로그래밍을 할 수 없지만, 커널단에서 샌드박스 형식으로 프로그래밍을 할 수 있는 eBPF 기술을 사용하여 그 내부에 구축된 소프트웨어.</p>
</li>
<li><p>초입에서 eBPF로 패킷을 가로채서 확인을 하는데 이때 미리 패킷을 열어보아서 3계층(IP), 4계층(Port) 정보를 미리 확인, 판별 및 조회하고 정상적인 패킷이라면, 리눅스 커널의 <code>Socket Redirection</code> 기능을 사용</p>
<ul>
<li><code>Socket Redirection</code> : 표준네트워크 스택 연산 과정을 생략(Bypass)하고 파드의 소켓(Socket)으로 데이터를 직접 전달</li>
</ul>
</li>
</ul>
<h3 id="최종결론">최종결론</h3>
<ul>
<li>복잡하고 많은 연산을 하는 <code>Calico</code>와 가정용 공유기가 통과시키지 않는 L2 단계에서의 ARP 광고를 하는 <code>MetalLB</code>를 사용했을 때는 ‘연산시간 오래 걸림’, ‘타임아웃’으로 인해 변조된 패킷 혹은 ARP 스푸핑으로 간주 &amp; MetalLB의 작업을 ARP 스푸핑으로 간주하여 통신이 안 되었음.<ul>
<li>→ 쿠버네티스에서는 Egress까지 가는 모든 설정과 연결을 완료했지만 Ingress가 오는 통로에서 수많은 장애물과 역경이 있는 상황.</li>
</ul>
</li>
<li>Calico와 MetalLB를 버리고 <code>Cilium</code>을 사용하였는데 이는 2계층 초입에서 eBPF 기술을 사용하여 패킷을 가로채어 확인하고 지능형 고속도로로 바로 쏴버림.<ul>
<li>→ 오랜 연산시간이 필요하지 않고 가정용 공유기가 혼동하게끔 L2 광고를 하지 않기 때문에 Ingress에 필요한 모든 작업이 정상 수행됨.</li>
</ul>
</li>
</ul>
<h3 id="마무리며칠-밤을-새며-얻은-교훈">마무리(며칠 밤을 새며 얻은 교훈….)</h3>
<ul>
<li><code>가정용 네트워크 환경은 실제 현업 환경에 비해 매우 열악하다.</code><ul>
<li>BGP, L2 ARP, VXLAN 같은 고급 기술은 기업용 스위치가 없는 가정용 공유기 환경에서 언제든 깨질 수 있고, 원인 파악이 쉽지 않다…🥲</li>
</ul>
</li>
<li><code>가시성이 참말로 중요하다.</code><ul>
<li><code>tcpdump</code> 가 없었다면 ARP 단계에서 신호가 끊긴다는 사실을 파악할 수 없었을 것 같다.(<del>땡큐 제미나이..!!</del>)</li>
</ul>
</li>
<li><code>주어진 환경에서 가능한(실용적) 방법을 최대한으로 사용하기.</code><ul>
<li>LoadBalancer 설정을 해보고 싶었지만,, 관리자(나)가 구조를 완벽하게 파악할 수 있는 <code>공유기 포트포워딩 + NodePort</code> 조합이 좋은 것 같다. (<del>집에다가 고가 장비를 구축하기엔,,,</del>)</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (8) External Access Point]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-8-External-Access-Point</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-8-External-Access-Point</guid>
            <pubDate>Sun, 08 Mar 2026 04:58:25 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>쿠버네티스 내부의 파드는 기본적으로 외부와 격리되어 있습니다. 관리자(나)는 각 테넌트 네임스페이스에 <code>MetalLB</code> 를 이용한 <code>LoadBalancer IP</code> 를 할당하고, 이를 통해 사용자가 할당받은 IP로 즉시 <code>SSH</code> 접속을 할 수 있는 환경을 구축해보고자 합니다. 이렇게 한다면 각 테넌트 입장에서 고유한 가상 서버를 제공받고, 사용하는 경험을 하게 됩니다. (초기 프로젝트 목표였던, 외부 테넌트에게 CSP처럼 인스턴스를 제공하는 부분의 핵심입니다!)</p>
</blockquote>
<h3 id="핵심-목표">핵심 목표</h3>
<ul>
<li><code>네트워크 독립성 확보</code> : 테넌트별로 고유한 내부망 IP(LoadBalancer IP) 를 할당하여 자원 간 간섭을 최소화</li>
<li><code>접속 편의성 극대화</code> : 22번 포트(SSH 전용)를 외부로 노출하여 접속 가능하도록 환경 구성</li>
<li><code>인프라 추상화</code> : 테넌트가 쿠버넽네티스 노드나 파드의 개념을 몰라도, 부여받은 IP 하나로 서버(인스턴스)처럼 활용할 수 있게 함</li>
</ul>
<hr>
<h1 id="1-기능-설명-테넌트별-전용-고정-ip-할당">1. [기능 설명] 테넌트별 전용 고정 IP 할당</h1>
<blockquote>
<p>사용자가 파드를 ‘하나의 독립된 가상 서버’처럼 느끼게 하기 위해 네트워크 접점을 제공</p>
</blockquote>
<h2 id="1-1-주요-설계-목표">1-1. 주요 설계 목표</h2>
<ul>
<li><code>1인 1 IP 제공</code><ul>
<li>MetalLB 의 IP 풀에서 테넌트당 하나의 외부 IP를 고정적으로 할당</li>
</ul>
</li>
<li><code>SSH 통로 개방</code><ul>
<li>80/443 포트가 아닌, SSH용 22번 포트를 외부로 노출하여 사용자가 터미널</li>
</ul>
</li>
<li><code>추상화 제거</code><ul>
<li>사용자는 본인의ㅏ 파드가 쿠버네티스 위에 있는지 몰라도 되며, 오직 부여받은 IP로만 소통<ul>
<li>독립된 서버 경험 제공</li>
<li>고정성(Persistence) - 파드가 재시작되거나 노드가 바뀌어도 사용자가 할당 받은 IP와 접속 포트는 변하지 않는 고정성 제공</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="1-2-필요한-기술적-장치">1-2. 필요한 기술적 장치</h2>
<ul>
<li><code>MetalLB (L2 Mode)</code><ul>
<li>별도의 라우터 설정 없이 ARP 응답 대행을 통해 가상 IP를 물리 네트워크에 바인딩 하는 엔진</li>
<li>클라우드의 LoadBalancer 서비스를 집 안의 홈랩 클러스터로 가져오는 핵심!!</li>
</ul>
</li>
<li><code>Service (Type: LoadBalancer)</code><ul>
<li>파드의 22번 포트를 <code>MetalLB</code>가 선점한 <code>사설 IP의 22번 포트</code>와 1:1로 매핑</li>
</ul>
</li>
<li><code>공유기 포트포워딩(Port Forwarding)</code><ul>
<li>외부 공인 IP의 특정 포트로 들어온 요청을 MetalLB가 할당한 사설 IP의 22번 포트로 전달하는 이정표</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h2 id="📌-metallb-란">📌 MetalLB 란?</h2>
<blockquote>
</blockquote>
<ul>
<li>퍼블릭 클라우드에만 존재하는 LoadBalancer 기능을 일반 베어메탈 환경에서도 사용할 수 있게 해주는 네트워크 구현체<blockquote>
</blockquote>
<h3 id="✅-왜-사용하는가">✅ 왜 사용하는가??</h3>
<blockquote>
</blockquote>
</li>
<li>쿠버네티스에서 서비스의 <code>type: LoadBalancer</code> 는 원래 클라우드 환경을 위해 설계되었다고 합니다.<ul>
<li><code>클라우드</code>: LoadBalancer 를 생성하면 CSP가 자동으로 외부 IP를 할당하고 로드밸러서를 대신 띄워줌</li>
<li><code>홈랩</code> : 기본적으로 쿠버네티스는 외부 IP를 줄 수 있는 능력이 없다. 그래서 아무리 설정헤도 외부 IP가 <code>&lt;pending&gt;</code> 상태에 머물게 됨..</li>
<li>→ <code>해결책</code> : MetalLB 가 클러스터 내에서 가상의 로드밸런서 역할을 수행하며 외부 IP를 직접 할당하고 관리함<blockquote>
</blockquote>
<h3 id="✅-주요-작동-모드-l2-vs-bgp">✅ 주요 작동 모드: L2 vs BGP</h3>
<blockquote>
</blockquote>
(현재 사용 중인 모드는 L2)<blockquote>
</blockquote>
<code>L2 모드 (Layer 2)</code><blockquote>
</blockquote>
</li>
</ul>
</li>
<li>MetalLB가 클러스터 내에서 리더 노드(마스터 노드가 아닌, 특정 외부 IP를 책임지는 통신 대표!)를 선출하고, 해당 노드가 네트워크의 ARP 요청에 대신 응답하여 트래픽을 독점적으로 수신한 뒤 실제 파드로 전달하는 방식</li>
<li>장점: 특별한 네트워크 장비(관리형 스위치 등)가 필요 없다. 일반 가정 공유기에서도 잘 작동한다.(<del>홈랩의 한계 == 가정용 공유기/네트워크,,,ㅜ</del>)</li>
<li>단점: 특정 시점에 트래픽이 한 대의 노드로만 몰릴 수 있기 때문에 대역폭의 한계가 있을 수 있음<blockquote>
</blockquote>
<code>BGP 모드 (Layer 3)</code><blockquote>
</blockquote>
(데이터센터 급(?)의 설정을 할 때 사용한다고 합니다.)<blockquote>
</blockquote>
</li>
<li>원리: 라우터와 클러스터가 BGP 프로토콜로 직접 통신하며 경로를 설정함</li>
<li>장점: 여러 노드로 트래픽을 분산시킬 수 있음</li>
<li>단점: BGP를 지원하는 고가의 라우터 장비가 필요함<blockquote>
</blockquote>
<h3 id="✅-핵심-구성-요소">✅ 핵심 구성 요소</h3>
<blockquote>
</blockquote>
</li>
<li><code>Controller</code> : IP 할당을 담당. 서비스가 생성되면 기존에 설정한 <code>IPAddressPool</code> 에서 남는 IP를 찾아 서비스에 매칭시킴</li>
<li><code>Speaker</code> : 네트워크 통신을 담당. L2 모드에서는 각 노드에서 실행되며 <code>광고(Announce)</code>를 수행함.<blockquote>
</blockquote>
<h3 id="⚠️-주의사항">⚠️ 주의사항</h3>
<blockquote>
</blockquote>
</li>
<li>할당하려는 IP 대역은 반드시 공유기의 DHCP 할당 범위와 겹치지 않아야 함!!! (IP 충돌 방지)</li>
</ul>
<h1 id="2-기능-개발-metallb-설치-및-전용-ip-부여">2. [기능 개발] MetalLB 설치 및 전용 IP 부여</h1>
<h2 id="2-1-metallb-설치-컨트롤러-배포">2-1. MetalLB 설치 (컨트롤러 배포)</h2>
<ul>
<li>IP를 관리하고 ARP 응답을 대신해 줄 MetalLB 시스템을 클러스터에 올림</li>
</ul>
<pre><code class="language-bash"># MetalLB 네이티브 매니페스트 적용
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml

# 설치 확인
kubectl get pods -n metallb-system</code></pre>
<h2 id="2-2-ip-주소-범위-지정-ipaddresspool">2-2. IP 주소 범위 지정 (IPAddressPool)</h2>
<ul>
<li><code>metallb-config.yaml</code></li>
</ul>
<pre><code class="language-yaml">apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: tenant-ip-pool
  namespace: metallb-system
spec:
  # 공유기 대역 내에서 안전하게 비어있는 구간 50개 설정
  addresses:
  - 192.168.55.150-192.168.55.199
  # 자동으로 꺼내 쓸 수 있도록 설정
  autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-adv
  namespace: metallb-system
spec:
  # 위에서 만든 pool을 실제로 네트워크에 광고(Announce)함
  ipAddressPools:
  - tenant-ip-pool</code></pre>
<h2 id="2-3-테넌트용-ssh-서비스-생성-external-access">2-3. 테넌트용 SSH 서비스 생성 (External Access)</h2>
<ul>
<li>테넌트 파드를 외부 IP와 연결</li>
<li>테넌트가 생성될 때마다 하나씩 만들어 줌</li>
<li><code>tenant-alpha-ssh-service.yaml</code></li>
</ul>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: external-access
  namespace: tenant-alpha
spec:
  selector:
    role: server
  ports:
  - protocol: TCP
    port: 22
    targetPort: 22
  type: LoadBalancer</code></pre>
<h2 id="2-4-포트포워딩">2-4. 포트포워딩</h2>
<blockquote>
<p>네트워크 관리자 페이지에서 테넌트 서버의 IP로 사용할 주소를 사전에 포트포워딩 해줌</p>
</blockquote>
<h1 id="3-검증-테스트-자동-할당-및-ssh-접속-확인">3. [검증 테스트] 자동 할당 및 SSH 접속 확인</h1>
<h2 id="3-1-ip-할당-확인">3-1. IP 할당 확인</h2>
<pre><code class="language-bash">kubectl get svc -n tenant-alpha</code></pre>
<ul>
<li>→ <code>EXTERNAL -IP</code> 항목이 <code>&lt;pending&gt;</code> 에서 <code>192.168.55.150</code> (첫 번째 IP) 으로 바뀌면 성공</li>
</ul>
<h3 id="flow-정리">Flow 정리</h3>
<blockquote>
<p>트래픽 경로)
<code>외부 클라이언트</code> → <code>공유기(포트포워딩)</code> → <code>MetalLB 할당 IP</code> → <code>Service(LB)</code> → <code>Pod</code></p>
</blockquote>
<table>
<thead>
<tr>
<th>구분</th>
<th>명칭</th>
<th>예시</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>접속 IP</td>
<td>공인 IP (Public)</td>
<td>218.x.x.x</td>
<td>외부 인터넷에서 우리 집을 찾는 주소</td>
</tr>
<tr>
<td>접속 포트</td>
<td>외부 포트 (Outer)</td>
<td>50111</td>
<td>공유기 대문에서 테넌트를 구분하는 포트 번호</td>
</tr>
<tr>
<td>내부 IP</td>
<td>MetalLB 할당 IP</td>
<td>192.168.x.x</td>
<td>클러스터가 외부 네트워크와 소통하는 사설 주소</td>
</tr>
<tr>
<td>내부 포트</td>
<td>서비스 포트 (Port)</td>
<td>22</td>
<td>MetalLB가 외부에 노출한 포트</td>
</tr>
<tr>
<td>타겟 포트</td>
<td>파드 포트 (Target)</td>
<td>22</td>
<td>실제 파드 내부의 SSH 서비스 번호</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (7) Zero-Trust Network Policy - 2]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-7-Zero-Trust-Network-Policy-2</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-7-Zero-Trust-Network-Policy-2</guid>
            <pubDate>Sun, 08 Mar 2026 03:42:18 GMT</pubDate>
            <description><![CDATA[<hr>
<h1 id="1-기능-설명-egress-제어와-dns-화이트리스팅">1. [기능 설명] Egress 제어와 DNS 화이트리스팅</h1>
<h2 id="1-1-주요-설계-목표">1-1. 주요 설계 목표</h2>
<ul>
<li><code>Egress All Deny</code> - 나가는 문(Egress)을 모두 잠그기<ul>
<li>쿠버네티스의 기본 설정은 모든 파드가 외부와 자유롭게 통신할 수 있는 ‘Open’ 상태임/</li>
<li>테넌트 환경에서는 보안 사고 방지를 위해 <code>화이트리스트</code> 전략이 필수임</li>
</ul>
</li>
<li><code>DNS 허용 (Essential)</code> - 이름(DNS)을 찾는 통로만 개방<ul>
<li>인터넷을 막더라도 클러스터 내부의 서비스끼리 통신하거나, 외부 주소의 IP를 알아내기 위해서는 <code>CoreDNS</code> 와의 통신이 반드시 필요함<ul>
<li><strong>[왜 DNS만 허용을 하는가?]</strong><ul>
<li>모든 트래픽을 막으면 파드 내부에서 apt-get 이나 curl 명령을 입력했을 때 도메인 해석부터 실패하여 서비스 운영이 불가능해짐</li>
</ul>
</li>
<li><strong>[최소 권한의 원칙]</strong><ul>
<li>무조건적인 통신이 아니라, 특정 포트<code>[UDP/TCP 53]</code> 만 개방하는 것이 핵심</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><code>특정 외부망 허용</code> - 신뢰할 수 있는 통로만 선별<ul>
<li>모든 인터넷을 차단하더라도, 특정 업무를 위해 반드시 필요한 외부 API나 업데이트 서버는 열어주어야 함</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h2 id="📌-ingress--egress-란">📌 Ingress &amp; Egress 란?</h2>
<blockquote>
</blockquote>
<h3 id="ingress-안으로-들어오는-길">Ingress: 안으로 들어오는 길</h3>
<blockquote>
</blockquote>
<ul>
<li>클러스터 외부에서 내부의 서비스(파드)로 <code>들어오는 트래픽</code>을 의미</li>
<li><code>[Ingress Policy]</code><ul>
<li>어떤 외부 IP가 우리 서버에 접속할 수 있는지 제어</li>
<li>특정 포트만 열어두고 나머지는 닫는 설정을 함</li>
</ul>
</li>
<li>설정 예시<ul>
<li>NodePort, LoadBalancer, Ingress<blockquote>
</blockquote>
<h3 id="egress-밖으로-나가는-길">Egress: 밖으로 나가는 길</h3>
<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>클러스터 내부의 파드에서 외부(다른 노드, 다른 네임스페이스, 혹은 인터넷)로 <code>나가는 트래픽</code>을 의미</li>
<li><code>[Egress Policy]</code><ul>
<li>Zero-Trust 의 핵심: 허락 없이 외부로 데이터를 보내는 것을 막음</li>
<li>기본적으로 모든 출구를 막고(All Deny), 꼭 필요한 통로만 열어줌</li>
</ul>
</li>
<li>설정 예시<ul>
<li>NetworkPolicy (Egress rule)<blockquote>
</blockquote>
<h3 id="egress-의-중요-포인트">Egress 의 중요 포인트</h3>
<blockquote>
</blockquote>
</li>
</ul>
</li>
<li>공격자의 경로 차단</li>
<li>내부 전파 방지(Lateral Movement)</li>
<li>네트워크 비용 및 리소스 낭비 방지</li>
</ul>
<h1 id="2-기능-개발-egress-격리-정책-작성">2. [기능 개발] Egress 격리 정책 작성</h1>
<ul>
<li><code>tenant-alpha</code> 내의 모든 파드가 오직 <code>DNS</code> 만 가능하고 나머지는 모두 차단되도록 설정</li>
</ul>
<h2 id="2-1-egress-격리-정의-tenant-egress-policyyaml">2-1. Egress 격리 정의 (tenant-egress-policy.yaml)</h2>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-and-deny-egress
  namespace: tenant-alpha
spec:
  podSelector: {} # 테넌트 내 모든 파드에 적용
  policyTypes:
  - Egress
  egress:
  # 1. 클러스터 내부 DNS(CoreDNS) 통신 허용
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53</code></pre>
<h1 id="3-검증-테스트-차단-및-허용-확인">3. [검증 테스트] 차단 및 허용 확인</h1>
<h2 id="3-1-정책-적용">3-1. 정책 적용</h2>
<pre><code class="language-bash">kubectl apply -f tenant-egress-policy.yaml</code></pre>
<h2 id="3-2-외부-인터넷-차단-확인-egress-deny-테스트">3-2. 외부 인터넷 차단 확인 (Egress Deny 테스트)</h2>
<p>시나리오: <code>tenant-alpha</code> 내부 파드에서 8.8.8.8 로 직접 통신을 시도</p>
<pre><code class="language-bash">kubectl exec local-tester -n tenant-alpha -- nc -zv 8.8.8.8 53 -w 2</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/d63e9fa9-5d1a-4b0d-a473-874b6ebb0123/image.png" alt=""></p>
<ul>
<li>결과 → <code>nc: 8.8.8.8 (8.8.8.8:53): Connection timed out</code> 출력</li>
</ul>
<h2 id="3-3-내부-dns-해석-확인-화이트리스트-테스트">3-3. 내부 DNS 해석 확인 (화이트리스트 테스트)</h2>
<p>시나리오: 외부 서버의 IP는 몰라도 DNS lookup은 성공해야 함</p>
<pre><code class="language-bash">kubectl exec local-tester -n tenant-alpha -- nslookup google.com</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/dce5c831-2c8c-40cd-adea-2b7604970ede/image.png" alt=""></p>
<ul>
<li>결과 → 구글의 IP 주소들이 정상적으로 출력</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (6) CNI 설정 - Calico 사용
]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-6-CNI-%EC%84%A4%EC%A0%95-Calico-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-6-CNI-%EC%84%A4%EC%A0%95-Calico-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Fri, 06 Mar 2026 09:29:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스팅에서 Network Policy 적용을 위해 Calico를 설치하는 과정 중, 기존에 사용하던 Flannel 과 충돌하여 파드 생성에 실패하는 오류가 발생했습니다.
이번 포스팅에서는 CNI의 개념을 다시 한 번 짚어보고, 구체적인 문제 원인 파악과 함께 Calico 로의 마이그레이션 과정을 다시 한 번 정리해보겠습니다..(ㅜ…)</p>
</blockquote>
<hr>
<h1 id="📌-쿠버네티스-네트워크의-심장-cnicontainer-network-interface">📌 쿠버네티스 네트워크의 심장, CNI(Container Network Interface)</h1>
<h2 id="cni란"><code>CNI란?</code></h2>
<ul>
<li>쿠버네티스는 자체적으로 파드 간의 네트워크를 구축하는 기능을 가지고 있지 않음</li>
<li>대신 <code>CNI</code> 라는 표준 규격을 만들어 놓고, 네트워크 구현은 플러그인에게 맡김</li>
<li><code>핵심 역할</code> : 파드가 생성될 때 가상 네트워크 인터페이스를 할당하고, 고유한 IP를 부여하며, 파드 간 통신 경로(Router)를 생성</li>
</ul>
<h1 id="⚠️-flannel-에서-에러가-발생한-이유">⚠️ Flannel 에서 에러가 발생한 이유</h1>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/f22fb191-564f-4356-a0bf-87b5b8d68f2a/image.png" alt=""></p>
<pre><code class="language-bash">Warning  FailedCreatePodSandBox  49m                    kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox &quot;7a46363082ddde997807d803e54195e8a5b6d3fc96eab219318c78e6bb6aa93c&quot;: plugin type=&quot;flannel&quot; failed (add): failed to load flannel &#39;subnet.env&#39; file: open /run/flannel/subnet.env: no such file or directory. Check the flannel pod log for this node.</code></pre>
<ul>
<li><code>/run/flannel/subnet.env</code> 파일이 없다는 에러 발생…!</li>
</ul>
<h3 id="상태-불일치"><code>상태 불일치</code></h3>
<ul>
<li>Flannel 설정 파일은 노드에 남아있는데, 정작 네트워크 일꾼 역할을 하는 Flannel Pod는 클러스터에 배포되지 않았거나 삭제된 상태</li>
<li>Kubelet 관점) CNI 설정 파일은 존재하기 때문에 Flannel을 호출하지만, 정작 일을 해야 할 Flannel 프로세스가 없어 발생하는 MissMatch 상황..!</li>
</ul>
<h3 id="sandbox-생성-실패"><code>Sandbox 생성 실패</code></h3>
<ul>
<li>쿠버네티스는 파드를 만들기 전 Sandbox를 먼저 만드는데, CNI가 응답하지 않으니 파드 생성 자체가 멈춘 상황</li>
</ul>
<h3 id="격리-기능의-한계"><code>격리 기능의 한계</code></h3>
<ul>
<li>Flannel은 단순 통신에는 강하지만, 현재 작업 중인 <code>Network Policy(특정 네임스페이스 차단)</code> 기능을 지원하지 않음.<ul>
<li>설계 자체가 통로만 뚫어주는 구조이기 때문</li>
</ul>
</li>
</ul>
<h1 id="cni-도구-비교-flannel-vs-calico-vs-cilium">CNI 도구 비교: Flannel vs Calico vs Cilium</h1>
<table>
<thead>
<tr>
<th></th>
<th>Flannel</th>
<th>Calico</th>
<th>Cilium</th>
</tr>
</thead>
<tbody><tr>
<td>핵심 기술</td>
<td>VXLAN (터널링)</td>
<td>IPTables / BGP</td>
<td>eBPF</td>
</tr>
<tr>
<td>보안 정책</td>
<td>불가능 (차단 안 됨)</td>
<td>강력함 (L3/L4 격리)</td>
<td>매우 강력함 (L7/API 격리 가능)</td>
</tr>
<tr>
<td>복잡도</td>
<td>매우 낮음</td>
<td>중간</td>
<td>높음</td>
</tr>
<tr>
<td>적절한 상황</td>
<td>단순 학습용</td>
<td>멀티 테넌시/엔터프라이즈</td>
<td>고성능/대규모/관제 중심</td>
</tr>
</tbody></table>
<blockquote>
<p>현재 프로젝트에서는 <code>Calico</code>를 사용해보겠습니다.</p>
</blockquote>
<h1 id="calico-cni-설치-및-전환">Calico CNI 설치 및 전환</h1>
<ul>
<li>기존의 꼬인 설정(Flannel 관련)을 삭제하고 새롭게 네트워크를 구축하는 과정</li>
</ul>
<h2 id="1-구형-flannel-설정-제거">1. 구형 Flannel 설정 제거</h2>
<pre><code class="language-bash"># 모든 노드(Master, Worker)에서 실행
sudo rm /etc/cni/net.d/10-flannel.conflist</code></pre>
<h2 id="2-calico-operator">2. Calico Operator</h2>
<ul>
<li><code>Operator</code>는 Calico의 컴포넌트들을 자동으로 관리해주는 네트워크 관리자 파드</li>
</ul>
<pre><code class="language-bash">kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml</code></pre>
<h2 id="3-클러스터-맞춤형-자원cr-배포">3. 클러스터 맞춤형 자원(CR) 배포</h2>
<ul>
<li>클러스터의 <code>Pod CIDR(주소 대역)</code> 에 맞춰 설정을 주입해야 함.</li>
<li>현재 라즈베리파이 홈랩에서는 <code>10.244.0.0/16</code> 을 사용</li>
</ul>
<pre><code class="language-bash">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    ipPools:
    - blockSize: 26
      cidr: 10.244.0.0/16
      encapsulation: VXLANCrossSubnet
      natOutgoing: Enabled
      nodeSelector: all()
EOF</code></pre>
<blockquote>
</blockquote>
<h2 id="📌-calico-인데-vxlan을-사용하는-이유">📌 Calico 인데 VXLAN을 사용하는 이유?</h2>
<blockquote>
</blockquote>
<p>Calico 는 기본적으로 BGP 라우팅을 권장하지만, 네트워크 환경에 따라 VXLAN 터널링 모드도 완벽하게 지원한다고 합니다.</p>
<blockquote>
</blockquote>
<p>설정의 편의성과 범용성을 고려하여 VXLAN 모드를 채택했습니다. VXLAN 방식을 사용하더라도, Calico를 사용하는 가장 큰 목적인 ‘L3/L4 Network Policy’ 기능은 그대로 활용할 수 있기에 적용해보았습니다.</p>
<blockquote>
</blockquote>
<p>(VXLAN, BGP, eBPF 에 대한 자세한 내용은 학습 후 정리해봐야겠습니다…. <del>넘 어렵습니다…ㅜ</del>)</p>
<h1 id="zero-trust-네트워크-격리-테스트">Zero-Trust 네트워크 격리 테스트</h1>
<ul>
<li>허가받은 내부 통신은 허용하고, 허가받지 않은 외부 접근은 차단하는지 검증</li>
</ul>
<h2 id="1-테스트-환경-준비">1. 테스트 환경 준비</h2>
<ul>
<li>네트워크 교체(Calico) 과정에서 파드 재시작이 되었을 수 있으니 현재 타겟 파드의 IP를 변수에 저장</li>
</ul>
<pre><code class="language-bash">TARGET_IP=$(kubectl get pod limit-test-pod -n tenant-alpha -o jsonpath=&#39;{.status.podIP}&#39;)

# 확인
echo &quot;Target Pod IP: $TARGET_IP&quot;</code></pre>
<h2 id="2-case-1-내부-통신-테스트-intra-namespace">2. [Case 1] 내부 통신 테스트 (Intra-Namespace)</h2>
<ul>
<li>시나리오: 같은 네임스페이스(현재 <code>tenant-alpha</code>)에 속한 파드끼리는 통신이 가능해야 함</li>
</ul>
<pre><code class="language-bash">kubectl exec local-tester -n tenant-alpha -- wget -qO- --timeout=2 $TARGET_IP</code></pre>
<ul>
<li>결과</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/3a0c91b9-ea35-44c7-a9cb-a8fc162a7c00/image.png" alt=""></p>
<ul>
<li>이전에 작성해둔 Nginx 의 Welcome HTML 소스코드가 출력됨 (→ 정상 작동)</li>
<li><code>NetworkPolicy</code> 의 <code>from.podSelector: {}</code> 규칙이 동일 네임스페이스 내의 트래픽을 <code>화이트리스트</code>로 승인</li>
</ul>
<h2 id="3-case-2-외부-침입-테스트-inter-namespace">3. [Case 2] 외부 침입 테스트 (Inter-Namespace)</h2>
<ul>
<li>시나리오: 다른 네임스페이스(<code>default</code>)에서 <code>tenant-alpha</code> 로의 접근은 차단되어야 함</li>
</ul>
<pre><code class="language-bash">kubectl run intruder -n default --image=busybox --restart=Never -it --rm -- wget -qO- --timeout=5 $TARGET_IP</code></pre>
<ul>
<li>결과</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/854174de-69c9-4be6-9b86-cee54e8fdfc8/image.png" alt=""></p>
<ul>
<li><code>wget: download timed out</code> (→ 정상 작동)</li>
<li>정책에 정의되지 않은 외부 네임스페이스의 패킷을 Calico가 생성한 iptables 규칙에 의해 패킷이 커널 레벨에서 Drop 됨</li>
</ul>
<p>.</p>
<p>.</p>
<p>.</p>
<p>→ Calico 설치 완료! 😋</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (5) Zero-Trust Network Policy - 1]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-5-Zero-Trust-Network-Policy-1</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-5-Zero-Trust-Network-Policy-1</guid>
            <pubDate>Fri, 06 Mar 2026 08:12:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<ul>
<li>현재 클러스터는 자원은 분리되어 있지만, 네트워크는 여전히 모두가 모두와 대화할 수 있는 상태</li>
</ul>
</blockquote>
<ul>
<li>이를 해결하기 위해 <code>Zero-Trust 기반 Network Policy 설정</code>을 진행</li>
</ul>
<hr>
<h1 id="1-기능-설명-network-isolation">1. [기능 설명] Network Isolation</h1>
<ul>
<li>쿠버네티스의 기본 네트워크 구조는 <code>Flat Network</code><ul>
<li>네임스페이스가 달라도 서로의 IP만 알면 통신이 가능한 구조</li>
</ul>
</li>
<li><code>Network Policy</code> 는 파드로 드나드는 트래픽을 제어하는 L3/L4 방화벽 역할을 수행</li>
</ul>
<h2 id="1-1-주요-설계-목표">1-1. 주요 설계 목표</h2>
<ul>
<li><code>테넌트 간 통신 차단</code> : <code>tenant-alpha</code> 네임스페이스에 속한 파드들은 다른 테넌트 네임스페이스의 파드와 통신할 수 없어야 함</li>
<li><code>내부 통신만 허용 (Intra-Namespace)</code> : 같은 네임스페이스 안에 있는 파드들끼리는 자유롭게 데이터를 주고받을 수 있게 함</li>
</ul>
<blockquote>
<p>구조가 헷갈릴 수 있으나 정리를 하자면,
외부 트래픽은 막고(테넌트 간 통신 차단) - 내부 파드끼리는 통신 가능한(내부 통신만 허용) 구조입니다.</p>
</blockquote>
<h2 id="1-2-필요한-쿠버네티스-기술-객체">1-2. 필요한 쿠버네티스 기술 객체</h2>
<h3 id="networkpolicy"><code>NetworkPolicy</code></h3>
<ul>
<li>어떤 트래픽을 허용(<code>Allow</code>) 할지 정의하는 규칙<ul>
<li>쿠버네티스 네트워크 정책은 기본적으로 <code>허용되지 않은 모든 것은 거부(Deny)</code> 하는 방식으로 작동함 → <code>Default Deny 모델</code></li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h3 id="📌-cnicoontainer-network-interface-호환성">📌 CNI(Coontainer Network Interface) 호환성</h3>
<blockquote>
</blockquote>
<ul>
<li>설정이 실제로 동작하려면 클러스터에 Network Policy 를 지원하는 CNI 가 설치되어 있어야 함</li>
<li><code>확인 사항</code> : 라즈베리파이에서 주로 사용하는 <code>Flannel</code>(현재 사용 중)은 기본적으로 Network Policy를 지원하지 않음..<ul>
<li><code>Calico</code>나 <code>Cilium</code>, 또는 Flannel 위에 <code>Canal</code>을 얹어서 사용 중이라면 정책이 즉시 반영됨.</li>
</ul>
</li>
<li><code>동작 원리</code> : 정책을 적용하는 순간, 해당 파드와 연결된 가상 인터페이스에 <code>iptables</code> 혹은 <code>eBPF</code> 규칙이 생성되어 패킷을 필터링 함</li>
<li>-&gt; 우선 <code>Flannel</code> 에서 <code>Calico</code> 로 CNI 설정을 변경하도록 하겠습니다.</li>
</ul>
<h1 id="2-기능-개발-테넌트-간-격리-정책-작성">2. [기능 개발] 테넌트 간 격리 정책 작성</h1>
<ul>
<li><code>tenant-alpha</code> 에 동일 네임스페이스가 아니면 다 차단하는 정책을 적용</li>
</ul>
<h2 id="2-1-네임스페이스-레이블링">2-1. 네임스페이스 레이블링</h2>
<ul>
<li>네트워크 정책에서 네임스페이스를 구분하려면 각 네임스페이스에 <code>Label</code>(이름표 역할)이 붙어 있어야 함</li>
</ul>
<pre><code class="language-bash"># tenant-alpha 네임스페이스에 label 부착
kubectl label namespace tenant-alpha ns-name=tenant-alpha</code></pre>
<h2 id="2-2-네트워크-격리-정의-tenant-network-policyyaml">2-2. 네트워크 격리 정의 (tenant-network-policy.yaml)</h2>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-other-namespaces
  namespace: tenant-alpha
spec:
  podSelector: {} # {}는 이 네임스페이스 내의 모든 파드를 의미함
  policyTypes:
  - Ingress # 들어오는 트래픽 제어
  ingress:
  - from:
    - podSelector: {} # 동일 네임스페이스 내의 모든 파드로부터의 접속 허용
    - namespaceSelector:
        matchLabels:
          ns-name: tenant-alpha # 본인의 네임스페이스에서 오는 것만 허용</code></pre>
<ul>
<li><code>podSelector: {}</code> 만 단독으로 있으면 ‘해당 네임스페이스 내의 모든 파드’를 의미</li>
</ul>
<h1 id="3-검증-테스트-차단-확인-절차">3. [검증 테스트] 차단 확인 절차</h1>
<h2 id="3-1-정책-적용">3-1. 정책 적용</h2>
<pre><code class="language-bash">kubectl apply -f tenant-network-policy.yaml</code></pre>
<h2 id="3-2-테넌트-내-통신-intra-namespace">3-2. 테넌트 내 통신 (Intra-Namespace)</h2>
<ul>
<li>시나리오: <code>tenant-alpha</code> 네임스페이스 내부의 <code>local-tester</code> 파드에서 웹 서버(<code>limit-test-pod</code>)로 접속을 시도</li>
</ul>
<pre><code class="language-bash"># 1. 대상 파드의 최신 IP 확인
TARGET_IP=$(kubectl get pod limit-test-pod -n tenant-alpha -o jsonpath=&#39;{.status.podIP}&#39;)

# 2. 내부 통신 시도 (성공 시 HTML 코드 출력)
kubectl exec local-tester -n tenant-alpha -- wget -qO- --timeout=2 $TARGET_IP</code></pre>
<p>→ Nginx의 Welcome 메시지가 담긴 HTML 소스코드가 출력</p>
<h2 id="3-3-테넌트-간-통신-inter-namespace--침입-테스트">3-3. 테넌트 간 통신 (Inter-Namespace / 침입 테스트)</h2>
<ul>
<li>시나리오: <code>default</code> 네임스페이스에 임시 파드를 생성하여 <code>tenant-alpha</code> 내부의 웹 서버로 무단 접속을 시도</li>
</ul>
<pre><code class="language-bash"># 외부(default NS)에서 침입 시도 (차단 시 Timeout 발생)
kubectl run intruder -n default --image=busybox --restart=Never -it --rm -- wget -qO- --timeout=5 $TARGET_IP</code></pre>
<p>→ <code>wget: download timed out</code> 메시지 출력 (격리 성공!)</p>
<blockquote>
<p><code>Connection Refused</code> 가 아닌 <code>Timeout</code> 이 발생하는 이유는, Network Policy 가 패킷이 들어오는 단계에서 조용히 드롭시키기 때문이다..</p>
</blockquote>
<blockquote>
</blockquote>
<h2 id="📌-connection-refused-vs-timeout">📌 Connection Refused vs. Timeout</h2>
<blockquote>
</blockquote>
<ul>
<li><code>Connection Refused</code><ul>
<li>서버까지 패킷은 도달했으나, 포트가 닫혀 있어 운영체제가 거절 응답(RST)을 보낸 경우</li>
</ul>
</li>
<li><code>Timeout</code><ul>
<li>패킷이 방화벽(Network Policy)에 의해 입구에서 소멸(Drop)되어 서버가 패킷의 존재조차 모르는 상태.</li>
<li>Zero-Trust 관점에서는 내부 정보를 일절 노출하지 않는 이 방식이 더 안전하다..</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (4) Dynamic Resource Quota - CPU/Memory
]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-4-Dynamic-Resource-Quota-CPUMemory</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-4-Dynamic-Resource-Quota-CPUMemory</guid>
            <pubDate>Fri, 06 Mar 2026 03:34:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난번에 스토리지에 대한 제한 및 격리를 진행했습니다. 이번에는 CPU와 RAM에 대한 제한과 격리 작업을 진행해보겠습니다.</p>
</blockquote>
<hr>
<h1 id="1-기능-설명-cpuram-격리-및-할당">1. [기능 설명] CPU/RAM 격리 및 할당</h1>
<ul>
<li>테넌트가 생성하는 파드들이 사용하는 물리적인 연산 자원(CPU/RAM)을 제한하는 기능</li>
</ul>
<h2 id="1-1-주요-설계-목표">1-1. 주요 설계 목표</h2>
<ul>
<li><code>안전한 멀티 테넌시</code> : 특정 테넌트의 코드가 무한 루프에 빠져 CPU를 점유하거나, 메모리 누수가 발생해도 다른 테넌트와 마스터 노드의 동작에 영향을 주지 않아야 함</li>
<li><code>사양 정의</code> : t2.micro 인스턴트처럼 1 vCPU, 1GiB RAM 수준의 리소스 프로파일을 테넌트 네임스페이스에 강제함</li>
</ul>
<h2 id="1-2-필요한-쿠버네티스-기술-객체">1-2. 필요한 쿠버네티스 기술 객체</h2>
<h3 id="resourcequota-총량"><code>ResourceQuota (총량)</code></h3>
<ul>
<li>네임스페이스 전체에서 돌아가는 모든 파드들의 자원 합계를 제한함<ul>
<li>→ 비유: 지갑(네임스페이스)의 총액(네임스페이스가 갖는 리소스) 제한</li>
</ul>
</li>
</ul>
<h3 id="limitrange-개별"><code>LimitRange (개별)</code></h3>
<ul>
<li>파드 하나가 가질 수 있는 최소/최대 자원 크기를 결정</li>
<li>테넌트가 자원 설정을 누락하고 파드를 띄울 때 자동으로 적용될 기본값을 부여<ul>
<li>→ 비유: 지갑(네임스페이스) 안에서 꺼내 쓰는 지폐 한 장(파드가 갖는 리소스)의 단위 제한</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h3 id="📌-자원-제한의-기준-requests-와-limits">📌 자원 제한의 기준 Requests 와 Limits</h3>
<blockquote>
<p>💡 라즈베리파이는 저전력 장치이기에,, 특정 테넌트에서 OOM이 발생하게 될 경우 노드 전체가 멈추는 Freeze 상황에 놓이기 쉽습니다. 때문에 <code>limits.memory</code> 설정이 필수적입니다!</p>
</blockquote>
<ul>
<li><code>Requests (보장량)</code><ul>
<li>파드가 실행되기 위해 최소한으로 필요한 자원</li>
<li>K8s 스케줄러는 노드에 보장량 만큼의 여유 공간이 있을 때만 파드를 배치함<blockquote>
</blockquote>
</li>
</ul>
</li>
<li><code>Limits (상한선)</code><ul>
<li>파드가 최대로 사용할 수 있는 자원</li>
<li><code>CPU</code>: Limit을 넘으면 스로틀링(Throttling)이 발생하여 속도가 느려지지만 파드가 죽지는 않음</li>
<li><code>Memory</code>: Limit 을  넘으면 커널이 파드를 즉시 종료시킴.(OOM Kill)<ul>
<li>→ Memory 는 압축할 수 없는 자원이기 때문</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="2-기능-개발-연산-자원-쿼터-및-제한-매니페스트-작성">2. [기능 개발] 연산 자원 쿼터 및 제한 매니페스트 작성</h1>
<h2 id="2-1-연산-자원-총량-정의-tenant-compute-quotayaml">2-1. 연산 자원 총량 정의 (tenant-compute-quota.yaml)</h2>
<ul>
<li>네임스페이스 전체의 장부 역할</li>
</ul>
<pre><code class="language-yaml">apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: tenant-alpha
spec:
  hard:
    requests.cpu: &quot;500m&quot;      # 0.5 코어 보장
    requests.memory: &quot;512Mi&quot;  # 512MiB 보장
    limits.cpu: &quot;1000m&quot;       # 최대 1 코어 (1000m = 1 vCPU)
    limits.memory: &quot;1Gi&quot;      # 최대 1GiB (t2.micro 급)</code></pre>
<h2 id="2-2-기본-규격-강제-정의-tenant-compute-limityaml">2-2. 기본 규격 강제 정의 (tenant-compute-limit.yaml)</h2>
<ul>
<li>테넌트가 자원 설정을 누락하더라도 기본값을 넣어주는 설정</li>
</ul>
<pre><code class="language-yaml">apiVersion: v1
kind: LimitRange
metadata:
  name: compute-limit-range
  namespace: tenant-alpha
spec:
  limits:
  - type: Container
    default:                 # Limit 미설정 시 자동 부여
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;
    defaultRequest:          # Request 미설정 시 자동 부여
      cpu: &quot;200m&quot;
      memory: &quot;256Mi&quot;
    max:                     # 개별 컨테이너가 가질 수 있는 최대치
      cpu: &quot;800m&quot;
      memory: &quot;800Mi&quot;</code></pre>
<h1 id="3-검증-테스트-쿼터-및-제한-적용-후-검증">3. [검증 테스트] 쿼터 및 제한 적용 후 검증</h1>
<ul>
<li>매니페스트를 적용한 후, 테넌트가 자원을 지정하지 않고 파드를 생성했을 때 관리자가 정한 기본값이 정상적으로 주입되는지 확인</li>
</ul>
<h2 id="3-1-매니페스트-적용">3-1. 매니페스트 적용</h2>
<pre><code class="language-yaml">kubectl apply -f tenant-compute-quota.yaml
kubectl apply -f tenant-compute-limit.yaml</code></pre>
<h2 id="3-2-테스트용-파드-생성-test-podyaml">3-2. 테스트용 파드 생성 (test-pod.yaml)</h2>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: limit-test-pod
  namespace: tenant-alpha
spec:
  containers:
  - name: nginx
    image: nginx</code></pre>
<ul>
<li>실행</li>
</ul>
<pre><code class="language-yaml">kubectl apply -f test-pod.yaml</code></pre>
<h2 id="3-3-주입된-값-검증">3-3. 주입된 값 검증</h2>
<pre><code class="language-yaml">kubectl get pod limit-test-pod -n tenant-alpha -o yaml</code></pre>
<h3 id="마무리">마무리</h3>
<p>이제 테넌트가 CPU와 RAM을 마음껏 사용해도(제한된 범위 내에서) 다른 테넌트에 영향을 주지 않게 되었습니다. 리소스에 대한 제한과 격리는 잘 되었습니다만, 다른 테넌트에 네트워크로 접속해서 데이터를 훔쳐보는(??) 상황은 아직 처리를 안 한 것 같습니다. 다음에는 Zero-Trust Network Policy 를 통해 네트워크 통신까지 제한하는 작업을 해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (3) Storage Quota & Isolation
]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-3-Storage-Quota-Isolation</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-3-Storage-Quota-Isolation</guid>
            <pubDate>Fri, 06 Mar 2026 02:19:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>RBAC 설정을 통해 <code>‘누가(admin/tenant)’</code> 들어올 수 있는지를 정의했습니다. 이제 각 테넌트가 사용할 수 있는 <code>물리적 자원의 크기</code>를 결정할 차례입니다.
그 중에서 라즈베리파이의 SD 카드나 외장 SSD 를 보호하기 위한 Storage Quota &amp; Isolation 을 먼저 진행해보겠습니다.</p>
</blockquote>
<hr>
<h1 id="1-기능-설명-스토리지-격리-및-용량-제한">1. [기능 설명] 스토리지 격리 및 용량 제한</h1>
<ul>
<li>특정 테넌트가 무분별하게 대용량 데이터를 저장하여 클러스터 전체의 디스크 공간(Storage)을 고갈시키는 것을 방지하는 작업</li>
</ul>
<h2 id="1-1-주요-설계-목표">1-1. 주요 설계 목표</h2>
<ul>
<li><code>총량 제한</code> : 해당 네임스페이스(<code>tenant-alpha</code>) 내에서 생성되는 모든 <code>PVC(PersistentVolumeClaim)</code> 용량의 합계를 제한</li>
<li><code>개수 제한</code> : 테넌트가 생성할 수 있는 스토리지 요청(PVC) 객체의 개수 자체를 제한하여 관리 오버헤드를 방지</li>
</ul>
<h2 id="1-2-필요한-쿠버네티스-기술-객체">1-2. 필요한 쿠버네티스 기술 객체</h2>
<h3 id="resourcequota"><code>ResourceQuota</code></h3>
<ul>
<li>CPU/RAM 뿐만 아니라 스토리지의 총합(<code>requests.storage</code>) 을 제한하는 용도로 사용</li>
</ul>
<h3 id="persistentvolumeclaim-pvc"><code>PersistentVolumeClaim (PVC)</code></h3>
<ul>
<li>사용자가 스토리지를 사용하겠다고 요청하는 예약권</li>
</ul>
<blockquote>
</blockquote>
<h2 id="📌-pv-와-pvc">📌 PV 와 PVC</h2>
<blockquote>
</blockquote>
<ul>
<li>쿠버네티스에서 스토리지는 크게 두 단계로 관리됩니다.<blockquote>
</blockquote>
<h3 id="pv-persistentvolume">PV (PersistentVolume)</h3>
<blockquote>
</blockquote>
</li>
<li>실제 물리적 스토리지(SSD)의 일부분을 쿠버네티스가 인식할 수 있는 자원 형태로 등록해둔 것 → <code>관리자 영역</code><blockquote>
</blockquote>
<h3 id="pvc-persistentvolumeclaim">PVC (PersistentVolumeClaim)</h3>
<blockquote>
</blockquote>
</li>
<li>사용자가 특정 용량과 접근 권한을 명시하여 스토리지를 할당받으려고 제출하는 요청 → <code>테넌트 영역</code><blockquote>
</blockquote>
테넌트가 PVC를 생성하면, 쿠버네티스는 테넌트의 요구사항(용량, 읽기/쓰기 모드)에 맞는 PV를 찾아서 서로 연결(<code>Binding</code>) 해줍니다.</li>
</ul>
<h3 id="storageclass"><code>StorageClass</code></h3>
<ul>
<li>어떤 종류의 저장 장치(SSD, HDD 등)를 사용할지 정의하며, 쿼터 계산의 기준이 됨</li>
</ul>
<h1 id="2-기능-개발-yaml-매니페스트-및-테스트">2. [기능 개발] YAML 매니페스트 및 테스트</h1>
<ul>
<li><code>tenant-alpha</code> 네임스페이스에 총 <code>5Gi 의 스토리지 사용량</code>을 제한하는 설정을 적용</li>
</ul>
<h2 id="2-1-스토리지-쿼터-정의-tenant-storage-quotayaml">2-1. 스토리지 쿼터 정의 (<code>tenant-storage-quota.yaml</code>)</h2>
<pre><code class="language-yaml">apiVersion: v1
kind: ResourceQuota
metadata:
  name: storage-quota
  namespace: tenant-alpha # 테넌트 알파 공간에 적용
spec:
  hard:
    requests.storage: &quot;5Gi&quot;    # 모든 PVC 용량의 합계를 5Gi로 제한
    persistentvolumeclaims: &quot;3&quot; # 생성 가능한 PVC 개수를 3개로 제한</code></pre>
<h2 id="2-2-환경-적용">2-2. 환경 적용</h2>
<pre><code class="language-yaml"># 마스터 노드에서 실행
kubectl apply -f tenant-storage-quota.yaml</code></pre>
<h2 id="2-3-검증-테스트-quota-초과-상황-재현">2-3. 검증 테스트 (Quota 초과 상황 재현)</h2>
<ul>
<li>테넌트가 할당량을 넘어서는 요청을 할 때 시스템이 어떻게 차단하는지 확인</li>
<li><code>정상 요청 (2Gi)</code><ul>
<li><code>tenant-alpha</code> 내에서 2Gi 크기의 PVC를 생성하면 정상적으로 승인</li>
</ul>
</li>
<li><code>초과 요청 (4Gi 추가)</code><ul>
<li>이미 2Gi 를 사용 중인 상태에서 4Gi를 추가 요청하면, 총합이 6Gi가 되어 설정된 5Gi를 초과하므로 API 서버에서 생성을 거부</li>
</ul>
</li>
</ul>
<pre><code class="language-bash"># 쿼터 상태 확인
kubectl describe resourcequota storage-quota -n tenant-alpha</code></pre>
<ul>
<li><code>USED</code> 열은 현재 테넌트가 사용하는 양을, <code>HARD</code> 열은 설정한 상한선을 보여줌</li>
</ul>
<h3 id="마무리">마무리</h3>
<p>이제 단순히 용량만 제한하는 것이 아니라, 네임스페이스별로 PVC를 분리함으로써 테넌트 A가 테넌트 B의 데이터를 물리적/논리적으로 들여다볼 수 없는 구조가 완성되었습니다. 다음 포스트에서는 CPU/RAM 에 대한 격리를 설정해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (2) RBAC - admin/tenant]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-2-RBAC-admintenant</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-2-RBAC-admintenant</guid>
            <pubDate>Thu, 05 Mar 2026 03:17:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>우선 제 홈랩 영역에 외부 사용자(테넌트)가 들어올 수 있기 때문에 권한 격리를 했습니다.</p>
</blockquote>
<h1 id="1-기능-설명-테넌트-인증-및-권한-격리-rbac">1. [기능 설명] 테넌트 인증 및 권한 격리 (RBAC)</h1>
<blockquote>
<p>클러스터 내에서 각 테넌트가 자신의 자원만 제어할 수 있도록 논리적 격리를 구현하는 것.(CSP 서비스에서 사용자가 본인의 인스턴스만 제어하고 타인의 정보는 볼 수 없는 환경을 쿠버네티스에서 재현하고자 함.)</p>
</blockquote>
<h2 id="1-1-주요-설계-목표">1-1. 주요 설계 목표</h2>
<ul>
<li><code>논리적 공간 분리</code> : 각 테넌트에게 전용 <strong>Namespace</strong> 를 할당하여 자원이 섞이지 않게 함</li>
<li><code>전용 계정 생성</code> : 클러스터 전체 관리자 권한이 아닌, 특정 테넌트 전용 <strong>ServiceAccount</strong> 를 발급함</li>
<li><code>권한 범위 제한</code> : <strong>RoleBinding</strong> 을 통해 해당 계정이 지정된 <strong>Namespace 내부의 리소스</strong>(Pod, Service 등)<code>만</code> 관리할 수 있도록 제한함</li>
</ul>
<h2 id="1-2-필요한-쿠버네티스-기술-객체">1-2. 필요한 쿠버네티스 기술 객체</h2>
<h3 id="namespace"><code>Namespace</code></h3>
<ul>
<li>클러스터 내의 가상 클러스터, 리소스 격리의 기본 단위</li>
</ul>
<h3 id="serviceaccount"><code>ServiceAccount</code></h3>
<ul>
<li>파드(Pod) 또는 외부 사용자(테넌트)가 쿠버네티스 API를 호출할 때 사용하는 인증 주체<ul>
<li>→ 일반 User 는 인증서 발급 등 외부 인프라 관리가 필요하지만, ServiceAccount 는 쿠버네티스 내부 리소스로서 <strong>API Token(인증 토큰)</strong> 기반으로 관리가 용이하기 떄문에 테넌트에게 즉시 권한을 부여하는 모델에 적합함.</li>
</ul>
</li>
</ul>
<h3 id="clusterrole-admin"><code>ClusterRole (admin)</code></h3>
<ul>
<li>쿠버네티스에 미리 정의된 역할로, 리소스에 대한 생성/수정/삭제 권한을 가짐</li>
</ul>
<h3 id="rolebinding"><code>RoleBinding</code></h3>
<ul>
<li>특정 ServiceAccount 를 특정 Namespace 내의 ClusterRole 과 연결하여 권한을 발효시킴</li>
</ul>
<hr>
<h1 id="2-기능-개발-yaml-매니페스트-및-테스트">2. [기능 개발] YAML 매니페스트 및 테스트</h1>
<h2 id="2-1-테넌트-격리-환경-정의">2-1. 테넌트 격리 환경 정의</h2>
<ul>
<li>tenant-setup.yaml</li>
</ul>
<pre><code class="language-yaml"># 1. 테넌트 전용 네임스페이스 생성
apiVersion: v1
kind: Namespace
metadata:
  name: tenant-alpha
---
# 2. 테넌트 전용 서비스 어카운트 생성
apiVersion: v1
kind: ServiceAccount
metadata:
  name: alpha-user
  namespace: tenant-alpha
---
# 3. 권한 연결 (RoleBinding)
# ClusterRole인 &#39;admin&#39;을 &#39;tenant-alpha&#39; 네임스페이스 내의 &#39;alpha-user&#39;에게만 매핑합니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: alpha-user-admin-binding
  namespace: tenant-alpha
subjects:
- kind: ServiceAccount
  name: alpha-user
  namespace: tenant-alpha
roleRef:
  kind: ClusterRole
  name: admin
  apiGroup: rbac.authorization.k8s.io</code></pre>
<blockquote>
</blockquote>
<h3 id="📌-rolebinding">📌 RoleBinding</h3>
<blockquote>
</blockquote>
<ul>
<li><code>누가(Subject)</code>, <code>무엇을(RoleRef)</code>, <code>어디에서(Namespace)</code> 할 수 있는지를 연결<ul>
<li><code>metadata.namespace</code><ul>
<li>권한이 효력을 발휘할 범위(Scope)를 결정</li>
<li><code>tenant-alpha</code> 를 적었기 때문에, 이 권한은 다른 네임스페이스에는 아무런 영향을 주지 않음</li>
</ul>
</li>
<li><code>subjects</code> (권한을 받는 대상)<ul>
<li><code>kind: ServiceAccount</code> : 권한을 부여할 대상의 유형</li>
<li><code>name: alpha-user</code> : 위에서 만든 서비스 어카운트의 이름을 정확히 지칭</li>
</ul>
</li>
<li><code>roleRef</code> (부여할 권한 내용)<ul>
<li><code>kind: ClusterRole</code> : 권한의 정의가 클러스터 전체 수준에서 공용으로 만들어진 것</li>
<li><code>name: admin</code> : 쿠버네티스가 기본적으로 제공하는 ‘관리자용 권한 세트’.<ul>
<li>파드 생성, 수정, 삭제, 서비스 생성 등 네임스페이스 내의 거의 모든 작업이 포함되어 있음</li>
</ul>
</li>
<li><code>apiGroup: rbac.authorization.k8s.io</code> : RBAC 기능을 담당하는 쿠버네티스 API 그룹</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="2-2-환경-적용-및-접속-토큰-발행">2-2. 환경 적용 및 접속 토큰 발행</h2>
<ul>
<li>작성한 매니페스트를 클러스터에 반영하고, 테넌트가 외부에서 API 서버에 인증할 때 사용할 Access Token을 발행</li>
</ul>
<pre><code class="language-yaml"># 매니페스트 적용
kubectl apply -f tenant-setup.yaml

# 테넌트용 토큰 발행 (보안을 고려하여 24시간 유효 기간 설정)
kubectl create token alpha-user -n tenant-alpha --duration=24h</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/38f3bb3c-7517-4aae-a77f-1c9723fc5164/image.png" alt=""></p>
<h2 id="2-3-권한-검증-impersonation-테스트">2-3. 권한 검증 (Impersonation 테스트)</h2>
<ul>
<li><p>관리자 권한을 가진 상태에서 특정 계정을 사칭(<code>--as</code>)하여, 설정한 RBAC 정책이 의도대로 동작하는지 확인</p>
</li>
<li><p>정상 동작 확인: <code>tenant-alpha</code> 네임스페이스 내의 자원 조회</p>
</li>
</ul>
<pre><code class="language-bash">kubectl get pods -n tenant-alpha --as=system:serviceaccount:tenant-alpha:alpha-user</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/2c3f1431-8c26-4116-a92e-bcdc4909ea10/image.png" alt=""></p>
<blockquote>
<p>-&gt; <code>No resources found</code> (성공 → 권한이 있어 조회가 가능하지만 생성된 파드가 없음)</p>
</blockquote>
<ul>
<li>격리 동작 확인: 관리 영역인 <code>kube-system</code> 네임스페이스 자원 조회</li>
</ul>
<pre><code class="language-bash">kubectl get pods -n kube-system --as=system:serviceaccount:tenant-alpha:alpha-user</code></pre>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/1eeb1a05-3fb2-4db1-b9f1-0273cf2e1fc9/image.png" alt=""></p>
<blockquote>
<p>-&gt; <code>Error from server (Forbidden)</code>  (성공 - 권한이 없어 접근이 차단됨)</p>
</blockquote>
<h3 id="마무리">마무리</h3>
<p>이번 작업에서는 <code>RBAC</code> 을 기반으로 관리자(나)와 테넌트(사용자)의 권한 격리를 진행해보았습니다. 다음 포스트에서는 스토리지 격리를 설정해보겠습니다.</p>
<hr>
<p>p.s) 처음 보는 쿠버네티스 개념이 너무 많은 것 같습니다. CKA 준비도 병행하고 있는데 운동 많<del>이 됩니다</del>^^</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Multi-Tenant K8s Cluster on ARM64 - (1) Intro]]></title>
            <link>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-1-Intro</link>
            <guid>https://velog.io/@pizza_loves_me/Project-Multi-Tenant-K8s-Cluster-on-ARM64-1-Intro</guid>
            <pubDate>Thu, 05 Mar 2026 02:37:38 GMT</pubDate>
            <description><![CDATA[<p>지난 <a href="https://velog.io/@pizza_loves_me/%ED%99%88%EB%9E%A9-%EA%B5%AC%EC%B6%95%EA%B8%B0-4-Kubernetes-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B5%AC%EC%B6%95">홈랩 구축기</a> 를 마무리 하며 드디어 온전한 홈랩을 갖게 되었습니다. 여기서 할 수 있는 것들이 너무나 많지만 가장 해보고 싶었던 것을 생각해보니 Google, AWS, MS 처럼 <code>CSP의 시스템을 구축</code>해보고 <code>그 역할을 수행</code>해보면 참 재밌겠다는 생각이 들었습니다. 또, 애플리케이션을 올리기 위해 만든 홈랩보다 <code>운영</code>의 관점에서 다양한 경험을 해보고 싶었기에 홈랩 프로젝트를 좀 더 고도화 하기로 결정했습니다.</p>
<h2 id="뭐-먼저-할까">뭐 먼저 할까?</h2>
<blockquote>
<p>사용자에게는 가상 인스턴스(SSH Server) 환경을 제공하고, 내부적으로는 Namespace 기반의 멀티 테넌시로 관리</p>
</blockquote>
<p>인프라는 모르는 것 투성이기에 제 지식이 클라우드 서비스 전체를 커버할 수 없겠다는 생각이 들었습니다.(<del>너무나 당연한 얘기..</del>) 초기 구축 이후에 사용자들의 피드백을 지속적으로 받고, 개선해나가며 방향성을 잡기로 했습니다. 일단 지금 놀고 있는 리소스가 넘쳐나기에,,(<code>RAM 24GB</code> + <code>CPU 12-Core</code> + <code>768GB SSD</code> - <code>K8s 구축 리소스</code> = <code>놀고 있는 리소스</code>) 이 리소스들을 사용자에게 CSP의 <code>인스턴스</code>(AWS-ec2, OCI-Instance 등)와 같이 제공을 해보면 좋겠다고 생각했습니다.</p>
<p>또, 현재 운영진으로 속해 있는 교내 개발 동아리 &#39;그리디(GREEDY)&#39; 에서 3기 멤버들이 프로젝트를 진행하고 있습니다. 4차 데모데이 요구사항 중 <code>무중단 배포</code> 와 <code>모니터링 서버</code> 가 예정되어 있는데, 이때 제 테넌트 서버가 멤버들에게 좋은 경험을 줄 수 있다고 생각했고, 4차 데모데이가 다가오기 전에 프로젝트를 1차 런칭하기로 했습니다.</p>
<h2 id="기술-스택">기술 스택</h2>
<blockquote>
<p>우선 테넌트 서버가 동작하는 홈랩의 스펙을 고려하여 라이트하게 사용할 기술 스택을 선정해봤습니다.</p>
</blockquote>
<ul>
<li><code>Infrastructure</code>: Raspberry Pi 5 (8GB) * 3, NVMe SSD</li>
<li><code>Orchestration</code>: Kubernetes v1.31 (kubeadm)</li>
<li><code>Runtime</code>: Containerd v1.6.20</li>
<li><code>CNI (Network)</code>: Cilium v1.18.5</li>
<li><code>Storage</code>: Local Path Provisioner</li>
</ul>
<h2 id="프로젝트-명세서-project-specs">프로젝트 명세서 (Project Specs)</h2>
<h3 id="1핵심-가치-core-values">1.핵심 가치 (Core Values)</h3>
<ul>
<li><code>격리성 (Isolation)</code> : 테넌트 간의 간섭 최소화 (리소스, 권한, 네트워크, 스토리지 등)</li>
<li><code>자율성 (Autonomy)</code> : 할당된 범위 내에서 테넌트가 자유롭게 리소스 관리</li>
<li><code>가시성 (Visibility)</code> : 관리자와 테넌트 모두 자원 사용 현황 확인 가능</li>
</ul>
<h3 id="2-상세-기능-명세-functional-specifications">2. 상세 기능 명세 (Functional Specifications)</h3>
<table>
<thead>
<tr>
<th></th>
<th><strong>기능명</strong></th>
<th><strong>상세 설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>권한 관리</td>
<td>RBAC</td>
<td><code>관리자(나)</code> : 클러스터 전체 권한(cluster-admin)</td>
</tr>
<tr>
<td></td>
<td></td>
<td><code>테넌트</code> : 특정Namespace 내에서만 리소스 생성/삭제 가능. 타 네임스페이스 및 노드 정보 조회 불가</td>
</tr>
<tr>
<td>자원 관리</td>
<td>Dynamic Resource Quota</td>
<td>Namespace별 CPU/RAM 상한선 설정</td>
</tr>
<tr>
<td></td>
<td></td>
<td><code>확장 기능</code> : 자원 부족 시 관리자 승인을 통항 쿼터 증설 프로세스 마련</td>
</tr>
<tr>
<td>네트워크</td>
<td>Zero-Trust Network Policy</td>
<td><code>테넌트 간 격리</code> : A 네임스페이스의 Pod 는 B 네임스페이스의 Pod 와 직접 통신 불가</td>
</tr>
<tr>
<td></td>
<td></td>
<td>외부(인터넷)을 거쳐 들어오는 통신은 허용 가능하도록 설계(Ingress / LoadBalancer 활용)</td>
</tr>
<tr>
<td>외부 접속</td>
<td>External Access Point</td>
<td><code>관리자/테넌트</code> : 포트포워딩을 통해 SSH 접속 지원</td>
</tr>
<tr>
<td></td>
<td></td>
<td><code>서비스</code> : 각 테넌트 앱에 고유 도메인 또는 포트 할당</td>
</tr>
<tr>
<td>모니터링</td>
<td>Tenant Dashboard</td>
<td>Prometheus &amp; Grafana 를 활용하여 테넌트별 실시간 자원 사용량 시각화</td>
</tr>
</tbody></table>
<h3 id="마무리">마무리</h3>
<p>우선 멀티 테넌트 제공 프로젝트의 초기 계획을 세워봤습니다. 본격적인 개발 및 구축 과정을 이어서 기록해보겠습니다.</p>
<p>p.s) 글을 쓰는 현재 시점에서는 테넌트 서버 제공 서비스가 런칭을 하였습니다. 구축 및 개발 과정에서 너무나 많은 시행착오가 있었기에 개발과 함께 블로그 포스팅을 못하였지만, 노션 등에 정리한 내용과 시행착오들을 잘 블로그에 잘 정리해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[부트캠프 - 39일차] 2/13.금 - Kubernetes]]></title>
            <link>https://velog.io/@pizza_loves_me/%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-39%EC%9D%BC%EC%B0%A8-213.%EA%B8%88-Kubernetes</link>
            <guid>https://velog.io/@pizza_loves_me/%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-39%EC%9D%BC%EC%B0%A8-213.%EA%B8%88-Kubernetes</guid>
            <pubDate>Sun, 15 Feb 2026 05:28:04 GMT</pubDate>
            <description><![CDATA[<hr>
<h1 id="pod">Pod</h1>
<h2 id="pod-1">Pod</h2>
<h3 id="컨테이너-접속과-명령어-실행">컨테이너 접속과 명령어 실행</h3>
<ul>
<li>생성된 컨테이너에 접속할 때는 <code>kubectl exec</code> 명령어를 사용하는데 가상 터니멀을 생성(<code>-t</code>)하고 표준 입력을 패스 스루(<code>-i</code>) 하면서 <code>/bin/sh</code> 를 실행하면 컨테이너에 SSH로 로그인 한 상태가 됨</li>
<li>정상 수행 중 인지 명령어 실행</li>
</ul>
<pre><code class="language-bash">kubectl exec -it sample-pod -- /bin/bash

# 정상 수행 중인지 명령어 실행
apt update &amp;&amp; apt -y install iproute2 procps

ip a | grep &quot;inet&quot;

ss -napt | grep LISTEN
</code></pre>
<ul>
<li>외부에서 명령어 실행</li>
</ul>
<pre><code class="language-bash"># 컨테이너가 1개인 파드에 명령어 수행
kubectl exec -it sample-pod -- /bin/ls

# 특정 파드를 지정해서 명령어 수행
kubectl exec -it sample-2pod -c nginx-container -- /bin/ls

# 옵션을 활용한 명령어 실행
kubectl exec -it sample-pod -- /bin/ls --all --classify

# 특수문자가 포함된 명령어 실행
kubectl exec -it sample-pod -- /bin/bash -c &quot;ls --all --classify | grep lib&quot;</code></pre>
<h3 id="entrypoint-명령-cmd-명령과-commandargs">ENTRYPOINT 명령 /CMD 명령과 command/args</h3>
<ul>
<li>도커 파일로 이미지를 생성할 때는 <code>ENTRYPOINT</code> 명령과 <code>CMD</code> 명령을 사용하여 컨테이너 실행 시 명령어를 정의하는데 <strong><code>쿠버네티스</code></strong>에서는 도커용 용어와 조금 다르게 <code>ENTRYPOINT</code>를 <code>command</code>로 <code>CMD</code> 를 <code>args</code> 라고 부름</li>
</ul>
<h3 id="파드명-제한">파드명 제한</h3>
<ul>
<li>이용 가능한 문자는 영문 소문자와 숫자</li>
<li>이용 가능한 기호는 <code>-</code> 와 <code>.</code></li>
<li>시작과 끝은 영문 소문자</li>
</ul>
<h3 id="호스트의-네트워크-구성을-사용한-파드-기동">호스트의 네트워크 구성을 사용한 파드 기동</h3>
<ul>
<li>쿠버네티스에 기동하는 파드에 할당된 IP 주소는 쿠버네티스 노드의 호스트 IP 주소와 범위가 달라 외부에서 볼 수 없는 IP 주소가 할당됨</li>
<li>호스트의 네트워크를 사용하는 설정(<code>spec.hostNetwork</code>)을 활성화하면 호스트상에서 프로세스를 기동하는 것과 같은 네트워크 구성(<code>IP 주소, DNS 설정, host 설정 등</code>)으로 파드를 기동시킬 수 있음</li>
<li><code>hostNetwork</code> 를 사용한 파드는 쿠버네티스 <code>노드의 IP 주소를 사용</code>하는 관계로 포트 번호 충돌을 방지하기 위해 기본적으로 사용하지 않고, 사용할 때는 <code>에지(edge) 환경에서의 사용</code>이나 <code>호스트 측의 네트워크를 감시 또는 제어</code>와 같은 특수한 애플리케이션 등에서만 사용</li>
</ul>
<h3 id="파드-dns-설정과-서비스-디스커버리">파드 DNS 설정과 서비스 디스커버리</h3>
<ul>
<li>DNS 서버에 관한 설정(dnsPolicy)은 파드 정의 <code>spec.dnsPolicy</code> 에 설정</li>
<li>dnsPolicy 설정값<ul>
<li><code>ClusterFirst(기본값)</code><ul>
<li>일반적으로 파드는 클러스터 내부 DNS 를 사용하여 이름을 해석</li>
<li>서비스 디스커버리나 클러스터 내부 로드 밸런싱에서 사용</li>
<li>dnsPolicy 가 ClusterFirst 인 경우 클러스터 내부의 DNS 서버ㅔ 질의를 하고 클러스터 내부 DNS에서 해석이 안 되는 도메인에 대해서는 업스트림 DNS 서버에 질의</li>
</ul>
</li>
<li><code>None</code><ul>
<li>특별한 요건에 따라서는 클러스터 외부 DNS 를 참조하는 경우도 있음</li>
<li>DNS 서버를 수동으로 설정하려면 <code>spec.dnsPolicy: None</code> 이라고 설정한 후 dnsConfig에 설정하고 싶은 값을 작성</li>
<li>정적으로 외부 DNS 서버만 설정하면 클러스터 내부 DNS를 사용한 서비스 디스커버리는 사용할 수 없게 되므로 주의</li>
</ul>
</li>
<li><code>Default</code><ul>
<li>쿠버네티스 노드의 DNS 설정을 그대로 상속받는 경우에는 <code>spec.dnsPolicy: Default</code> 로 설정</li>
<li>dnsPolicy 의 기본값은 Default 가 아니므로 주의</li>
<li>쿠버네티스 노드의 DNS 설정을 상속받게 설정하면 클러스터 내부의 DNS를 사용한 서비스 디스커버리를 할 수 없음</li>
</ul>
</li>
<li><code>ClusterFirstWithHostNet</code></li>
<li>hostNetwork 를 사용한 파드에 클러스터 내부의 DNS를 참조하고 싶은 경우에는 <code>spec.dnsPolicy: ClusterFirstWithHostNet</code> 을 설정</li>
<li>hostNetwork 를 사용하는 경우 기본값 ClusterFirst 의 설정값은 무시되고 쿠버네티스 <code>ClusterFirstWithHostNet</code> 을 지정해야 함</li>
</ul>
</li>
</ul>
<h3 id="정적-호스트명-설정">정적 호스트명 설정</h3>
<ul>
<li>리눅스 운영체제에서는 DNS로 호스트명을 해석하기 전에 <code>/etc/hosts</code> 파일로 정적 호스트명을 해석</li>
<li>쿠버네티스에서는 파드 내부 모든 컨테이너의 <code>/etc/hosts</code> 를 변경하는 기능이 준비되어 있으며 <code>spec.hostAliases</code> 로 지정하여 사용</li>
</ul>
<h3 id="작업-디렉터리-설정">작업 디렉터리 설정</h3>
<ul>
<li>컨테이너에서 동작하는 애플리케이션의 작업 디렉러티(Working Directory) 는 도커 파일의 <code>WORKDIR</code> 명령 설정을 따르지만, <code>spec.containers[].workingDir</code> 로 덮어 쓸 수 있음</li>
<li>특정 스크립트 등이 배치된 볼륨을 파드에 마운트할 때 그 스크립트가 배치된 디렉터리로 이동한 후 실행하고 싶은 경우가 있는데 이는 작업 디렉터리를 변경하는 기능을 사용하여 해결할 수 있음</li>
<li>컨테이너에 <code>workingDir</code> 을 설정한 경우 프로세스가 실행되는 디렉터리가 변경된 것을 확인할 수 있음</li>
</ul>
<h2 id="replicaset--replicationcontroller">ReplicaSet / ReplicationController</h2>
<h3 id="개요">개요</h3>
<ul>
<li>Replicaset은 일정한 개수의 동일한 Pod가 항상 실행되도록 관리하는데 이러한 기능이 필요한 이유는 서비스의 지속성 때문</li>
<li>노드의 하드웨어에서 발생하는 장애 등의 이유로 파드룰 사용할 수 없을 때 다른 노드에서 파드를 다시 생성해서 사용자에게 중단 없는 서비스를 제공할 수 있음</li>
<li>원래 파드의 레플리카를 생성하는 리소스의 이름은 레플리케이션 컨트롤러였는데 레플리카셋을 추가하고 일부 기능을 추가</li>
<li>레플리케이션 컨트롤러는 앞으로 사용하지 않는 추세이기 때문에 기본적으로 레플리카셋을 사용하는 것을 권장</li>
</ul>
<h3 id="파드-정지와-자동화된-복구">파드 정지와 자동화된 복구</h3>
<ul>
<li>레플리카셋에서는 노드나 파드에 장애가 발생했을 때 지정한 파드 수를 유지하기 위해 다른 노드에서 파드를 기동시켜 주기 때문에 장애 시에도 많은 영향을 받지 않는데 이것이 쿠버네티스의 중요한 콘셉트 중 하나로 <code>자동화된 복구</code>라는 기능</li>
</ul>
<h3 id="레플리카셋과-레이블">레플리카셋과 레이블</h3>
<ul>
<li>레플리카셋은 쿠버네티스가 파드를 모니터링하여 파드 수를 조정</li>
<li>모니터링은 특정 레이블을 가진 파드 수를 계산하는 형태로 이루어 짐</li>
<li>레플리카 수가 부족한 경우 매니페스트에 기술된 <code>spec.template</code> 로 파드를 생성하고 레플리카 수가 많을 경우 레이블이 일치하는 파드 중 하나를 삭제</li>
</ul>
<h3 id="일치성-기준-조건과-집합성-기준-조건">일치성 기준 조건과 집합성 기준 조건</h3>
<ul>
<li>레플리카 제어 조건은 서비스 중단 예정인 레플리케이션 컨트롤러의 일치성 기준(<code>equality-based</code>) 셀렉터였지만, 레플리카셋에서는 좀 더 강화된 집합성 기준(<code>set-based</code>) 셀렉터를 사용하여 유연한 제어도 가능<ul>
<li>일치성 기준(equality-based): 조건부에 일치 불일치(<code>=</code>, <code>!=</code>) 조건 지정</li>
<li>집합성 기준(set-based): 조건부에 일치 불일치(<code>=</code>, <code>!=</code>) 조건 지정과 집합(<code>in</code>, <code>notin</code>, <code>exists</code>) 조건 지정 가능</li>
</ul>
</li>
<li>쿠버네티스에서 어떤 조전을 지정할 때는 이 두 가지 방법이 있고, 레플리카셋 외에 스케줄링할 때도 이 조건 지정이 사용됨</li>
<li>일치성 기준 조건에서는 조건부에 같은지 혹은 같지 않은지에 대한 조건을 사용할 수 있는데 <code>app=sample-app</code> 과 같이 지정하는 것</li>
<li>집합성 기준 조건에서는 일치성 기준 조건과 함께 집합 조건을 지정할 수 있는데 <code>env ln [development, staging]</code> 과 같이 지정할 수 있으며 <code>ln</code> 에 해당하는 연산자는 스케줄링 조건에서 사용할 때 수치 비교도 가능</li>
</ul>
<h2 id="deployment">Deployment</h2>
<blockquote>
<p>Deployment → ReplicaSet → Pods</p>
</blockquote>
<h3 id="개요-1">개요</h3>
<ul>
<li>여러 레플리카셋을 관리하여 롤링 업데이트나 롤백 등을 구현하는 리소스</li>
<li>디플로이먼트가 레플리카셋을 관리하고 레플리카셋이 파드를 관리하는 관계</li>
<li>디플로이먼트는 레플리카셋의 상위 개념으로서 파드의 개수를 유지할 뿐만 아니라 배포 작업을 좀 더 세분화해 관리할 수 있음</li>
<li>디플로이먼트를 사용하면 신규 레플리카셋에 컨테이너가 기동되었는지와 헬스 체크는 통과했는지를 확인하면서 전환 작업이 진행되며 레플리카셋의 이행 과정에서 파드 수에 대한 상세한 지정도 가능</li>
<li>쿠버네티스에서 가장 권장하는 컨테이너 기동 방법으로 알려져 있음</li>
<li>하나의 파드를 기동만 하더라도 디플로이먼트 사용을 권장하는데 파드로만 배포한 경우에는 파드에 장애가 발생하면 자동으로 파드가 다시 생성되지 않으며 레플리카셋으로만 배포한 경우에는 롤링 업데이트 등의 기능을 사용할 수 없음</li>
</ul>
<h3 id="레플리카셋이-생성되는-조건">레플리카셋이 생성되는 조건</h3>
<ul>
<li>다플로이먼트에서는 변경이 발생하면 레플리카셋이 생성되는데 변경에는 레플리카 수의 변경 등은 포함되어 있지 않으며 생성된 파드의 내용 변경이 필요</li>
<li><code>spec.template</code> 에 변경이 있으면 생성된 파드의 설정이 변경되기 때문에 레플리카셋을 신규로 생성하고 롤링 업데이트를 수행</li>
<li>실제로 매니페스트를 쿠버네티스에 등록한 후 레플리카셋의 정의를 보면, <code>spec.template</code> 아래의 해시값(파드 템플릿 해시 - Pod Template Hash) 을 계산하고 이 값을 사용한 레이블로 관리</li>
<li>수작업으로 이미지 등을 이전 버전으로 재변경하여 해시값이 동일해진 경우에는 레플리카셋을 신규로 생성하지 않고 기존 레플리카셋을 사용</li>
</ul>
<h3 id="디플로에먼트에-속한-파드의-삭제-및-복구-과정">디플로에먼트에 속한 파드의 삭제 및 복구 과정</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/6c6e7b72-49ea-498a-b159-a95e0e7e4484/image.png" alt=""></p>
<h3 id="변경-롤백">변경 롤백</h3>
<ul>
<li>디플로이먼트에는 롤백 기능이 있음</li>
<li>롤백 기능의 실체는 현재 사용 중인 레플리카셋의 전환과 같은 것</li>
<li>디플로이먼트가 생성한 기존 레플리카셋은 레플리카 수가 0인 상태로 남아 있기 때문에 레플리카 수를 변경시켜 다시 사용할 수 있는 상태가 됨</li>
</ul>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/55e9c307-db3b-411e-a91a-c6b74e9370bb/image.png" alt=""></p>
<ul>
<li>변경 이력을 확인할 때는 <code>kubectl rollout history</code> 명령어를 사용하는데 <code>CHANGE-CAUSE</code> 부분은 디플로이먼트를 생성할 때 <code>--record</code> 옵션을 사용하여 이력 내용이 있지만 <code>--record</code> 옵션을 사용하지 않았다면  <code>&lt;none&gt;</code></li>
</ul>
<blockquote>
<p>실제 환경에서는 롤백 기능을 사용하는 경우가 많지 않은데, CI/CD 파이프라인에서 롤백을 하는 경우 <code>kubectl rollout</code> 명령어보다 <code>이전 매니페스트를 다시 kubectl apply</code> 명령어로 실행하여 적용하는 것이 <strong>호환성</strong> 면에서 더 좋기 때문이며, <code>spec.template</code> 을 같은 내용으로 되돌렸던 경우에는 <code>pod-template-hash</code> 값이 같기 때문에 <code>kubectl rollout</code> 처럼 기존에 있었던 레플리카셋의 파드가 기동됨</p>
</blockquote>
<h3 id="업데이트-전략">업데이트 전략</h3>
<ul>
<li>Deployment 의 배포 전략은 주로 애플리케이션이 변경될 때 사용</li>
<li>이전 버전의 애플리케이션에서 업데이트가 필요한 경우에 주로 사용되며 방법으로는 <code>RollingUpdate</code>, <code>Recreate</code> 가 있음</li>
<li>기본 전략은 <code>RollingUpdate</code></li>
</ul>
<h3 id="recreate"><strong><code>recreate</code></strong></h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/4f1f106e-79d2-412f-85b6-40860275543c/image.png" alt=""></p>
<ul>
<li>재생성(recreate) 업데이트는 모든 이전 버전(V1)의 파드를 모두 한 번에 종료하고 새 버전(V2)의 파드로 일괄적으로 교체하는 방식</li>
<li>빠르게 교체할 수 있지만 새로운 버전의 파드에 문제가 발생하면 대처가 늦어질 수 있음</li>
</ul>
<h3 id="rollingupdate"><strong><code>RollingUpdate</code></strong></h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/92e743bd-514c-4654-be64-7dec88686ed8/image.png" alt=""></p>
<ul>
<li><p>롤링 업데이트는 새 버전의 애플리케이션을 배포할 때 새 버전의 애플리케이션은 하나씩 늘려가고 기존 버전의 애플리케이션은 하나씩 줄여나가는 방식</p>
</li>
<li><p>쿠버네티스에서 사용하는 표준 배포 방식</p>
</li>
<li><p>새로운 버전(v2)으로 배포된 파드에 문제가 발생하면 다시 이전 버전(v1)의 파드로 서비스를 대체할 수 있어서 상당히 안정적인 배포 방식이지만 업데이트가 느리게 진행된다는 단점이 있음</p>
</li>
<li><p><code>maxSurge</code></p>
<ul>
<li>롤링 업데이트를 위해 최대로 생성할 수 있는 파드 개수</li>
<li>maxSurge 를 높게 설정하면 롤링 배포를 빠르게 적용 가능</li>
<li><code>%</code> 또는 개수로 지정 가능</li>
<li>설정하지 않을 경우 기본 값은 <code>25%</code></li>
</ul>
</li>
<li><p><code>maxUnavailable</code></p>
<ul>
<li>롤링 업데이트 중 최대로 삭제할 파드 개수</li>
<li>maxUnavailable 을 높게 설정하면 롤링 배포를 빠르게 적용 가능</li>
<li>한 번에 많은 파드를 삭제할 경우 트래픽이 남아있는 소수의 파드로 집중될 수 있어 값을 적절히 설정 필요</li>
<li><code>%</code> 또는 개수로 지정 가능</li>
<li>설정하지 않을 경우 기본 값은 <code>25%</code></li>
</ul>
</li>
<li><p><code>maxUnavailable=0 / maxSurge=1</code></p>
<p>  <img src="https://velog.velcdn.com/images/pizza_loves_me/post/a8cd1d53-c342-47bb-9d1a-2cd85d9ce19b/image.png" alt=""></p>
</li>
</ul>
<ul>
<li><p><code>maxUnavailable=1 / maxSurge=0</code></p>
<p>  <img src="https://velog.velcdn.com/images/pizza_loves_me/post/1d01049d-e675-48d2-b271-484b655ac269/image.png" alt=""></p>
</li>
</ul>
<h3 id="블루그린-업데이트">블루/그린 업데이트</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/ab39a63f-8d1b-41c9-885f-a0bc78a6d288/image.png" alt=""></p>
<ul>
<li>블루/그린 업데이트는 애플리케이션의 이전 버전(블루, v1 pod)과 새 버전(그린, v2 pod)을 동시에 운영</li>
<li>서비스 목적으로 접속할 수 있는 것은 새 버전의 파드만 가능하며 이전 버전의 파드는 테스트 목적으로만 접속할 수 있음</li>
<li>새로운 버전의 파드에 문제가 발생했을 때 빠르게 대응할 수 있으며 안정적으로 배포할 수 있음</li>
<li>많은 파드가 필요하므로 그만큼 많은 자원이 필요하다는 단점이 있블로</li>
<li>블루/그린 업데이트를 이용해 애플리케이션을 배포할 때는 version으로 구분해 관리</li>
<li>green 배포용 yaml 파일을 생성하는데 이전 버전의 배포 yaml 파일과 내용이 같지만 사용하는 이미지의 경로와 labels 의 version 만 수정</li>
</ul>
<h3 id="canary-업데이트">Canary 업데이트</h3>
<ul>
<li>Canary 업데이트는 블루/그린과 비슷하지만 더 진보적인 단계적 접근 방식</li>
<li>Canary는 주로 애플리케이션의 몇몇 새로운 기능을 테스트할 때 사용</li>
<li>Canary는 두 개의 버전을 모두 배포하지만 새 버전에는 조금씩 트래픽을 증가시켜(처음에는 5% 정도의 트래픽만 흘려 보내지만 나중에는 50%의 트래픽을 흘려보냄) 새로운 기능들을 테스트</li>
<li>기능 테스트가 끝나고 새 버전에 문제가 없다고 판단하면 이전 버전은 모두 종료시키고 새 버전으로만 서비스</li>
<li>Canary 업데이트용 새로운 배포 파일을 생성하는데 <code>track: canary</code> 를 통해 Canary 업데이트를 만들어 줄 수 있음</li>
<li>Canary 업데이트의 문제점<ul>
<li>한명의 유저가 서비스를 사용할 때 로드밸런서에 의해 자동으로 여러 서버에 접속될 수 있음</li>
<li>유저가 새로고침을 할 때 로드 밸런싱이 일어나서 구버전/신버전으로 바뀌게 되는 문제가 생길 수 있음</li>
<li>스티키 세션을 사용하여 사용자가 한 번 특정한 서버에 접속되어 있으면 그 서버에만 계속 접속되도록 만들 수 있움</li>
</ul>
</li>
</ul>
<h3 id="상세-업데이트-파라미터">상세 업데이트 파라미터</h3>
<ul>
<li><code>minReadySeconds(최소 대기 시간- 초)</code><ul>
<li>파드가 Ready 상태가 된 다음부터 디플로이먼트 리소스에서 파드 기동이 완료되었다고 판단(다음 파드의 교체가 가능하다고 판단하기까지의 최소 시간</li>
</ul>
</li>
<li><code>revisionHistoryLimit(수정 버전 기록 제한)</code><ul>
<li>디플로이먼트가 유지할 레플리카셋 수</li>
<li>롤백이 가능한 이력 수</li>
</ul>
</li>
<li><code>progressDeadlineSeconds(진행 기한 시간(초))</code><ul>
<li>Recreate/Rollingllpdate 처리 타임아웃 시간</li>
<li>타임아웃 시간이 경과하면 자동으로 롤백</li>
</ul>
</li>
</ul>
<h3 id="디플로이먼트-스케일링">디플로이먼트 스케일링</h3>
<ul>
<li>디플로이먼트가 관리하는 레플리카셋의 레플리카 수는 레플리카셋과 같은 방법으로 <code>kubectl apply -f</code> 또는 <code>kubectl scale</code> 을 사용하여 스케일링</li>
</ul>
<h2 id="daemonset">DaemonSet</h2>
<h3 id="개요-2">개요</h3>
<p><img src="https://velog.velcdn.com/images/pizza_loves_me/post/ff4f05b4-0d5a-411d-81ad-b21de96e0932/image.png" alt=""></p>
<ul>
<li>ReplicaSet 의 특수한 형태</li>
<li>데몬셋은 모든 노드에 파드를 생성</li>
<li>ReplicaSet 이 특정 개수의 파드를 유지할 때 사용하는 것이라면 데몬셋은 모든 노드에 파드를 배포할 때 사용</li>
<li>데몬셋은 레플리카 수를 지정할 수 없고 하나의 노드에 누 개의 파드를 배치할 수도 없음</li>
<li>파드를 배치하고 싶지 않은 노드가 있을 경우 <code>nodeselector</code>나 노드 안티어피니티를 사용한 스케줄링으로 예외 처리</li>
<li>쿠버네티스 노드를 늘렸을 때도 데몬셋의 파드는 자동으로 늘어난 노드에서 기동하기 때문에 데몬셋은 각 파드가 출력하는 로그를 호스트 단위로 수집하는 플루언트디나 각 파드 리소스 사용 현황 및 노드 상태를 모니터링 하는 데이터독 등 모든 노드에서 반드시 동작해야 하는 프로세스를 위해 사용하는 것이 유용</li>
</ul>
<h3 id="데몬셋-업데이트-전략-ondelete--rollingupdate">데몬셋 업데이트 전략: OnDelete &amp; RollingUpdate</h3>
<h3 id="ondelete">OnDelete</h3>
<ul>
<li>OnDelete 에서는 데몬셋 매니페스트를 수정하여 이미지 등을 변경했더라도 기존 파드는 업데이트 되지 않음</li>
<li>디플로이먼트와 달리 데몬셋은 모니터링이나 로그 전송과 같은 용도로 많이 사용하기 때문에 업데이트는 다음 번에 다시 생성할 때나 수동으로 임의의 시점에 하게 되어 있으며 type 외에 지정할 수 있는 항목은 없음</li>
<li>OnDelete로 설정하고 아무것도 하지 않을 경우 노드의 유지 보수를 이유로 파드가 정지되거나 예기치 못하게 파드가 정지되는 등 다양한 이유로 파드가 정지되어 다시 생성되기 전까지는 업데이트 되지 않기 때문에 운영상의 이유로 정지하면 안 되는 파드이거나 업데이트가 급하게 필요 없는 경우 OnDelete 설정으로 사용해도 되지만 이전 버전이 계속 장기간 사용된다는 점에 주의</li>
<li>임의의 시점에 파드를 업데이트 하는 경우에는 데몬셋과 연결된 파드를 kubectl delete pod 명령어로 수동으로 정지시키고 자동화된 복구 기능으로 새로운 파드를 생성해야 함</li>
</ul>
<h3 id="rollingupdate-1">RollingUpdate</h3>
<ul>
<li>데몬셋도 디플로이먼트와 마찬가지로 RollingUpdate 를 사용한 업데이트가 가능</li>
<li>데몬셋에서는 하나의 노드에 동일한 파드를 여러 개 생성할 수 없으므로 디플로이먼트와 달리 동시에 생성할 수 있는 최대 파드 수(masSurge)를 설정할 수 없고 동시에 정지 가능한 최대 파드 수(maxUnavailable)만 지정하여 RollingUpdate를 수행</li>
<li>maxUnavailable=2 의 경우 파드를 두 개씩 동시에 업데이트해 나가는 형태</li>
<li>maxUnavailable 의 기본값은 1이며 maxUnavailable 을 0으로 지정할 수는 없음</li>
</ul>
<h2 id="statefullset">StatefullSet</h2>
<ul>
<li>스테이트풀셋도 데몬셋과 마찬가지로 레플리카셋의 특수한 형태라고 할 수 있는 리소스</li>
<li>데이터베이스 등과 같은 스테이트풀한 워크로드에 사용하기 위한 리소스</li>
<li>RepliaSet과 차이점<ul>
<li>생성되는 파드명의 접미사는 숫자 인덱스가 부여된 것</li>
<li>파드명이 바뀌지 변경되어도 내용은 바뀌지 않음</li>
<li>데이터를 영구적으로 저정하기 위한 구조</li>
<li>스테이트풀셋에서는 spec.volumeClaimTemplates를 지정할 수 있는데 이 설정으로 스테이트풀셋으로 생성되는 각 파드에 영구 볼륨 클레임(영구 볼륨 요청)을 설정할 수 있으며 영구 볼륨 클레임을 사용하면 클러스터 외부의 네트워크를 통해 제공되는 영구 볼륨을 파드에 연결할 수 있으므로 파드를 재기동할 때나 다른 노드로 이동할 때 같은 데이터를 보유한 상태로 컨테이너가 다시 생성되고 영구 볼륨은 하나의 파드가 소유할 수도 있고 영구 볼륨 종류에 따라 여러 파드에서 공유할 수도 있음</li>
</ul>
</li>
</ul>
<h3 id="스케일링">스케일링</h3>
<ul>
<li>스케일링 수행<ul>
<li><code>kubectl scale statefulset sample-statefulset --replicas=5</code></li>
</ul>
</li>
<li>스테이트풀셋에서 레플리카 수를 변경하여 파드를 생성하고 삭제하면 레플리카셋이나 데몬셋 등과 달리 기본적으로 파드를 동시에 하나씩만 생성하고 삭제하기 때문에 조금 시간이 걸림</li>
<li>스케일 아웃일 때는 인덱스가 가장 작은 것부터 파드를 하나씩 생성하고 이전에 생성된 파드가 Ready 상태가 되고 나서 다음 파드를 생성하기 시작하는데, 위 예제에서는 <code>sample-statefulset-0, sample-statefulset-1, sample-statefulset-2</code> 순서로 생성</li>
<li>스케일 인일 때는 인덱스가 가장 큰 파드(가장 최근에 생성된 컨테이너)부터 삭제되므로 2 → 1 → 0 순서로 삭제</li>
</ul>
<h3 id="라이프사이클">라이프사이클</h3>
<ul>
<li>스테이트풀셋에서도 <code>spec.podManagementPolicy</code> 를 Parallel 로 설정하여 레플리카셋 등과 마찬가지로 병렬로 동시에 파드를 기동시킬 수 있음</li>
<li><code>spec.podManagementPolicy</code> 기본값은 <code>OrderReady</code> 로 설정되어 있음</li>
</ul>
<h3 id="업데이트-전략-1">업데이트 전략</h3>
<h3 id="ondelete-1">OnDelete</h3>
<ul>
<li>OnDelete 에서는 스테이트풀셋 매니페스트를 수정하여 이미지 등을 변경했더라도 기존 파드는 업데이트 되지 않음</li>
<li>스테이트풀셋에서 OnDelete는 영속성 영역을 가진 데이터베이스나 클러스터 등에서 많이 사용되기 때문에 수동으로 업데이트 하고 싶을 경우 OnDelete 를 사용하여 임의의 시점에서나 다음에 재기동할 때 업데이트를 진행하게 되어 있는데 type 외에 지정할 수 있는 항목은 없음</li>
</ul>
<h3 id="rollingupdate-2">RollingUpdate</h3>
<ul>
<li>디플로이먼트와 마찬가지로 롤링업데이트를 사용한 스테이트풀셋 업데이트가 가능하지만 스테이트풀셋에는 영속성 데이터가 있으므로 디플로이먼트와 다르게 추가 파드를 생성해서 롤링 업데이트를 할 수 없고, 동시에 정지 가능한 최대 파드 수(maxUnavailable)를 지정하여 롤링 업데이트를 할 수도 없으므로 파드마다 Ready 상태인지를 확인하고 업데이트</li>
<li><code>spec.podManagementPolicy</code>가 Parallel로 설정되어 있는 경우에도 병렬로 동시에 처리되지 않고 파드 하나씩 업데이트</li>
<li>스테이트풀셋의 RollingUpdate에서는 partition이라는 특정 값을 설정할 수 있는데 partition을 설정하면 전체 파드 중 어떤 파드까지 업데이트할지를 지정할 수 있고 이 설정을 사용하면 전체에 영향을 주지 않고 부분적으로 업데이트를 적용하고 확인할 수 있어 안전한 업데이트를 할 수 있음</li>
<li>OnDelete와 달리 수동으로 재기동한 경우에도 partition 값보다 작은 인덱스를 가진 파드는 업데이트되지 않음</li>
</ul>
<h3 id="삭제">삭제</h3>
<ul>
<li>스테이트풀셋을 생성하면 파드에 영구 볼륨 클레임(영구 볼륨 요청(을 설정할 수 있어 영구 볼륨도 동시에 확보할 수 있음</li>
<li>영구 볼륨은 스테이프풀셋이 삭제되어도 동시에 해제되지 않음</li>
<li>스테이트풀셋이 영구 볼륨을 해제하기 전에 볼륨에서 데이터를 백업할 수 있도록 시간을 주기 때문</li>
<li>스테이트풀셋을 삭제하고 스테이트풀셋이 영구 볼륨 클레임으로 확보한 영구 볼륨을 해제하지 않고 다시 스테이트풀셋을 생성한 경우 영구 볼륨 데이터는 그대로 파드가 기동되는데 스케일 인하여 레플리카 수를 줄인 경우도 마찬가지</li>
<li>스테이트풀셋을 삭제해도 영구 볼륨이 확보된 상태일 경우 비용이 발생</li>
<li>관리형 서비스의 종량 과금 방식인 경우 볼륨을 그대로 유지하면 비용이 발생하고 온프레미스 환경인 경우에도 디스크 공간이 확보된 상태로 남아 있기 때문에 스테이트풀셋을 삭제한 후에는 확보된 영구 볼륨도 해제해야 함</li>
<li>스테이트풀셋이 확보한 영구 볼륨 해제<ul>
<li><code>kubectl delete persistentvolumeclaims www-sample-statefulset-{0..4}</code></li>
</ul>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>