<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>leenah.log</title>
        <link>https://velog.io/</link>
        <description>정도를 걷는 엔지니어</description>
        <lastBuildDate>Thu, 25 Jun 2026 01:02:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>leenah.log</title>
            <url>https://velog.velcdn.com/images/lee_nah/profile/2dac464d-968c-4d72-a818-a46f23b5115b/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. leenah.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lee_nah" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[보안 엔지니어 신입의 기록 - 허브부터 VLAN까지 네트워크 기초]]></title>
            <link>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-%ED%97%88%EB%B8%8C%EB%B6%80%ED%84%B0-VLAN%EA%B9%8C%EC%A7%80-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-%ED%97%88%EB%B8%8C%EB%B6%80%ED%84%B0-VLAN%EA%B9%8C%EC%A7%80-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Thu, 25 Jun 2026 01:02:33 GMT</pubDate>
            <description><![CDATA[<h1 id="보안-엔지니어-신입의-기록---허브부터-vlan까지-네트워크-기초">보안 엔지니어 신입의 기록 - 허브부터 VLAN까지 네트워크 기초</h1>
<h2 id="허브hub란">허브(Hub)란?</h2>
<p>허브란 여러 PC를 연결해주는 네트워크 장비이다. 그러나 허브는 MAC 주소를 저장하지 않기 때문에 패킷이 들어오면 연결된 모든 포트에 뿌려버린다.</p>
<br>

<h3 id="허브의-문제점---반이중-방식-half-duplex">허브의 문제점 - 반이중 방식 (Half-Duplex)</h3>
<p>허브는 반이중 방식을 사용한다. 반이중이란 한 번에 한 방향으로만 데이터를 전송할 수 있는 방식이다.</p>
<blockquote>
<p>무전기와 똑같다. 말할 때는 듣지 못하고, 들을 때는 말하지 못한다.</p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th>반이중 (Half-Duplex)</th>
<th>전이중 (Full-Duplex)</th>
</tr>
</thead>
<tbody><tr>
<td>방향</td>
<td>한 번에 한 방향</td>
<td>동시에 양방향</td>
</tr>
<tr>
<td>예시</td>
<td>무전기, 허브</td>
<td>전화통화, 스위치</td>
</tr>
<tr>
<td>충돌</td>
<td>발생 가능</td>
<td>없음</td>
</tr>
</tbody></table>
<br>

<h3 id="csmacd란">CSMA/CD란?</h3>
<p>CSMA/CD(Carrier Sense Multiple Access / Collision Detection)란 허브 환경에서 충돌을 감지하고 처리하는 방식이다.</p>
<pre><code>1. 보내기 전에 &quot;지금 조용해?&quot; 확인 (Carrier Sense)
        ↓
2. 조용하면 전송 시작
        ↓
3. 전송 중에 충돌 감지하면 (Collision Detection)
        ↓
4. 즉시 멈추고 &quot;충돌났다!&quot; 신호 보냄
        ↓
5. 랜덤 시간 기다렸다가 다시 1번부터</code></pre><p>랜덤하게 기다리는 이유는 둘 다 똑같이 기다리면 또 동시에 출발해서 충돌이 발생하기 때문이다.</p>
<hr>
<h2 id="스위치switch란">스위치(Switch)란?</h2>
<p>스위치란 허브의 문제점을 해결한 네트워크 장비이다. MAC 주소를 테이블로 저장하여 목적지 포트에만 패킷을 전달한다.</p>
<br>

<h3 id="허브-vs-스위치">허브 vs 스위치</h3>
<table>
<thead>
<tr>
<th></th>
<th>허브</th>
<th>스위치</th>
</tr>
</thead>
<tbody><tr>
<td>MAC 주소</td>
<td>저장 안 함</td>
<td>저장함</td>
</tr>
<tr>
<td>패킷 전달</td>
<td>모든 포트에 뿌림</td>
<td>목적지 포트에만 전달</td>
</tr>
<tr>
<td>방식</td>
<td>반이중</td>
<td>전이중</td>
</tr>
<tr>
<td>충돌</td>
<td>발생</td>
<td>없음</td>
</tr>
</tbody></table>
<blockquote>
<p>요즘은 허브 거의 안 쓰고 다 스위치를 사용한다.</p>
</blockquote>
<br>

<h3 id="스위치-종류">스위치 종류</h3>
<p>네트워크 규모가 커질수록 스위치를 계층화하여 관리한다.</p>
<pre><code>PC (수백대)
    ↓ 묶기
액세스 스위치 (PC 직접 연결)
    ↓ 묶기
분배 스위치 (액세스 스위치 묶음)
    ↓ 묶기
코어 스위치 (전체 트래픽 담당)
    ↓
라우터 → 인터넷</code></pre><table>
<thead>
<tr>
<th>스위치 종류</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>액세스 스위치</strong></td>
<td>PC 직접 연결, 우리가 흔히 보는 스위치</td>
</tr>
<tr>
<td><strong>분배 스위치</strong></td>
<td>액세스 스위치가 많아질 때 중간에서 묶어주는 역할</td>
</tr>
<tr>
<td><strong>코어 스위치</strong></td>
<td>전체 트래픽 담당, 라우터와 연결</td>
</tr>
</tbody></table>
<blockquote>
<p>소규모 회사는 액세스 스위치 → 라우터로 단순하게 구성하기도 한다.</p>
</blockquote>
<br>

<h3 id="업링크-포트란">업링크 포트란?</h3>
<p>업링크 포트란 하위 장비에서 상위 장비로 연결하는 포트이다.</p>
<pre><code>라우터 (인터넷 연결)
    ↑ 업링크
코어 스위치
    ↑ 업링크
분배 스위치
    ↑ 업링크
액세스 스위치 (PC 연결)</code></pre><blockquote>
<p>트렁크 포트 = 스위치끼리 옆으로 연결<br>
업링크 포트 = 상위 장비로 위로 연결</p>
</blockquote>
<hr>
<h2 id="vlan이란">VLAN이란?</h2>
<p>VLAN(Virtual LAN)이란 물리적으로 같은 네트워크인데 논리적으로 나눈 것이다.</p>
<pre><code>물리적으로 같은 스위치에 연결돼있지만

VLAN 10 → 개발팀
VLAN 20 → 인사팀
VLAN 30 → 서버팀</code></pre><p>서로 다른 VLAN끼리는 통신하지 못한다.</p>
<br>

<h3 id="vlan을-쓰는-이유">VLAN을 쓰는 이유</h3>
<ul>
<li><strong>보안</strong> : 같은 스위치에 연결돼있어도 VLAN이 다르면 통신 불가</li>
<li><strong>비용 절감</strong> : 스위치 여러 대 안 사도 하나로 논리적으로 나눌 수 있음</li>
<li><strong>성능 향상</strong> : 브로드캐스트 트래픽을 VLAN 안에서만 돌게 해서 전체 트래픽 감소</li>
<li><strong>유연한 관리</strong> : 물리적 위치 상관없이 논리적으로 묶을 수 있음</li>
</ul>
<br>

<h3 id="트렁크-포트란">트렁크 포트란?</h3>
<p>트렁크 포트란 여러 VLAN 정보를 한 번에 전달할 수 있는 포트이다. 스위치끼리 연결할 때 사용한다.</p>
<table>
<thead>
<tr>
<th></th>
<th>일반 포트 (Access Port)</th>
<th>트렁크 포트</th>
</tr>
</thead>
<tbody><tr>
<td>VLAN</td>
<td>1개만</td>
<td>여러 개</td>
</tr>
<tr>
<td>용도</td>
<td>PC 연결</td>
<td>스위치끼리 연결</td>
</tr>
</tbody></table>
<pre><code>1층 개발자 PC
    ↓ (일반 포트 - VLAN 10)
1층 스위치
    ↓ (트렁크 포트 - VLAN 10, 20, 30 다 전달)
3층 스위치
    ↓ (일반 포트 - VLAN 10)
3층 개발자 PC</code></pre><blockquote>
<p>트렁크 포트 덕분에 물리적 위치가 달라도 같은 VLAN으로 묶을 수 있다.</p>
</blockquote>
<hr>
<h2 id="ids-vs-ips란">IDS vs IPS란?</h2>
<table>
<thead>
<tr>
<th></th>
<th>IDS</th>
<th>IPS</th>
</tr>
</thead>
<tbody><tr>
<td>이름</td>
<td>Intrusion Detection System</td>
<td>Intrusion Prevention System</td>
</tr>
<tr>
<td>역할</td>
<td>탐지만 함</td>
<td>탐지 + 차단</td>
</tr>
<tr>
<td>트래픽</td>
<td>복사본 분석</td>
<td>실제 트래픽 분석</td>
</tr>
<tr>
<td>공격 감지 시</td>
<td>알림만</td>
<td>자동으로 차단</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>IDS</strong> = CCTV (보기만 함)<br>
<strong>IPS</strong> = CCTV + 경비원 (보고 막음)</p>
</blockquote>
<p>보안 솔루션에서 미러링 탭으로 트래픽 복사 → IDS/IPS가 분석 → 공격 탐지/차단하는 구조로 동작한다.</p>
<hr>
<h2 id="vpn-virtual-private-network이란">VPN (Virtual Private Network)이란?</h2>
<p>VPN이란 인터넷을 통해 안전한 사설 터널을 만드는 것이다.</p>
<pre><code>내 PC ──암호화된 터널──→ VPN 서버 ──→ 목적지</code></pre><h3 id="vpn-동작-순서">VPN 동작 순서</h3>
<pre><code>1. VPN 서버에 접속
2. 암호화된 터널 생성
3. 내 트래픽이 터널 통해서 나감
4. 목적지엔 VPN 서버 IP로 보임</code></pre><h3 id="vpn을-쓰는-이유">VPN을 쓰는 이유</h3>
<ul>
<li>내 IP 숨길 수 있음</li>
<li>암호화로 도청 방지</li>
<li>회사 내부망 원격 접속</li>
</ul>
<hr>
<h2 id="방화벽-계층이란">방화벽 계층이란?</h2>
<p>방화벽은 레벨에 따라 종류가 다르다.</p>
<table>
<thead>
<tr>
<th>방화벽 종류</th>
<th>계층</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>패킷 필터링</strong></td>
<td>L3 / L4</td>
<td>IP, 포트 기준으로 차단</td>
</tr>
<tr>
<td><strong>상태 기반 방화벽</strong></td>
<td>L4</td>
<td>세션 상태 추적해서 차단</td>
</tr>
<tr>
<td><strong>WAF</strong></td>
<td>L7</td>
<td>HTTP 내용까지 분석해서 차단</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>L3/L4 방화벽</strong> = &quot;이 IP, 이 포트 막아&quot;<br>
<strong>L7 방화벽 (WAF)</strong> = &quot;이 URL, 이 내용 막아&quot;</p>
</blockquote>
<hr>
<h2 id="네트워크-트러블슈팅이란">네트워크 트러블슈팅이란?</h2>
<p>서비스 접속이 안 될 때 계층별로 좁혀나가며 확인하는 것이다.</p>
<pre><code>ping (L3) → 네트워크 연결 확인
        ↓
telnet (L7) → 포트 + 서비스 확인
        ↓
ss, iptables → OS/방화벽 레벨 확인
        ↓
로그 확인 → 서비스단 확인</code></pre><h3 id="ping이란">ping이란?</h3>
<pre><code class="language-bash">ping 서버IP</code></pre>
<p>서버가 살아있는지, 네트워크 연결 자체가 되는지 확인한다. ICMP 프로토콜을 사용하며 L3 계층이다.</p>
<br>

<h3 id="telnet이란">telnet이란?</h3>
<pre><code class="language-bash">telnet 서버IP 포트번호</code></pre>
<p>특정 포트가 열려있는지 확인할 때 사용한다. 접속되면 포트 열린 것, 안되면 포트 막힌 것이다.</p>
<blockquote>
<p>단, telnet 실패가 무조건 포트가 닫혀있다는 뜻은 아니다. 포트는 열렸는데 서비스단에서 막아놨을 수도 있으니 추가 확인이 필요하다.</p>
</blockquote>
<hr>
<h2 id="프로세스-상태란">프로세스 상태란?</h2>
<h3 id="hang이란">Hang이란?</h3>
<p>프로세스가 살아있는데 멈춰버린 상태이다.</p>
<pre><code class="language-bash"># 로그 확인으로 Hang 여부 판단
tail -f /var/log/[로그경로]</code></pre>
<p>로그가 계속 찍히면 정상, 멈춰있으면 Hang 의심이다.</p>
<br>

<h3 id="좀비-프로세스란">좀비 프로세스란?</h3>
<p>프로세스가 죽었는데 완전히 사라지지 않고 남아있는 상태이다. 자식 프로세스가 죽었을 때 부모 프로세스가 확인을 안 하면 좀비 상태로 남는다.</p>
<pre><code class="language-bash"># 좀비 프로세스 확인
ps aux | grep Z</code></pre>
<table>
<thead>
<tr>
<th></th>
<th>Hang</th>
<th>좀비</th>
</tr>
</thead>
<tbody><tr>
<td>상태</td>
<td>살아있는데 멈춤</td>
<td>죽었는데 안 사라짐</td>
</tr>
<tr>
<td>ps 상태값</td>
<td>S, R</td>
<td>Z</td>
</tr>
<tr>
<td>CPU</td>
<td>잡아먹을 수 있음</td>
<td>안 먹음</td>
</tr>
</tbody></table>
<hr>
<h2 id="nic와-리눅스-네트워크-명령어란">NIC와 리눅스 네트워크 명령어란?</h2>
<h3 id="nic-network-interface-card란">NIC (Network Interface Card)란?</h3>
<p>NIC란 네트워크에 연결하게 해주는 랜카드이다. 리눅스에서는 <code>ens192</code>, <code>ens33</code> 같은 이름으로 표시된다.</p>
<pre><code class="language-bash"># 네트워크 인터페이스 목록 확인
ip addr</code></pre>
<br>

<h3 id="ip-addr-flush란">ip addr flush란?</h3>
<pre><code class="language-bash">sudo ip addr flush dev ens192</code></pre>
<p>해당 인터페이스에 설정된 IP 주소를 전부 초기화하는 명령어이다. IP 새로 설정하기 전에 기존 IP를 싹 지울 때 사용한다.</p>
<blockquote>
<p>주의: SSH로 접속 중이면 연결이 끊길 수 있다.</p>
</blockquote>
<br>

<h3 id="scp--rp란">scp -rp란?</h3>
<pre><code class="language-bash">scp -rp /home/user/폴더명 root@서버IP:/목적지경로</code></pre>
<table>
<thead>
<tr>
<th>옵션</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>r</code></td>
<td>디렉토리 재귀적으로 복사 (폴더 안에 폴더까지)</td>
</tr>
<tr>
<td><code>p</code></td>
<td>원본 파일 권한/시간 정보 유지하면서 복사</td>
</tr>
</tbody></table>
<hr>
<h2 id="tcpreplay와-pcap이란">tcpreplay와 pcap이란?</h2>
<h3 id="pcap-packet-capture이란">pcap (Packet Capture)이란?</h3>
<p>네트워크에서 오간 패킷을 캡처해서 저장한 파일 형식이다.</p>
<pre><code class="language-bash"># tcpdump로 pcap 파일 생성
tcpdump -i ens192 -w 캡처파일.pcap</code></pre>
<blockquote>
<p>pcap = 네트워크 블랙박스 영상 파일</p>
</blockquote>
<br>

<h3 id="tcpreplay란">tcpreplay란?</h3>
<p>캡처해둔 패킷을 다시 재생(전송)하는 도구이다. 보안 솔루션 테스트할 때 주로 사용한다.</p>
<pre><code class="language-bash"># pcap 파일 재생
tcpreplay -i ens192 캡처파일.pcap

# 속도 조절해서 재생
tcpreplay -i ens192 --mbps=10 캡처파일.pcap</code></pre>
<pre><code>wireshark로 패킷 캡처 (.pcap 파일)
        ↓
tcpreplay로 그 패킷 다시 전송
        ↓
보안 솔루션이 제대로 탐지하는지 확인</code></pre><hr>
<h2 id="ole-vs-ocr이란">OLE vs OCR이란?</h2>
<table>
<thead>
<tr>
<th></th>
<th>OLE</th>
<th>OCR</th>
</tr>
</thead>
<tbody><tr>
<td>의미</td>
<td>Object Linking and Embedding</td>
<td>Optical Character Recognition</td>
</tr>
<tr>
<td>역할</td>
<td>다른 프로그램 객체를 문서에 삽입</td>
<td>이미지 안의 텍스트 인식</td>
</tr>
<tr>
<td>보안 관점</td>
<td>악성코드 숨기는 공격 수단</td>
<td>이미지 안 텍스트 탐지 도구</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>OLE</strong> = 공격자가 악용하는 것<br>
<strong>OCR</strong> = 그걸 탐지하는 도구</p>
</blockquote>
<hr>
<h2 id="마무리">마무리</h2>
<p>오늘은 허브와 스위치의 차이부터 VLAN, 트러블슈팅, 프로세스 상태까지 다양한 개념을 학습하였다. 각각의 개념들이 네트워크 계층과 연결되면서 전체적인 흐름이 조금씩 보이기 시작하였다. 앞으로도 모르는 개념이 나올 때마다 찾아보고 정리하는 습관을 이어나갈 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[하둡(Hadoop) 완전 정복 - 신입 엔지니어의 질문 모음]]></title>
            <link>https://velog.io/@lee_nah/%ED%95%98%EB%91%A1Hadoop-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EC%8B%A0%EC%9E%85-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%9D%98-%EC%A7%88%EB%AC%B8-%EB%AA%A8%EC%9D%8C</link>
            <guid>https://velog.io/@lee_nah/%ED%95%98%EB%91%A1Hadoop-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EC%8B%A0%EC%9E%85-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%9D%98-%EC%A7%88%EB%AC%B8-%EB%AA%A8%EC%9D%8C</guid>
            <pubDate>Tue, 23 Jun 2026 06:47:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글은 여러 포스팅에 뿔뿔이 흩어져 있던 하둡 관련 개념들을 한 곳에 모아 정리하기 위해 작성하였다. 하둡을 배우면서 생겼던 의문들과 그에 대한 해답들을 함께 담았다.</p>
</blockquote>
<h1 id="하둡hadoop-완전-정복---신입-엔지니어의-질문-모음">하둡(Hadoop) 완전 정복 - 신입 엔지니어의 질문 모음</h1>
<h2 id="하둡hadoop이란">하둡(Hadoop)이란?</h2>
<p>하둡(Hadoop)이란 대용량 데이터를 여러 서버에 나눠서 저장하고 처리하는 오픈소스 분산 처리 프레임워크이다.</p>
<p>여기서 오픈소스란 단순히 무료라는 의미가 아니라, 소스코드가 공개되어 있어 우리 환경에 맞게 커스터마이징이 가능하다는 의미이다. 실제로 오픈소스이지만 유료인 경우도 있다. (ex. Red Hat Linux)</p>
<br>

<p>하둡은 크게 세 가지 구성요소로 이루어져 있다.</p>
<pre><code>하둡 (Hadoop)
    ├── HDFS (파일 시스템 - 저장 담당)
    ├── MapReduce (데이터 처리 담당)
    └── YARN (자원 관리 담당)</code></pre><blockquote>
<p>하둡 = 건물 전체, HDFS = 건물 안에 있는 창고</p>
</blockquote>
<hr>
<h2 id="hdfshadoop-distributed-file-system란">HDFS(Hadoop Distributed File System)란?</h2>
<p>HDFS란 하둡 안에서 파일 저장을 담당하는 분산 파일 시스템이다. 대용량 데이터를 여러 서버에 나눠서 저장하는 방식이다.</p>
<br>

<p>데이터가 들어오면 HDFS가 자동으로 블록 단위로 나눠서 저장한다.</p>
<h3 id="hdfs-핵심-특징">HDFS 핵심 특징</h3>
<ul>
<li>데이터를 블록 단위(기본 128MB)로 쪼개서 여러 DataNode에 분산 저장한다.</li>
<li>블록을 3개씩 복제하여 저장하기 때문에, 서버 하나가 죽어도 데이터가 유실되지 않는다.</li>
<li>한 번 쓰면 수정이 어려운 구조로, 읽기에 최적화되어 있다.</li>
</ul>
<br>

<h3 id="hdfs-구조---namenode와-datanode">HDFS 구조 - NameNode와 DataNode</h3>
<table>
<thead>
<tr>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>NameNode</strong></td>
<td>데이터가 어디 저장되어 있는지 관리하는 주체 (메타데이터 담당)</td>
</tr>
<tr>
<td><strong>DataNode</strong></td>
<td>실제 데이터를 저장하는 서버들</td>
</tr>
</tbody></table>
<br>

<p><strong>NameNode</strong>는 전체 파일 시스템의 메타데이터를 관리한다. 어떤 파일이 어느 DataNode에 저장되어 있는지 알고 있는 관리자 역할이다. NameNode가 죽으면 전체 시스템이 위험해진다는 단점이 있다.</p>
<br>

<p><strong>DataNode</strong>는 실제 데이터 블록을 저장하는 서버들이다. DataNode에는 디스크가 여러 개 꽂혀있고, 각 디스크를 마운트하여 그 위에 블록을 저장한다.</p>
<pre><code>DataNode 서버
    하드디스크 1 → /data/disk1 마운트 → 블록 1, 블록 2 저장
    하드디스크 2 → /data/disk2 마운트 → 블록 3, 블록 4 저장</code></pre><br>

<h3 id="datanode-마운트란">DataNode 마운트란?</h3>
<p>마운트란 저장 장치(디스크)를 특정 디렉토리에 연결해서 사용할 수 있게 하는 것이다. 리눅스에서는 디스크를 꽂아도 자동으로 인식되지 않고, 직접 명령어로 연결해줘야 한다.</p>
<blockquote>
<p>마운트 = 책장 칸(디스크)을 연결하는 것<br>
블록 = 책장에 꽂힌 책들</p>
</blockquote>
<br>

<p><strong>DataNode에 마운트가 올라간다는 건?</strong></p>
<p>DataNode에 연결된 디스크 개수가 늘어난다는 뜻이다. 저장 공간 자체가 늘어나는 것으로, 디스크를 추가 구매해야 하므로 비용이 증가한다.</p>
<br>

<p><strong>DataNode에 블록 카운트가 올라간다는 건?</strong></p>
<p>DataNode에 저장된 블록 개수가 늘어난다는 뜻이다. 데이터가 쌓일수록 블록이 생기고 그 개수가 늘어나는 것으로, 저장 공간이 줄어든다.</p>
<table>
<thead>
<tr>
<th></th>
<th>의미</th>
<th>영향</th>
</tr>
</thead>
<tbody><tr>
<td><strong>마운트 증가</strong></td>
<td>디스크 연결 늘어남</td>
<td>비용 증가</td>
</tr>
<tr>
<td><strong>블록 카운트 증가</strong></td>
<td>데이터 쌓임</td>
<td>저장공간 감소</td>
</tr>
</tbody></table>
<br>

<h3 id="raid와-비교">RAID와 비교</h3>
<table>
<thead>
<tr>
<th></th>
<th>RAID</th>
<th>HDFS</th>
</tr>
</thead>
<tbody><tr>
<td>범위</td>
<td>디스크 여러 개</td>
<td>서버 여러 대</td>
</tr>
<tr>
<td>목적</td>
<td>스토리지 안전성 확보</td>
<td>대용량 분산 저장 및 처리</td>
</tr>
</tbody></table>
<hr>
<h2 id="hdfs에서-데이터를-가져올-때">HDFS에서 데이터를 가져올 때</h2>
<p>HDFS에서 데이터를 가져올 때도 여러 서버에서 동시에 가져온다. 이를 <strong>병렬 처리</strong> 또는 <strong>분산 처리</strong>라고 한다.</p>
<pre><code>파일 요청
    ↓
NameNode한테 &quot;이 파일 어디 있어?&quot; 물어봄
    ↓
NameNode가 &quot;서버1, 서버2, 서버3에 있어&quot; 알려줌
    ↓
서버1, 서버2, 서버3에서 동시에 블록 가져옴
    ↓
합쳐서 원본 파일 복원</code></pre><hr>
<h2 id="mapreduce란">MapReduce란?</h2>
<p>MapReduce란 HDFS에 저장된 대용량 데이터를 꺼내서 분석/처리하는 방식이다.</p>
<blockquote>
<p><strong>HDFS</strong> = 창고 (저장만 함)<br>
<strong>MapReduce</strong> = 창고에서 꺼내서 가공하는 공장</p>
</blockquote>
<br>

<h3 id="map-단계">Map 단계</h3>
<p>데이터를 이동시키지 않고 <strong>있는 곳에서 바로 처리</strong>하는 단계이다.</p>
<pre><code>전체 데이터 (1억 줄)
    ↓
서버 1 → 자기 데이터에서 처리
서버 2 → 자기 데이터에서 처리
서버 3 → 자기 데이터에서 처리</code></pre><p>한 곳에 모아서 처리하면 데이터 이동에만 시간이 엄청 걸리기 때문에, 있는 곳에서 바로 처리하는 것이다.</p>
<br>

<h3 id="reduce-단계">Reduce 단계</h3>
<p>각 서버에서 처리한 결과를 <strong>합쳐서 최종 결과를 만드는</strong> 단계이다.</p>
<pre><code>서버 1 결과: &quot;에러 로그&quot; 300개 ─┐
서버 2 결과: &quot;에러 로그&quot; 500개 ─┼──→ 합산 → 최종 결과: 1000개
서버 3 결과: &quot;에러 로그&quot; 200개 ─┘</code></pre><br>

<h3 id="한-줄-요약">한 줄 요약</h3>
<blockquote>
<p><strong>Map</strong> = 데이터 있는 곳에서 바로 처리 (이동 최소화)<br>
<strong>Reduce</strong> = 각자 처리한 결과 합쳐서 최종 답 도출</p>
</blockquote>
<hr>
<h2 id="yarn이란">YARN이란?</h2>
<p>YARN(Yet Another Resource Negotiator)이란 하둡에서 자원(CPU, 메모리)을 나눠주는 관리자이다.</p>
<p>하둡 클러스터에 서버가 여러 대 있고 작업도 여러 개 들어오면, YARN이 어떤 서버에서 어떤 작업을 처리할지 배분해준다.</p>
<blockquote>
<p>서버들 = 직원들<br>
작업들 = 해야 할 업무<br>
YARN = 팀장 (누구한테 어떤 업무 줄지 결정)</p>
</blockquote>
<br>

<h3 id="스케줄링이란">스케줄링이란?</h3>
<p>작업을 언제 어디서 처리할지 순서를 정해주는 것이다. 작업이 동시에 100개 들어오면 YARN이 각 서버에 배분하는 역할을 한다.</p>
<br>

<h3 id="전체-흐름-정리">전체 흐름 정리</h3>
<pre><code>데이터 들어옴
    ↓
HDFS가 자동으로 분산 저장
    ↓
사용자가 분석 요청
    ↓
YARN이 자원 배분
    ↓
MapReduce가 처리 후 결과 반환</code></pre><hr>
<h2 id="har-hadoop-archive란">HAR (Hadoop Archive)란?</h2>
<p>HAR이란 하둡에서 작은 파일 여러 개를 하나로 묶는 것이다.</p>
<br>

<h3 id="왜-필요하냐">왜 필요하냐?</h3>
<p>HDFS는 큰 파일에 최적화되어 있다. 작은 파일이 엄청 많으면 NameNode가 파일 하나하나의 메타데이터를 다 관리해야 하므로 NameNode 메모리 부담이 커지고 성능이 저하된다.</p>
<pre><code>작은 파일 1000개 → NameNode가 1000개 메타데이터 관리 → 부하 큼
        ↓ HAR로 묶기
.har 파일 1개 → NameNode가 몇 개만 관리 → 부하 감소</code></pre><br>

<h3 id="har의-효과">HAR의 효과</h3>
<p><strong>NameNode 관점</strong></p>
<ul>
<li>블록 카운터 감소 → NameNode 메모리 부하 감소 ✅</li>
</ul>
<p><strong>DataNode 관점</strong></p>
<ul>
<li>블록 수는 줄어들지만 실제 디스크 용량은 그대로 ❌</li>
<li>HAR은 DataNode 디스크 용량 확보와는 관계없음</li>
</ul>
<br>

<h3 id="har-단점">HAR 단점</h3>
<ul>
<li>묶인 파일 안에서 특정 파일 찾으려면 인덱스 탐색 필요</li>
<li>파일 하나 수정하려면 전체 다시 풀고 다시 묶어야 함</li>
<li>MapReduce 처리 속도 느려질 수 있음</li>
<li>자주 접근하는 데이터에 사용하면 오히려 성능 저하</li>
</ul>
<blockquote>
<p>자주 안 쓰는 콜드 데이터에 사용하는 것이 적합하다.</p>
</blockquote>
<br>

<h3 id="har-명령어">HAR 명령어</h3>
<pre><code class="language-bash"># HAR 파일 만들기
hadoop archive -archiveName 이름.har -p /입력경로 /출력경로

# HAR 파일 내용 보기
hadoop fs -ls har:///출력경로/이름.har</code></pre>
<br>

<h3 id="crontab으로-har-자동화">crontab으로 HAR 자동화</h3>
<p>HAR 작업을 매번 수동으로 하기 번거롭기 때문에 crontab으로 자동화할 수 있다.</p>
<pre><code class="language-bash"># 매일 새벽 2시에 자동으로 HAR 생성
0 2 * * * hadoop archive -archiveName logs_$(date +%Y%m%d).har -p /logs/input /logs/archive</code></pre>
<blockquote>
<p>crontab = 알람 설정<br>
makehar = 알람 울리면 할 일</p>
</blockquote>
<p>수동으로 실행할 수도 있고, crontab으로 자동화해서 운영할 수도 있다.</p>
<hr>
<h2 id="datanode-디스크-용량-확보-방법">DataNode 디스크 용량 확보 방법</h2>
<p>HAR은 NameNode 부하를 줄이는 용도이지 DataNode 디스크 용량 확보와는 관계없다. DataNode 디스크 용량을 확보하려면 별도의 방법이 필요하다.</p>
<ul>
<li>디스크 추가 마운트 (비용 증가)</li>
<li>오래된 데이터 다른 곳으로 이관 (콜드 스토리지)</li>
<li>오래된 데이터 삭제</li>
<li>압축해서 저장</li>
</ul>
<p>실무에서는 보통 핫 데이터는 HDFS에 그대로 두고, 콜드 데이터는 S3 같은 저렴한 스토리지로 이관하는 방식으로 계층화하여 관리한다.</p>
<hr>
<h2 id="juicefs란">JuiceFS란?</h2>
<p>JuiceFS란 HDFS와 동일한 역할을 하지만 저장소를 클라우드(S3 등 오브젝트 스토리지)로 바꾼 분산 파일 시스템이다.</p>
<blockquote>
<p><strong>JuiceFS = HDFS의 클라우드 버전</strong></p>
</blockquote>
<br>

<h3 id="hdfs-vs-juicefs-구조-비교">HDFS vs JuiceFS 구조 비교</h3>
<pre><code>HDFS
    ├── 메타데이터 → NameNode (전용 서버)
    └── 실제 데이터 → DataNode (전용 서버)

JuiceFS
    ├── 메타데이터 → Redis / MySQL (DB)
    └── 실제 데이터 → S3 (클라우드 스토리지)</code></pre><table>
<thead>
<tr>
<th></th>
<th>HDFS</th>
<th>JuiceFS</th>
</tr>
</thead>
<tbody><tr>
<td>메타데이터</td>
<td>NameNode 전용 서버</td>
<td>Redis, MySQL 등 범용 DB</td>
</tr>
<tr>
<td>실제 데이터</td>
<td>DataNode 전용 서버</td>
<td>S3 같은 클라우드 스토리지</td>
</tr>
<tr>
<td>배포 환경</td>
<td>온프레미스 (데이터센터)</td>
<td>클라우드</td>
</tr>
<tr>
<td>확장성</td>
<td>서버 직접 추가해야 함</td>
<td>클라우드라 탄력적으로 확장 가능</td>
</tr>
</tbody></table>
<br>

<h3 id="s3랑-juicefs의-관계">S3랑 JuiceFS의 관계</h3>
<p>S3는 데이터를 저장하는 스토리지이지만, 파일 시스템이 아니라서 HDFS처럼 쓰기가 불편하다. JuiceFS가 S3 위에 올라타서 S3를 HDFS처럼 사용할 수 있게 만들어주는 역할을 한다.</p>
<blockquote>
<p>S3 = 그냥 하드디스크<br>
JuiceFS = 그 하드디스크를 파일 시스템으로 만들어주는 것</p>
</blockquote>
<br>

<h3 id="hdfs가-클라우드에서-불리한-이유">HDFS가 클라우드에서 불리한 이유</h3>
<p>HDFS는 블록을 3개씩 복제하는 설계 때문에 클라우드 디스크에 배포하면 온프레미스 대비 약 3배의 비용이 발생한다. 반면 JuiceFS는 S3를 기반으로 하여 탄력적으로 확장 가능하고 비용도 절감된다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>하둡은 단순한 저장 시스템이 아니라 HDFS, MapReduce, YARN이 유기적으로 연결된 분산 처리 생태계이다. 처음에는 복잡해 보이지만 각 구성요소의 역할을 이해하면 전체 흐름이 보이기 시작한다. JuiceFS처럼 하둡 생태계를 클라우드 환경에 맞게 발전시킨 기술들도 함께 알아두면 실무에서 큰 도움이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[보안 엔지니어 신입의 기록 - 파티션, 프록시 그리고 DMA]]></title>
            <link>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-%ED%8C%8C%ED%8B%B0%EC%85%98-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DMA</link>
            <guid>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-%ED%8C%8C%ED%8B%B0%EC%85%98-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-DMA</guid>
            <pubDate>Tue, 23 Jun 2026 01:12:18 GMT</pubDate>
            <description><![CDATA[<h1 id="보안-엔지니어-신입의-기록---파티션-프록시-dma-그리고-보안-개념들">보안 엔지니어 신입의 기록 - 파티션, 프록시, DMA 그리고 보안 개념들</h1>
<h2 id="파티션이란">파티션이란?</h2>
<p>파티션이란 하나의 디스크를 논리적으로 나눈 구역이다. 물리적으로는 디스크 1개인데 여러 개처럼 사용하는 것이다.</p>
<br>

<h3 id="파티션을-잡는다는-의미">파티션을 잡는다는 의미</h3>
<p>디스크를 어떻게 나눌지 구역을 설정하는 것이다.</p>
<pre><code>디스크 1TB
    ├── 파티션 1 (200GB) → 운영체제
    ├── 파티션 2 (600GB) → 데이터
    └── 파티션 3 (200GB) → 백업</code></pre><br>


<h3 id="파티션을-나누는-이유">파티션을 나누는 이유</h3>
<p><strong>이점</strong></p>
<ul>
<li>운영체제랑 데이터 분리 → OS 날아가도 데이터 안전</li>
<li>파티션별로 파일 시스템 다르게 설정 가능</li>
<li>특정 파티션만 포맷 가능</li>
<li>로그가 쌓여서 디스크 꽉 차도 다른 파티션에 영향 없음</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>파티션 크기 잘못 잡으면 한쪽은 꽉 차고 한쪽은 남는 낭비 발생</li>
<li>나중에 크기 조정이 번거로움</li>
<li>파티션 너무 많으면 관리 복잡</li>
</ul>
<br>


<h3 id="보안-솔루션-관점에서">보안 솔루션 관점에서</h3>
<p>로그가 엄청 쌓이는 환경이라 로그 파티션을 따로 잡는 것이 중요하다. 로그가 꽉 차서 OS 파티션까지 영향을 주면 서버가 다운될 수 있기 때문이다.</p>
<hr>
<br>


<h2 id="lvm-logical-volume-manager이란">LVM (Logical Volume Manager)이란?</h2>
<p>LVM이란 파티션을 유연하게 관리해서 공간이 부족하면 동적으로 크기를 늘릴 수 있는 기술이다.</p>
<blockquote>
<p>일반 파티션 = 크기가 고정된 방<br>
LVM = 필요하면 벽을 밀어서 방 크기를 늘릴 수 있는 방</p>
</blockquote>
<br>


<h3 id="lvm-설정-순서">LVM 설정 순서</h3>
<p><strong>1. 물리 볼륨 생성 (PV)</strong></p>
<pre><code class="language-bash">pvcreate /dev/sdb</code></pre>
<p><strong>2. 볼륨 그룹 생성 (VG)</strong></p>
<pre><code class="language-bash">vgcreate my_vg /dev/sdb</code></pre>
<p><strong>3. 논리 볼륨 생성 (LV)</strong></p>
<pre><code class="language-bash">lvcreate -L 100G -n my_lv my_vg</code></pre>
<p><strong>4. 파일 시스템 생성 후 마운트</strong></p>
<pre><code class="language-bash">mkfs.ext4 /dev/my_vg/my_lv
mount /dev/my_vg/my_lv /mnt/data</code></pre>
<p><strong>나중에 용량 늘리고 싶으면</strong></p>
<pre><code class="language-bash">lvextend -L +50G /dev/my_vg/my_lv
resize2fs /dev/my_vg/my_lv</code></pre>
<br>


<h3 id="lvm-구조">LVM 구조</h3>
<pre><code>물리 디스크 (PV)
    ↓
볼륨 그룹 (VG) ← 여러 PV 묶음
    ↓
논리 볼륨 (LV) ← 실제 사용하는 공간</code></pre><hr>
<br>


<h2 id="세그먼트-종류란">세그먼트 종류란?</h2>
<p>세그먼트라는 단어는 쓰이는 곳마다 의미가 다르다.</p>
<table>
<thead>
<tr>
<th>세그먼트</th>
<th>어디서 쓰냐</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><strong>네트워크 세그먼트</strong></td>
<td>L2/L3 네트워크</td>
<td>네트워크 구역/영역</td>
</tr>
<tr>
<td><strong>TCP 세그먼트</strong></td>
<td>전송 계층 (L4)</td>
<td>쪼개진 패킷 조각 (데이터 단위)</td>
</tr>
<tr>
<td><strong>메모리 세그먼트</strong></td>
<td>리눅스 메모리</td>
<td>메모리의 특정 구역</td>
</tr>
</tbody></table>
<br>


<h3 id="네트워크-세그먼트">네트워크 세그먼트</h3>
<p>네트워크를 구역별로 나눈 것이다. 현업에서는 보통 네트워크 대역, IP 대역, 서브넷이라고 더 많이 말한다.</p>
<pre><code>회사 네트워크
    ├── 개발팀 세그먼트 (192.168.1.0/24)
    ├── 인사팀 세그먼트 (192.168.2.0/24)
    └── 서버 세그먼트  (192.168.3.0/24)</code></pre><br>


<h3 id="tcp-세그먼트">TCP 세그먼트</h3>
<p>전송 계층(L4)에서 데이터를 쪼갠 조각이다. 목적지에서 재조립하여 원본 데이터를 복원한다.</p>
<br>


<h3 id="메모리-세그먼트">메모리 세그먼트</h3>
<p>프로세스 하나가 실행되면 메모리가 역할별로 나뉜다.</p>
<pre><code>메모리 세그먼트 (상위 개념)
    ├── 코드 세그먼트   → 실행할 코드 저장
    ├── 데이터 세그먼트 → 전역변수 저장
    ├── 스택 세그먼트   → 함수 호출 정보 저장
    └── 힙 세그먼트     → 동적 할당 메모리</code></pre><hr>
<br>


<h2 id="worker-단일복수-스레드란">Worker 단일/복수 스레드란?</h2>
<p>Worker란 작업을 처리하는 단위 프로세스이다.</p>
<pre><code>Worker (프로세스)
    ├── A 스레드 (단일) → a1 → a2 → a3 → a4 순서대로 처리
    ├── B 스레드 (복수) → 여러 스레드가 동시에 처리
    ├── C 스레드 (복수)
    └── D 스레드 (복수)</code></pre><ul>
<li><strong>단일 스레드</strong> : 그 작업을 처리하는 스레드가 1개이다. 순서가 중요한 작업일 때 사용한다.</li>
<li><strong>복수 스레드</strong> : 그 작업을 처리하는 스레드가 여러 개이다. 동시에 처리해야 할 때 사용한다.</li>
</ul>
<p>A가 단일 스레드인 이유는 a1 → a2 → a3 → a4가 순서가 중요한 작업이기 때문이다. 동시에 처리하면 순서가 꼬일 수 있으므로 혼자 순서대로 처리하는 것이다.</p>
<hr>
<br>


<h2 id="yarn이란">YARN이란?</h2>
<p>YARN(Yet Another Resource Negotiator)이란 하둡에서 자원(CPU, 메모리)을 나눠주는 관리자이다.</p>
<p>하둡 클러스터에 서버가 여러 대 있고 작업도 여러 개 들어오면, YARN이 어떤 서버에서 어떤 작업을 처리할지 배분해준다.</p>
<blockquote>
<p>서버들 = 직원들<br>
작업들 = 해야 할 업무<br>
YARN = 팀장 (누구한테 어떤 업무 줄지 결정)</p>
</blockquote>
<br>


<h3 id="스케줄링이란">스케줄링이란</h3>
<p>작업을 언제 어디서 처리할지 순서를 정해주는 것이다. 작업이 동시에 100개 들어오면 YARN이 각 서버에 배분하는 역할을 한다.</p>
<hr>
<br>


<h2 id="프록시proxy란">프록시(Proxy)란?</h2>
<p>프록시란 클라이언트와 서버 사이에서 중간 대리인 역할을 하는 것이다.</p>
<pre><code>클라이언트 → 프록시 → 서버
클라이언트 ← 프록시 ← 서버</code></pre><br>


<h3 id="프록시-종류">프록시 종류</h3>
<p><strong>1. 포워드 프록시 (Forward Proxy)</strong></p>
<p>클라이언트 앞에 있는 프록시로, 클라이언트를 대신해서 서버에 요청을 전달한다.</p>
<pre><code>클라이언트 → 포워드 프록시 → 인터넷 → 서버</code></pre><ul>
<li>클라이언트 IP 숨김</li>
<li>회사에서 직원 인터넷 감시/차단할 때 사용</li>
<li>특정 사이트 접근 우회할 때 사용</li>
</ul>
<p><strong>2. 리버스 프록시 (Reverse Proxy)</strong></p>
<p>서버 앞에 있는 프록시로, 서버를 대신해서 클라이언트 요청을 받는다.</p>
<pre><code>클라이언트 → 인터넷 → 리버스 프록시 → 서버</code></pre><ul>
<li>서버 IP 숨김</li>
<li>로드밸런싱 (여러 서버에 트래픽 분산)</li>
<li>DDoS 방어</li>
<li>Nginx가 대표적인 리버스 프록시</li>
</ul>
<p><strong>3. 투명 프록시 (Transparent Proxy)</strong></p>
<p>클라이언트가 프록시 존재를 모르는 것이다. 설정 없이 자동으로 트래픽을 가로챈다.</p>
<p><strong>4. 익명 프록시 (Anonymous Proxy)</strong></p>
<p>클라이언트 IP를 숨겨주는 프록시이다. VPN과 비슷한 개념이다.</p>
<p><strong>5. SOCKS 프록시</strong></p>
<p>HTTP뿐만 아니라 모든 트래픽을 처리할 수 있는 범용적인 프록시이다.</p>
<p><strong>6. SSL 프록시</strong></p>
<p>HTTPS 트래픽을 복호화해서 내용을 검사하는 프록시이다. 보안 솔루션에서 많이 사용하며 DPI와 연결되는 개념이다.</p>
<br>


<h3 id="암호화-통신에서-프록시가-필요한-이유">암호화 통신에서 프록시가 필요한 이유</h3>
<p>HTTPS, SSH 같은 암호화 통신은 패킷을 가져와도 Key가 없으면 내용을 볼 수 없다. 프록시는 중간에서 양쪽을 속여 Key를 모두 획득한다.</p>
<pre><code>클라이언트 → &quot;나 서버야&quot; (프록시가 속임) → Key 교환
프록시 → &quot;나 클라이언트야&quot; (프록시가 속임) → 서버랑 Key 교환</code></pre><p>이 원리는 중간자 공격(MITM)과 동일하다.</p>
<br>


<h3 id="socket-injection">Socket Injection</h3>
<p>클라이언트가 프록시로 접속하게 만들기 위해 드라이버를 사용자 PC에 설치한다. 이 드라이버가 소켓이 열릴 때 목적지 IP를 프록시 IP로 바꿔치기하는 방식이다.</p>
<hr>
<br>


<h2 id="dma-direct-memory-access란">DMA (Direct Memory Access)란?</h2>
<h3 id="일반적인-패킷-수집-흐름">일반적인 패킷 수집 흐름</h3>
<pre><code>NIC 카드 버퍼 (랜카드)
    ↓ 복사 1번
커널 메모리 (OS)
    ↓ 복사 2번
솔루션 버퍼 (보안 프로그램)</code></pre><p>패킷 하나가 복사를 두 번 당해서 느리고 메모리 낭비가 발생한다.</p>
<br>


<h3 id="dma-방식">DMA 방식</h3>
<p>솔루션이 미리 버퍼를 만들어서 NIC에 전달하면, NIC가 패킷을 해당 버퍼에 직접 쓰는 방식이다.</p>
<pre><code>NIC 카드 버퍼 → 솔루션 버퍼 (복사 1번으로 끝!)</code></pre><blockquote>
<p>택배가 물류센터를 거치지 않고 집으로 바로 오는 것</p>
</blockquote>
<br>


<h3 id="dma-장단점">DMA 장단점</h3>
<table>
<thead>
<tr>
<th></th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>장점</strong></td>
<td>복사 횟수 감소로 속도 향상, 메모리 낭비 감소</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>커널 검사를 거치지 않아 보안 취약, 구현 복잡, NIC 하드웨어 지원 필요</td>
</tr>
</tbody></table>
<hr>
<br>


<h2 id="ole-object-linking-and-embedding이란">OLE (Object Linking and Embedding)이란?</h2>
<p>OLE란 다른 프로그램에서 만든 객체를 문서 안에 삽입하거나 연결하는 기술이다.</p>
<blockquote>
<p>워드 문서 안에 엑셀 표를 넣는 것이 OLE이다.</p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th>Linking (연결)</th>
<th>Embedding (삽입)</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>원본 파일 연결</td>
<td>원본 복사해서 넣음</td>
</tr>
<tr>
<td>원본 수정하면</td>
<td>문서에도 반영됨</td>
<td>반영 안 됨</td>
</tr>
<tr>
<td>파일 크기</td>
<td>작음</td>
<td>커짐</td>
</tr>
</tbody></table>
<br>


<h3 id="보안에서-ole가-중요한-이유">보안에서 OLE가 중요한 이유</h3>
<p>OLE 객체 안에 악성코드를 숨길 수 있다. 워드 파일 안에 OLE 객체로 악성 매크로가 숨어있는 경우가 있으며, 이메일 피싱 공격에서 많이 사용되는 방식이다.</p>
<hr>
<br>


<h2 id="스테가노그래피-steganography란">스테가노그래피 (Steganography)란?</h2>
<p>스테가노그래피란 데이터를 다른 데이터 안에 숨기는 기술이다.</p>
<p>암호화와의 차이점은 다음과 같다.</p>
<ul>
<li><strong>암호화</strong> : 내용을 못 읽게 변환 (숨긴다는 걸 알 수 있음)</li>
<li><strong>스테가노그래피</strong> : 데이터가 숨겨져 있다는 것 자체를 모르게 함</li>
</ul>
<br>


<h3 id="이미지에서-예시">이미지에서 예시</h3>
<p>이미지 픽셀의 색상값 마지막 비트를 살짝 바꿔도 사람 눈으로는 차이를 느끼지 못한다. 그 비트에 데이터를 숨기는 것이다.</p>
<pre><code>원본 픽셀값: 11001010
숨긴 픽셀값: 11001011  ← 마지막 비트만 바꿈</code></pre><p>보안에서는 정상적인 이미지 파일 안에 악성코드를 숨겨서 전송하는 방식으로 악용된다.</p>
<hr>
<br>


<h2 id="ocr-optical-character-recognition이란">OCR (Optical Character Recognition)이란?</h2>
<p>OCR이란 이미지나 스캔된 문서에서 텍스트를 자동으로 인식하는 기술이다.</p>
<blockquote>
<p>손으로 쓴 글씨나 인쇄된 글자를 컴퓨터가 인식해서 텍스트로 변환하는 것</p>
</blockquote>
<p>보안 솔루션에서는 이미지 안에 텍스트로 숨겨진 악성코드를 탐지할 때 OCR을 활용하기도 한다.</p>
<hr>
<br>


<h2 id="메타데이터-종류란">메타데이터 종류란?</h2>
<h3 id="기술-메타데이터-technical-metadata">기술 메타데이터 (Technical Metadata)</h3>
<p>파일의 기술적인 정보를 담고 있는 것이다.</p>
<ul>
<li>이미지 → 해상도, 파일 크기, 색상 정보, 촬영 카메라 모델</li>
<li>동영상 → 코덱, 해상도, 프레임레이트</li>
<li>문서 → 파일 형식, 인코딩 방식</li>
</ul>
<br>


<h3 id="구조-메타데이터-structural-metadata">구조 메타데이터 (Structural Metadata)</h3>
<p>파일이 어떻게 구성되어 있는지 나타내는 것이다.</p>
<ul>
<li>책 → 목차, 챕터 구조, 페이지 번호</li>
<li>데이터베이스 → 테이블 구조, 컬럼 정보</li>
<li>웹페이지 → HTML 태그 구조</li>
</ul>
<br>


<h3 id="관리-메타데이터-administrative-metadata">관리 메타데이터 (Administrative Metadata)</h3>
<p>파일을 관리하기 위한 정보이다.</p>
<ul>
<li>생성일, 수정일, 작성자</li>
<li>저작권 정보</li>
<li>접근 권한</li>
</ul>
<br>


<h3 id="보안에서-메타데이터가-중요한-이유">보안에서 메타데이터가 중요한 이유</h3>
<p>이미지 파일을 보내면서 모른다고 해도 메타데이터에 촬영 위치, 기기 정보, 작성자 정보가 남아있다. 포렌식할 때 메타데이터가 증거로 중요하게 활용된다.</p>
<hr>
<br>


<h2 id="마무리">마무리</h2>
<p>오늘은 스토리지 관리부터 프록시, DMA, 보안 개념까지 다양한 내용을 학습하였다. 각 개념들이 보안 솔루션에서 어떻게 연결되는지 조금씩 보이기 시작하였다. 앞으로도 모르는 개념이 나올 때마다 찾아보고 정리하는 습관을 이어나갈 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[보안 엔지니어 신입의 기록 - DPI, 세션, 소켓통신 그리고 shmget]]></title>
            <link>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-DPI-%EC%84%B8%EC%85%98-%EC%86%8C%EC%BC%93%ED%86%B5%EC%8B%A0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-shmget</link>
            <guid>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-DPI-%EC%84%B8%EC%85%98-%EC%86%8C%EC%BC%93%ED%86%B5%EC%8B%A0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-shmget</guid>
            <pubDate>Fri, 19 Jun 2026 04:03:21 GMT</pubDate>
            <description><![CDATA[<h1 id="각종-개념-정리">각종 개념 정리</h1>
<h2 id="풀패킷-캡처-vs-일반-패킷-수집이란">풀패킷 캡처 vs 일반 패킷 수집이란?</h2>
<p>패킷을 수집하는 방식에는 두 가지가 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>일반 패킷 수집</th>
<th>풀패킷 캡처</th>
</tr>
</thead>
<tbody><tr>
<td>저장 내용</td>
<td>헤더만 (IP, 포트, 프로토콜)</td>
<td>헤더 + 내용(payload) 전부</td>
</tr>
<tr>
<td>예시</td>
<td>&quot;A가 B한테 보냈다&quot;</td>
<td>&quot;A가 B한테 이런 내용을 보냈다&quot;</td>
</tr>
<tr>
<td>용량</td>
<td>작음</td>
<td>매우 큼</td>
</tr>
</tbody></table>
<p>택배로 비유하자면, 일반 패킷 수집은 송장만 보는 것이고, 풀패킷 캡처는 박스를 열어서 내용물까지 확인하는 것이다.</p>
<p>풀패킷 캡처는 용량 부담이 크지만, 침해사고 발생 시 원본 패킷을 재현해야 하거나 법적 증거 및 정밀 포렌식이 필요할 때 반드시 필요하다.</p>
<hr>
<br> 

<h2 id="디지털-포렌식이란">디지털 포렌식이란?</h2>
<p>디지털 포렌식이란 사건이 발생한 후 디지털 증거를 수집하고 분석해서 무슨 일이 있었는지 밝혀내는 것이다.</p>
<p>디지털 데이터는 삭제해도 흔적이 남는다는 원리를 기반으로 한다.</p>
<ul>
<li>파일을 삭제해도 실제 데이터는 디스크에 남아있다.</li>
<li>패킷 캡처본으로 당시 통신을 복원할 수 있다.</li>
<li>로그로 누가 언제 무엇을 했는지 추적할 수 있다.</li>
</ul>
<h3 id="포렌식-흐름">포렌식 흐름</h3>
<pre><code class="language-null">사고 발생
    ↓
증거 수집 (패킷, 로그, 파일, 메모리 등)
    ↓
증거 보존 (원본 훼손 방지)
    ↓
분석 (무슨 일이 있었는지 재구성)
    ↓
보고서 작성 (법적 증거로 활용)</code></pre>
<h3 id="풀패킷-캡처가-포렌식에-필요한-이유">풀패킷 캡처가 포렌식에 필요한 이유</h3>
<p>요약본(로그)만 있으면 &quot;이 IP가 접속했다&quot; 정도만 알 수 있다. 반면 풀패킷이 있으면 &quot;이 IP가 접속해서 이런 데이터를 주고받았다&quot; 까지 복원이 가능하다.</p>
<p>범인이 어떤 공격을 했는지 당시 상황을 그대로 재현할 수 있기 때문에, 풀패킷 캡처는 침해사고 분석과 법적 증거 확보에 있어 핵심적인 역할을 한다.</p>
<br>

<h3 id="풀패킷-데이터는-어떻게-관리할까">풀패킷 데이터는 어떻게 관리할까?</h3>
<p>풀패킷을 무한정 쌓으면 디스크가 감당이 안 되기 때문에, 실무에서는 보존 기간을 정해두고 관리한다.</p>
<pre><code class="language-null">풀패킷 계속 수집
    ↓
30일 지난 데이터 자동 삭제
    ↓
디스크 용량 일정하게 유지</code></pre>
<ul>
<li>보존 기간을 정해두고 일정 기간이 지나면 자동 삭제한다.</li>
<li>법적으로 요구되는 보존 기간이 있는 경우 그에 맞춰서 관리한다.</li>
<li>결국 <strong>용량 vs 보존 기간</strong> 의 트레이드오프이다.</li>
</ul>
<blockquote>
<p>디스크가 넉넉하면 오래 보관하고, 그렇지 않으면 보존 기간을 짧게 가져간다.</p>
</blockquote>
<p>참고로 &quot;삭제해도 흔적이 남는다&quot;는 것과 풀패킷 관리는 별개의 개념이다.</p>
<p>파일을 삭제한다는 것은 실제 데이터를 지우는 것이 아니라, &quot;이 공간을 써도 된다&quot;는 표시만 바꾸는 것이다. 마치 도서관에서 책을 반납했을 때 책 자체가 사라지는 게 아니라 &quot;대출 가능&quot; 표시로 바뀌는 것과 같다. 즉 새로운 데이터가 덮어쓰기 전까지는 원본 데이터가 디스크에 그대로 남아있어 복구가 가능하다.</p>
<p>풀패킷 보존 기간 관리는 이와 무관하게 운영 정책에 따라 의도적으로 삭제하는 것이다.</p>
<br>

<h2 id="dpi-deep-packet-inspection란">DPI (Deep Packet Inspection)란?</h2>
<p>DPI란 패킷이 지나가는 순간 실시간으로 내용을 분석하는 기술이다.</p>
<p>일반적인 패킷 검사는 헤더만 확인하지만, DPI는 헤더와 내용(payload)까지 전부 들여다본다.</p>
<pre><code>패킷 지나감
    ↓
DPI가 실시간으로 내용 분석
    ↓
원본 패킷은 버림
    ↓
분석 결과만 로그로 저장</code></pre><blockquote>
<p>일반 검사 = 편지 봉투만 보는 것 (보낸 사람, 받는 사람)<br>
DPI = 봉투를 열어서 편지 내용까지 읽는 것</p>
</blockquote>
<p>보안 솔루션에서 DPI를 쓰는 이유는 악성코드 탐지, 특정 키워드 필터링, 비정상 트래픽 감지 등은 헤더만 봐서는 알 수 없고 내용까지 분석해야 하기 때문이다.</p>
<hr>
<br> 


<h2 id="세션session이란">세션(Session)이란?</h2>
<p>세션이란 두 대상이 연결되어 있는 시간 동안의 상태를 말한다.</p>
<p>HTTP는 원래 무상태(Stateless) 프로토콜이라 매 요청마다 사용자를 기억하지 못한다. 그래서 로그인 상태를 유지하기 위해 세션을 사용한다.</p>
<pre><code>로그인 성공
    ↓
서버가 세션 ID 발급
    ↓
브라우저가 세션 ID 들고 다님
    ↓
다음 요청에 세션 ID 보내면 서버가 사용자를 알아봄</code></pre><hr>
<br> 


<h2 id="세션-로그란">세션 로그란?</h2>
<p>세션 로그란 누가 언제 어디에 얼마나 접속했는지를 기록하는 것이다.</p>
<p>세션 로그에 남는 정보는 다음과 같다.</p>
<ul>
<li>세션 ID</li>
<li>출발지 IP / 목적지 IP</li>
<li>시작 시간 / 종료 시간</li>
<li>사용한 프로토콜 (TCP, UDP 등)</li>
<li>전송된 데이터 양</li>
<li>접속한 포트</li>
</ul>
<p>풀패킷은 내용을 전부 저장하는 반면, 세션 로그는 접속 정보를 기록하는 것이라 용량이 작고 이상 행동 탐지에 효과적이다.</p>
<hr>
<br> 


<h2 id="세션-관리란">세션 관리란?</h2>
<p>세션 관리란 접속한 사용자의 연결 상태를 추적하고 제어하는 것이다.</p>
<ul>
<li><strong>세션 생성</strong> : 누가 언제 접속했는지 기록한다.</li>
<li><strong>세션 유지</strong> : 접속 중인 상태를 계속 추적한다.</li>
<li><strong>세션 만료</strong> : 일정 시간 이후 자동으로 종료한다.</li>
<li><strong>세션 종료</strong> : 로그아웃 시 완전히 끊는다.</li>
</ul>
<blockquote>
<p>헬스장 입장 관리와 비슷하다.<br>
회원이 들어오면 기록하고(생성), 안에 있는 동안 추적하고(유지), 오래 있으면 퇴장 안내하고(만료), 나가면 기록을 종료한다(종료).</p>
</blockquote>
<br> 


<h3 id="보안에서-세션-관리가-중요한-이유">보안에서 세션 관리가 중요한 이유</h3>
<p>세션 관리가 제대로 되지 않으면 다음과 같은 문제가 발생할 수 있다.</p>
<ul>
<li>로그아웃 후에도 세션 ID 재사용 가능 → 탈취 위험</li>
<li>세션 만료가 없으면 평생 로그인 상태 유지 → 보안 취약</li>
<li>동시 접속 제한이 없으면 세션 하이재킹 감지 불가</li>
</ul>
<p>세션 관리는 세션 하이재킹을 완전히 막는 것이 아니라, 탐지하고 피해를 최소화하는 역할을 한다. 완벽한 보안을 위해서는 세션 관리 + HTTPS + 추가 인증을 함께 사용해야 한다.</p>
<blockquote>
<p>보안은 100% 막는 것이 목표가 아니라, 공격자가 들어오기 어렵게 만들고 들어와도 빠르게 잡아내는 것이 목표이다.</p>
</blockquote>
<hr>
<br> 


<h2 id="세션-하이재킹session-hijacking이란">세션 하이재킹(Session Hijacking)이란?</h2>
<p>세션 하이재킹이란 다른 사람의 세션 ID를 탈취해서 그 사람인 척 하는 공격이다.</p>
<pre><code>피해자가 로그인
        ↓
서버가 세션 ID 발급 (ex. abc123)
        ↓
공격자가 세션 ID 탈취
        ↓
공격자가 abc123 들고 서버에 접속
        ↓
서버는 피해자로 인식</code></pre><p>탈취 방법으로는 네트워크 스니핑, XSS 공격으로 쿠키 탈취, 공개 와이파이에서 가로채기 등이 있다.</p>
<p>동시 접속 제한이 있으면 같은 계정이 두 곳에서 동시 접속할 때 이상을 감지할 수 있어 세션 하이재킹 탐지에 도움이 된다.</p>
<hr>
<br> 


<h2 id="scp-secure-copy-protocol란">SCP (Secure Copy Protocol)란?</h2>
<p>SCP란 파일을 네트워크를 통해 안전하게 전송하는 명령어이다. SSH 암호화를 기반으로 동작한다.</p>
<pre><code class="language-bash"># 내 PC → 서버로 보내기
scp 파일명 user@서버IP:/목적지경로

# 서버 → 내 PC로 받기
scp user@서버IP:/파일경로 ./</code></pre>
<blockquote>
<p>SSH가 전화 통화라면, SCP는 택배를 보내는 것이다.</p>
</blockquote>
<p>WinSCP의 이름도 Windows + SCP에서 나온 것이다.</p>
<hr>
<br> 


<h2 id="fabric이란">Fabric이란?</h2>
<p>Fabric이란 Python 기반의 자동화 배포 툴이다. 여러 서버에 동시에 명령어를 실행하거나 파일을 배포할 수 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>수동 설치</th>
<th>Fabric 설치</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>서버 하나하나 직접 접속해서 설치</td>
<td>스크립트 한 번 실행으로 자동 설치</td>
</tr>
<tr>
<td>서버 1대</td>
<td>비슷함</td>
<td>비슷함</td>
</tr>
<tr>
<td>서버 10대</td>
<td>10번 반복</td>
<td>한 번에 끝</td>
</tr>
</tbody></table>
<blockquote>
<p>수동 설치 = 음식 10인분 손으로 직접 만들기<br>
Fabric 설치 = 레시피 짜놓고 한 번에 10인분 만들기</p>
</blockquote>
<p>보안 솔루션을 여러 고객사 서버에 배포할 때 Fabric을 사용하면 효율적으로 작업할 수 있다.</p>
<hr>
<br> 


<h2 id="소켓-통신-vs-http-통신이란">소켓 통신 vs HTTP 통신이란?</h2>
<h3 id="http-통신">HTTP 통신</h3>
<p>요청하면 응답하고 연결이 종료되는 방식이다.</p>
<pre><code>클라이언트 → &quot;나 이 페이지 줘&quot; → 서버
클라이언트 ← &quot;여기&quot; ← 서버
연결 종료</code></pre><br> 


<h3 id="소켓-통신">소켓 통신</h3>
<p>한 번 연결하면 계속 연결을 유지하는 방식이다.</p>
<pre><code>클라이언트 ↔ 서버 (연결 유지)
클라이언트 → 서버 (언제든 전송 가능)
서버 → 클라이언트 (언제든 전송 가능)</code></pre><blockquote>
<p>HTTP = 편지 주고받기, 보내고 답장 오면 끝<br>
소켓 = 전화 통화, 연결되면 서로 언제든 말할 수 있음</p>
</blockquote>
<br> 


<h3 id="보안-솔루션에서-소켓-통신을-쓰는-이유">보안 솔루션에서 소켓 통신을 쓰는 이유</h3>
<p>로그는 1초에 수백 수천 개가 발생한다. HTTP 통신으로 로그를 수집하면 로그가 생길 때마다 연결을 맺고 끊는 작업을 반복해야 하므로 오버헤드가 엄청나게 크다.</p>
<p>반면 소켓 통신은 한 번 연결해두고 계속 실시간으로 데이터를 흘려보낼 수 있어 로그 수집에 훨씬 효율적이다.</p>
<blockquote>
<p>HTTP = 편지 올 때마다 우체국 갔다오기<br>
소켓 = 전화 연결해두고 계속 말하기</p>
</blockquote>
<hr>
<br> 

<h3 id="keep-alive란">Keep-Alive란?</h3>
<p>소켓 통신은 한 번 연결해두고 계속 유지하는 방식이지만, 네트워크 장비(방화벽, 라우터 등)가 일정 시간 동안 아무 데이터도 오가지 않으면 연결을 강제로 끊어버린다.</p>
<p>여기서 의문이 들었다. 그냥 계속 연결 상태로 설정해두면 되지 않을까? 굳이 Keep-Alive 방식을 써야 하는 이유가 뭘까?</p>
<p>결론부터 말하면 <strong>&quot;그냥 계속 유지&quot;를 네트워크 장비가 허락하지 않는다.</strong></p>
<blockquote>
<p>편의점 자동문이 사람이 서있어도 일정 시간 움직임이 없으면 닫혀버리는 것처럼, 네트워크 장비도 데이터가 없으면 연결을 끊어버린다. Keep-Alive는 주기적으로 신호를 보내 &quot;우리 아직 통신 중이야&quot;라고 알려주는 것이다.</p>
</blockquote>
<p>즉 Keep-Alive 자체가 곧 계속 연결을 유지하는 방법인 것이다.</p>
<pre><code class="language-null">클라이언트 ──────────────────── 서버
     │                           │
     │──── 데이터 전송 ─────────→│
     │                           │
     │   (일정 시간 데이터 없음)  │
     │                           │
     │──── &quot;나 살아있어&quot; ────────→│  ← Keep-Alive 신호
     │←─── &quot;ㅇㅇ 확인&quot; ──────────│
     │                           │
     │──── 데이터 전송 ─────────→│</code></pre>
<p>만약 Keep-Alive 신호에도 응답이 없으면 연결이 끊겼다고 판단하고 자동으로 재연결을 시도한다.</p>
<br>


<h2 id="shmget이란">shmget이란?</h2>
<p>shmget(Shared Memory Get)이란 리눅스에서 공유 메모리 세그먼트를 생성하거나 가져오는 시스템 콜이다.</p>
<blockquote>
<p>여러 프로세스가 같은 메모리 공간을 공유해서 데이터를 주고받을 때 사용하는 함수이다.</p>
</blockquote>
<pre><code class="language-null">프로세스 A ──┐
             ├──→ 공유 메모리 (shmget으로 생성)
프로세스 B ──┘</code></pre>
<br> 

<p>프로세스끼리 데이터를 주고받는 방법은 여러 가지가 있지만, 공유 메모리 방식은 메모리를 직접 공유하기 때문에 속도가 매우 빠르다는 장점이 있다.</p>
<h3 id="보안-솔루션에서-쓰는-이유">보안 솔루션에서 쓰는 이유</h3>
<p>네트워크 패킷이나 로그를 수집할 때는 프로세스 간 데이터 전달이 매우 빠르게 이루어져야 한다. 소켓이나 파이프 방식보다 공유 메모리 방식이 훨씬 빠르기 때문에, 고성능이 요구되는 보안 솔루션에서 shmget을 활용한 공유 메모리 방식을 많이 사용한다.</p>
<br> 

<br>


<h2 id="마무리">마무리</h2>
<p>오늘은 패킷 수집 방식의 차이부터 세션 관리, 프로세스 간 통신까지 다양한 개념을 학습하였다. 하나의 개념이 다른 개념으로 꼬리를 물며 연결되는 것을 느꼈고, 보안 솔루션이 왜 이런 방식으로 설계되었는지 조금씩 이해되기 시작하였다. 앞으로도 모르는 개념이 나올 때마다 찾아보고 정리하는 습관을 이어나갈 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[보안 엔지니어 신입의 기록 - 네트워크부터 분산 저장까지 (Raid, 미러링, NCPS 등)]]></title>
            <link>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EB%B6%80%ED%84%B0-%EB%B6%84%EC%82%B0-%EC%A0%80%EC%9E%A5%EA%B9%8C%EC%A7%80-Raid-%EB%AF%B8%EB%9F%AC%EB%A7%81-NCPS-%EB%93%B1</link>
            <guid>https://velog.io/@lee_nah/%EB%B3%B4%EC%95%88-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%8B%A0%EC%9E%85%EC%9D%98-%EA%B8%B0%EB%A1%9D-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EB%B6%80%ED%84%B0-%EB%B6%84%EC%82%B0-%EC%A0%80%EC%9E%A5%EA%B9%8C%EC%A7%80-Raid-%EB%AF%B8%EB%9F%AC%EB%A7%81-NCPS-%EB%93%B1</guid>
            <pubDate>Wed, 17 Jun 2026 08:58:55 GMT</pubDate>
            <description><![CDATA[<h1 id="네트워크보안-기초-개념-정리">네트워크/보안 기초 개념 정리</h1>
<blockquote>
<p>ojt때 사내 솔루션에 대한 교육을 듣는데, 중간 중간 들은 내용들에서 모르는 기초 개념들이랑 헷갈리는 개념들이 많아 이 글을 통해 정리하고자 작성하게되었다. </p>
</blockquote>
<h2 id="raid란">RAID란?</h2>
<p>RAID(Redundant Array of Independent Disks)란 여러 개의 디스크를 묶어서 하나처럼 사용하는 기술이다. 목적은 크게 두 가지로, 성능 향상과 데이터 안전성 확보이다.</p>
<h3 id="raid-레벨별-특징">RAID 레벨별 특징</h3>
<table>
<thead>
<tr>
<th>레벨</th>
<th>방식</th>
<th>속도</th>
<th>안전성</th>
<th>최소 디스크</th>
</tr>
</thead>
<tbody><tr>
<td>RAID 0</td>
<td>스트라이핑</td>
<td>⭐⭐⭐</td>
<td>❌</td>
<td>2</td>
</tr>
<tr>
<td>RAID 1</td>
<td>미러링</td>
<td>⭐</td>
<td>⭐⭐⭐</td>
<td>2</td>
</tr>
<tr>
<td>RAID 5</td>
<td>스트라이핑 + 패리티 1개</td>
<td>⭐⭐</td>
<td>⭐⭐</td>
<td>3</td>
</tr>
<tr>
<td>RAID 6</td>
<td>스트라이핑 + 패리티 2개</td>
<td>⭐⭐</td>
<td>⭐⭐⭐</td>
<td>4</td>
</tr>
<tr>
<td>RAID 10</td>
<td>미러링 + 스트라이핑</td>
<td>⭐⭐⭐</td>
<td>⭐⭐⭐</td>
<td>4</td>
</tr>
</tbody></table>
<h3 id="핵심-개념">핵심 개념</h3>
<ul>
<li><strong>스트라이핑</strong> : 데이터를 여러 디스크에 나눠서 저장하는 방식이다. RAID 0, 5, 6, 10에서 사용된다.</li>
<li><strong>미러링</strong> : 데이터를 여러 디스크에 동일하게 복사하는 방식이다. RAID 1에서 사용된다.</li>
<li><strong>패리티</strong> : 디스크 장애 시 데이터를 복구하기 위한 복구용 정보이다.</li>
</ul>
<h3 id="외우는-법">외우는 법</h3>
<ul>
<li>RAID 0 = 숫자 0처럼 안전성 제로, 속도만 존재</li>
<li>RAID 1 = 일란성 쌍둥이, 똑같은 데이터 1개 더 보관</li>
<li>RAID 5 = 보험 1개, 패리티 1개로 디스크 1개 장애 복구 가능</li>
<li>RAID 6 = 보험 2개, 패리티 2개로 디스크 2개 장애 복구 가능</li>
<li>RAID 10 = 이름 그대로 RAID 1 + RAID 0</li>
</ul>
<hr>
<h2 id="tar-명령어란">tar 명령어란?</h2>
<p>tar란 리눅스에서 파일을 압축하거나 압축을 해제할 때 사용하는 명령어이다.</p>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-bash">tar zxvf 파일명.tgz</code></pre>
<h3 id="옵션-설명">옵션 설명</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>z</code></td>
<td>gzip으로 압축된 파일 처리 (.tgz, .tar.gz)</td>
</tr>
<tr>
<td><code>x</code></td>
<td>압축 풀기 (extract)</td>
</tr>
<tr>
<td><code>v</code></td>
<td>풀리는 파일 목록 화면에 출력 (verbose)</td>
</tr>
<tr>
<td><code>f</code></td>
<td>다음에 오는 것이 파일명임을 지정</td>
</tr>
</tbody></table>
<blockquote>
<p><code>f</code> 옵션은 반드시 마지막에 위치해야 한다.</p>
</blockquote>
<h3 id="윈도우와-리눅스의-차이">윈도우와 리눅스의 차이</h3>
<p>윈도우에서는 설치 파일(.exe)을 실행하면 해당 위치에 프로그램이 설치된다. 반면 리눅스에서는 tgz 압축을 해제하면 설치용 파일들이 나오고, 그 안의 <code>install.sh</code> 같은 스크립트를 실행하면 <code>/usr/</code>, <code>/opt/</code>, <code>/etc/</code> 같은 정해진 위치에 자동으로 설치된다.</p>
<p>쉽게 말해 tmp 디렉토리는 택배 박스를 뜯는 공간이고, 실제 물건은 집 안 제자리로 들어가는 구조이다.</p>
<hr>
<h2 id="수집과-로깅의-차이란">수집과 로깅의 차이란?</h2>
<p>보안 솔루션을 공부하다 보면 수집과 로깅이라는 단어가 자주 등장한다. 두 개념은 비슷해 보이지만 엄연히 다르다.</p>
<ul>
<li><strong>수집</strong> : 데이터를 여기저기서 가져오는 행위이다.</li>
<li><strong>로깅</strong> : 가져온 데이터를 파일이나 DB에 기록하는 행위이다.</li>
</ul>
<p>수집이 먼저 이루어지고, 그 다음 로깅이 이루어지는 순서이다.</p>
<blockquote>
<p>로그 = 기록된 데이터 (결과물)<br>
로깅 = 로그를 기록하는 행위 (과정)</p>
</blockquote>
<hr>
<h2 id="transfer와-transmit의-차이란">transfer와 transmit의 차이란?</h2>
<p>보안 솔루션에서 데이터 흐름을 설명할 때 자주 등장하는 두 용어이다.</p>
<table>
<thead>
<tr>
<th>용어</th>
<th>의미</th>
<th>느낌</th>
</tr>
</thead>
<tbody><tr>
<td>transmit</td>
<td>데이터를 한 곳에서 내보내는 행위</td>
<td>단방향, 쏘는 것</td>
</tr>
<tr>
<td>transfer</td>
<td>데이터가 A에서 B로 완전히 이동</td>
<td>자리가 바뀌는 것</td>
</tr>
</tbody></table>
<h3 id="보안-솔루션-관점에서의-차이">보안 솔루션 관점에서의 차이</h3>
<ul>
<li><strong>transmit</strong> : 장비에서 발생한 로그를 수집 서버로 전송하는 행위이다. 원본 데이터는 그대로 유지되며 복사되어 전달된다.</li>
<li><strong>transfer</strong> : 수집된 로그 파일 자체가 한 위치에서 다른 위치로 이동하는 행위이다. 파일 분리도 이 단계에서 이루어진다.</li>
</ul>
<hr>
<h2 id="snmp란">SNMP란?</h2>
<p>SNMP(Simple Network Management Protocol)란 네트워크 장비(라우터, 스위치, 서버 등)를 중앙에서 모니터링하고 관리하기 위한 프로토콜이다.</p>
<h3 id="구조">구조</h3>
<table>
<thead>
<tr>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Manager</td>
<td>중앙에서 장비를 모니터링하는 주체</td>
</tr>
<tr>
<td>Agent</td>
<td>각 장비에 설치되어 정보를 전달하는 주체</td>
</tr>
<tr>
<td>MIB</td>
<td>주고받을 정보의 종류를 정의한 목록</td>
</tr>
</tbody></table>
<h3 id="동작-방식">동작 방식</h3>
<ul>
<li><strong>Get</strong> : Manager가 Agent에게 정보를 요청한다.</li>
<li><strong>Set</strong> : Manager가 Agent의 설정을 변경한다.</li>
<li><strong>Trap</strong> : Agent가 이상을 감지했을 때 Manager에게 능동적으로 알림을 보낸다.</li>
</ul>
<p>병원으로 비유하자면, 의사(Manager)가 각 병실(장비)을 돌며 상태를 확인하고, 간호사(Agent)가 보고하는 구조이다. 환자 상태가 위급할 때 간호사가 먼저 연락하는 것이 Trap에 해당한다.</p>
<hr>
<h2 id="ncps란">NCPS란?</h2>
<p>NCPS(Network Content Processing System)란 네트워크로 지나가는 패킷을 캡처하여 내용을 분석하는 시스템이다.</p>
<h3 id="전체-데이터-흐름">전체 데이터 흐름</h3>
<pre><code>네트워크 트래픽 유입
        ↓
NCPS (패킷 캡처 &amp; 분석)
        ↓
Writer (로그 파일 기록)
        ↓
Transfer (파일 이동 &amp; 분리)
        ↓
Decoder (데이터 해석 &amp; 파싱)</code></pre><hr>
<h2 id="스위치-미러포트-미러링와-미러링-탭tap이란">스위치 미러(포트 미러링)와 미러링 탭(TAP)이란?</h2>
<p>네트워크 트래픽을 복사하여 보안 장비나 분석 장비로 보내는 두 가지 방식이다.</p>
<h3 id="스위치-미러-포트-미러링이란">스위치 미러 (포트 미러링)이란?</h3>
<p>스위치에서 지나가는 트래픽을 소프트웨어적으로 복사하여 다른 포트로 보내는 방식이다. 원본 트래픽은 그대로 흐르고, 복사본을 분석 장비로 전달한다.</p>
<h3 id="미러링-탭-tap-test-access-point이란">미러링 탭 (TAP, Test Access Point)이란?</h3>
<p>네트워크 케이블 중간에 물리적으로 끼워서 트래픽을 복사하는 전용 장비이다. 스위치 설정 없이도 트래픽을 그대로 캡처할 수 있다.</p>
<h3 id="비교">비교</h3>
<table>
<thead>
<tr>
<th></th>
<th>스위치 미러 (포트 미러링)</th>
<th>미러링 탭 (TAP)</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>소프트웨어적 복사</td>
<td>물리적 복사</td>
</tr>
<tr>
<td>설치</td>
<td>스위치 설정으로 구성</td>
<td>케이블 중간에 장비 삽입</td>
</tr>
<tr>
<td>트래픽 유실</td>
<td>있을 수 있음</td>
<td>거의 없음</td>
</tr>
<tr>
<td>비용</td>
<td>저렴</td>
<td>비쌈</td>
</tr>
</tbody></table>
<h3 id="고객사가-미러링-탭을-선호하는-이유">고객사가 미러링 탭을 선호하는 이유</h3>
<p>스위치 미러는 스위치가 본래 업무와 트래픽 복사를 동시에 처리하기 때문에, 부하가 높아지면 미러링 트래픽을 먼저 버린다. 즉 스위치 입장에서 미러링은 우선순위가 낮아 유실이 발생할 수 있다.</p>
<p>반면 미러링 탭은 트래픽 복사만을 전담하는 장비라 유실이 거의 없다. 보안 관점에서 패킷 하나라도 유실되면 침해사고를 탐지하지 못할 수 있기 때문에, 비용이 더 비싸더라도 미러링 탭을 선호하는 것이다.</p>
<hr>
<h2 id="하둡hadoop과-hdfs란">하둡(Hadoop)과 HDFS란?</h2>
<h3 id="하둡이란">하둡이란?</h3>
<p>하둡(Hadoop)이란 대용량 데이터를 여러 서버에 나눠서 저장하고 처리하는 오픈소스 분산 처리 프레임워크이다.</p>
<h3 id="hdfs란">HDFS란?</h3>
<p>HDFS(Hadoop Distributed File System)란 하둡 안에서 파일 저장을 담당하는 분산 파일 시스템이다. 하둡이 더 큰 개념이고, HDFS는 그 안의 저장소 역할을 담당한다.</p>
<pre><code class="language-null">하둡 (Hadoop)
    ├── HDFS (파일 시스템 - 저장 담당)
    ├── MapReduce (데이터 처리 담당)
    └── YARN (자원 관리 담당)</code></pre>
<blockquote>
<p>하둡 = 건물 전체, HDFS = 건물 안에 있는 창고</p>
</blockquote>
<h3 id="hdfs-구조">HDFS 구조</h3>
<table>
<thead>
<tr>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>NameNode</strong></td>
<td>데이터가 어디 저장되어 있는지 관리하는 주체 (메타데이터 담당)</td>
</tr>
<tr>
<td><strong>DataNode</strong></td>
<td>실제 데이터를 저장하는 서버들</td>
</tr>
</tbody></table>
<h3 id="hdfs-핵심-특징">HDFS 핵심 특징</h3>
<ul>
<li>데이터를 블록 단위로 쪼개서 여러 DataNode에 분산 저장한다.</li>
<li>블록을 3개씩 복제하여 저장하기 때문에, 서버 하나가 죽어도 데이터가 유실되지 않는다.</li>
<li>한 번 쓰면 수정이 어려운 구조로, 읽기에 최적화되어 있다.</li>
</ul>
<h3 id="raid와-비교">RAID와 비교</h3>
<table>
<thead>
<tr>
<th></th>
<th>RAID</th>
<th>HDFS</th>
</tr>
</thead>
<tbody><tr>
<td>범위</td>
<td>디스크 여러 개</td>
<td>서버 여러 대</td>
</tr>
<tr>
<td>목적</td>
<td>스토리지 안전성 확보</td>
<td>대용량 분산 저장 및 처리</td>
</tr>
</tbody></table>
<hr>
<h2 id="juicefs란">JuiceFS란?</h2>
<p>JuiceFS란 HDFS와 동일한 역할을 하지만 저장소를 클라우드(S3 등 오브젝트 스토리지)로 바꾼 분산 파일 시스템이다.</p>
<h3 id="hdfs-vs-juicefs-구조-비교">HDFS vs JuiceFS 구조 비교</h3>
<pre><code class="language-null">HDFS
    ├── 메타데이터 → NameNode (전용 서버)
    └── 실제 데이터 → DataNode (전용 서버)

JuiceFS
    ├── 메타데이터 → Redis / MySQL (DB)
    └── 실제 데이터 → S3 (클라우드 스토리지)</code></pre>
<table>
<thead>
<tr>
<th></th>
<th>HDFS</th>
<th>JuiceFS</th>
</tr>
</thead>
<tbody><tr>
<td>메타데이터</td>
<td>NameNode 전용 서버</td>
<td>Redis, MySQL 등 범용 DB</td>
</tr>
<tr>
<td>실제 데이터</td>
<td>DataNode 전용 서버</td>
<td>S3 같은 클라우드 스토리지</td>
</tr>
<tr>
<td>배포 환경</td>
<td>온프레미스 (데이터센터)</td>
<td>클라우드</td>
</tr>
<tr>
<td>확장성</td>
<td>서버 직접 추가해야 함</td>
<td>클라우드라 탄력적으로 확장 가능</td>
</tr>
</tbody></table>
<h3 id="s3랑-juicefs의-관계">S3랑 JuiceFS의 관계</h3>
<p>S3는 데이터를 저장하는 스토리지이지만, 파일 시스템이 아니라서 HDFS처럼 쓰기가 불편하다. JuiceFS가 S3 위에 올라타서 S3를 HDFS처럼 사용할 수 있게 만들어주는 역할을 한다.</p>
<blockquote>
<p>S3 = 그냥 하드디스크<br>
JuiceFS = 그 하드디스크를 파일 시스템으로 만들어주는 것</p>
</blockquote>
<h3 id="한-줄-요약">한 줄 요약</h3>
<blockquote>
<p><strong>JuiceFS = HDFS랑 똑같은 역할인데 저장소만 클라우드(S3)로 바꾼 것</strong></p>
</blockquote>
<hr>
<br>

<h2 id="마무리">마무리</h2>
<p>오늘은 기초적인 스토리지 개념부터 네트워크 프로토콜, 그리고 실제 솔루션의 데이터 흐름까지 학습하였다. 생소한 개념들이 많아 어렵게 느껴졌지만, 하나씩 정리하다 보니 전체적인 흐름이 조금씩 보이기 시작하였다. 앞으로도 모르는 개념이 나올 때마다 찾아보고 정리하는 습관을 이어나갈 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB 하나만 쓰면 안될까? Hadoop, Kafka, Redis, S3 등 한번에 정리]]></title>
            <link>https://velog.io/@lee_nah/DB-%ED%95%98%EB%82%98%EB%A9%B4-%EC%95%88%EB%90%A0%EA%B9%8C-Hadoop-Kafka-Redis-S3-%EB%93%B1-%ED%95%9C%EB%B2%88%EC%97%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@lee_nah/DB-%ED%95%98%EB%82%98%EB%A9%B4-%EC%95%88%EB%90%A0%EA%B9%8C-Hadoop-Kafka-Redis-S3-%EB%93%B1-%ED%95%9C%EB%B2%88%EC%97%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 16 Jun 2026 04:38:14 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터-인프라-핵심-개념-정리-hadoop-kafka-elasticsearch-등">데이터 인프라 핵심 개념 정리 (Hadoop, Kafka, Elasticsearch 등)</h1>
<blockquote>
<p>보안 회사에 입사 후 이것저것 개념을 배우게 되었는데, Hadoop, Kafka, DB, Elasticsearch 등 각각의 개념은 알고 있었지만 이것들이 실제로 어떻게 상호작용하는지에 대해서는 깊게 고민해본 적이 없었다. 이번 기회에 그 고민을 정리하고, AI의 도움을 받아 작성한 글이다.</p>
</blockquote>
<br>

<hr>
<h2 id="📦-전체-저장소-한눈에-비교">📦 전체 저장소 한눈에 비교</h2>
<table>
<thead>
<tr>
<th>저장소</th>
<th>역할</th>
<th>특징</th>
<th>데이터 규모</th>
<th>언제 쓸까</th>
</tr>
</thead>
<tbody><tr>
<td><strong>일반 DB</strong> (MySQL, Oracle 등)</td>
<td>원본 데이터 저장</td>
<td>빠른 조회, 정확한 데이터</td>
<td>수천~수천만건</td>
<td>기본 데이터 저장</td>
</tr>
<tr>
<td><strong>Hadoop</strong></td>
<td>대용량 데이터 분산 저장/분석</td>
<td>수억건 이상 처리 가능</td>
<td>수억건 이상</td>
<td>장기 보관, 대규모 분석</td>
</tr>
<tr>
<td><strong>Elasticsearch</strong></td>
<td>검색 특화 저장소</td>
<td>키워드 검색 엄청 빠름</td>
<td>수백만~수억건</td>
<td>검색 기능 구현</td>
</tr>
<tr>
<td><strong>S3 Glacier</strong></td>
<td>초저가 장기 보관</td>
<td>느리지만 저렴</td>
<td>제한 없음</td>
<td>법적 의무 보관 데이터</td>
</tr>
<tr>
<td><strong>Redis</strong></td>
<td>메모리 기반 임시 저장</td>
<td>휘발성, 초고속</td>
<td>소량 (메모리 한계)</td>
<td>캐싱, 중복 체크</td>
</tr>
<tr>
<td><strong>Kafka</strong></td>
<td>메시지 큐</td>
<td>대용량 데이터 스트리밍</td>
<td>실시간 대용량</td>
<td>실시간 데이터 수집/전달</td>
</tr>
</tbody></table>
<br>

<hr>
<h2 id="🐘-hadoop이란">🐘 Hadoop이란</h2>
<p>대용량 데이터를 <strong>여러 서버에 분산 저장하고 처리</strong>하는 시스템이다.</p>
<pre><code>엄청 큰 데이터
→ 여러 서버에 쪼개서 저장 (HDFS)
→ 각 서버에서 동시에 분석 (MapReduce)
→ 결과 합치기</code></pre><br>

<h3 id="hadoop의-핵심-개념-클러스터">Hadoop의 핵심 개념: 클러스터</h3>
<p>여러 서버가 하나처럼 동작하는 묶음을 <strong>클러스터</strong>라고 한다.
Hadoop은 이 클러스터 단위로 데이터를 나눠서 저장한다.</p>
<pre><code>A 클러스터 (데이터 적음)     B 클러스터 (데이터 많음)
├── 서버1                   ├── 서버1
└── 서버2                   ├── 서버2
   (2~3대면 충분)            ├── 서버3
                            ...
                            └── 서버100
                               (수십~수백대 필요)</code></pre><p>데이터가 많을수록 서버를 더 늘리는 것을 <strong>스케일 아웃</strong>이라고 한다.</p>
<br>

<h3 id="hadoop-vs-일반-db">Hadoop vs 일반 DB</h3>
<table>
<thead>
<tr>
<th></th>
<th>일반 DB</th>
<th>Hadoop</th>
</tr>
</thead>
<tbody><tr>
<td>저장 방식</td>
<td>테이블 (행/열)</td>
<td>파일 (분산 저장)</td>
</tr>
<tr>
<td>조회 속도</td>
<td>빠름</td>
<td>느림</td>
</tr>
<tr>
<td>용량</td>
<td>한계 있음</td>
<td>거의 무제한</td>
</tr>
<tr>
<td>적합한 데이터</td>
<td>최근 데이터</td>
<td>수억건 이상 대용량</td>
</tr>
<tr>
<td>비용</td>
<td>서버 1대</td>
<td>서버 수십~수백대</td>
</tr>
</tbody></table>
<br>

<h3 id="hadoop에서-sql처럼-쿼리하기-hive">Hadoop에서 SQL처럼 쿼리하기 (Hive)</h3>
<p>Hadoop은 원래 SQL 문법이 없는데, <strong>Hive</strong>라는 도구를 쓰면 SQL과 거의 동일하게 쿼리할 수 있다.</p>
<pre><code class="language-sql">-- 일반 DB
SELECT * FROM PAMATHS;

-- Hive (Hadoop)
SELECT * FROM PAMATHS;  -- 거의 똑같다</code></pre>
<br>

<hr>
<h2 id="📨-kafka란">📨 Kafka란</h2>
<p><strong>대용량 메시지 큐 시스템</strong>이다. 데이터를 중간에서 받아서 전달해주는 역할을 한다.</p>
<pre><code>[데이터 발생] → [Kafka] → [처리 서버]</code></pre><p>생산자(Producer)가 메시지를 던지면, Kafka가 받아서 쌓아두고, 소비자(Consumer)가 꺼내가는 구조다.</p>
<br>

<h3 id="kafka-vs-redis--둘-다-큐로-쓸-수-있는데-차이가-있을까">Kafka vs Redis — 둘 다 큐로 쓸 수 있는데 차이가 있을까</h3>
<table>
<thead>
<tr>
<th></th>
<th>Redis</th>
<th>Kafka</th>
</tr>
</thead>
<tbody><tr>
<td>용량</td>
<td>작음 (메모리)</td>
<td>대용량 가능</td>
</tr>
<tr>
<td>데이터 보존</td>
<td>휘발성</td>
<td>디스크에 저장</td>
</tr>
<tr>
<td>속도</td>
<td>더 빠름</td>
<td>빠름</td>
</tr>
<tr>
<td>용도</td>
<td>간단한 캐시/큐</td>
<td>대규모 데이터 스트리밍</td>
</tr>
</tbody></table>
<br>

<hr>
<h2 id="🦁-zookeeper란">🦁 Zookeeper란</h2>
<p><strong>Kafka 서버들을 관리하고 조율해주는 도구</strong>다.</p>
<p>Kafka가 여러 서버에 분산돼서 돌아갈 때, 그 서버들이 살아있는지 죽었는지 확인하고 관리해준다.</p>
<pre><code>[Kafka 서버1]  [Kafka 서버2]  [Kafka 서버3]
        ↑            ↑            ↑
               [Zookeeper]
        &quot;얘네 다 살아있나? 관리해줄게&quot;</code></pre><blockquote>
<p>⚠️ Zookeeper는 Hadoop이 아닌 <strong>Kafka</strong>를 관리한다. 처음에 헷갈리기 쉬운 부분이다.</p>
</blockquote>
<p>요즘은 Kafka가 Zookeeper 없이도 동작하도록 업데이트됐지만, 아직 같이 쓰는 곳이 많다.</p>
<br>

<hr>
<h2 id="🔍-elasticsearch란">🔍 Elasticsearch란</h2>
<p><strong>검색 특화 저장소</strong>이다. 일반 DB보다 키워드 검색이 훨씬 빠르다.</p>
<pre><code class="language-sql">-- 일반 DB 검색
SELECT * FROM PAMATHS WHERE NAME LIKE &#39;%이나형%&#39;;
-- → 전체 데이터 다 뒤짐 → 느림

-- Elasticsearch
GET /pamaths/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: { &quot;NAME&quot;: &quot;이나형&quot; }
  }
}
-- → 미리 색인해둠 → 바로 찾음</code></pre>
<br>

<h3 id="elasticsearch는-db에-붙이는-플러그인일까">Elasticsearch는 DB에 붙이는 플러그인일까</h3>
<p>처음에 이 부분에 대한 고민을 해보았는데, 정답은 아니었다.
Elasticsearch는 DB에 붙이는 것이 아닌 <strong>독립적인 저장소</strong>이다.</p>
<pre><code>[DB] ──→ 데이터 복사 ──→ [Elasticsearch]
  원본 저장                검색용으로 따로 저장
                               ↑
                          [백엔드 API] ← [UI에서 검색]</code></pre><p>DB는 원본 저장, Elasticsearch는 검색용 복사본이라고 생각하면 된다.</p>
<br>

<hr>
<h2 id="⚡-redis란">⚡ Redis란</h2>
<p><strong>메모리 기반 초고속 저장소</strong>이다. 휘발성이라 서버가 꺼지면 데이터가 날아간다.</p>
<p>컴퓨터 구조로 비유하면 아래와 같다.</p>
<blockquote>
<p>Redis = RAM (빠르고 휘발성)
일반 DB = HDD/SSD (느리고 영구적)</p>
</blockquote>
<br>

<h3 id="redis가-하는-일이-너무-많은-것-아닐까">Redis가 하는 일이 너무 많은 것 아닐까</h3>
<p>Redis를 처음 접했을 때 하는 일이 너무 많아서 만능처럼 느껴졌는데, 핵심은 하나이다.</p>
<blockquote>
<p>메모리에 데이터를 저장하는 초고속 저장소</p>
</blockquote>
<p>나머지는 그 특성 덕분에 파생된 역할들이다.</p>
<ul>
<li>빠르니까 → <strong>캐싱</strong></li>
<li>빠르니까 → <strong>세션 저장소</strong></li>
<li>빠르니까 → <strong>중복 체크 (멱등성 보장)</strong></li>
<li>빠르니까 → <strong>메시지 큐</strong></li>
</ul>
<br>

<h3 id="멱등성이란">멱등성이란</h3>
<blockquote>
<p>같은 작업을 여러 번 해도 결과가 1번 한 것과 같은 것</p>
</blockquote>
<pre><code>같은 로그가 네트워크 오류로 3번 전송됨
→ 멱등성 없으면: 로그 3개 저장됨 (중복)
→ 멱등성 있으면: 1개만 저장됨 (정상)</code></pre><p>Redis가 &quot;이미 처리한 ID&quot;를 기억해둬서 중복을 막아주는 방식으로 멱등성을 보장한다.</p>
<br>

<hr>
<h2 id="☁️-s3-glacier란">☁️ S3 Glacier란</h2>
<p>AWS S3의 <strong>초저가 장기 보관 전용</strong> 버전이다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Hadoop</th>
<th>S3 Glacier</th>
</tr>
</thead>
<tbody><tr>
<td>비용</td>
<td>서버 유지비 비쌈</td>
<td>엄청 저렴</td>
</tr>
<tr>
<td>관리</td>
<td>직접 서버 관리</td>
<td>AWS가 다 관리</td>
</tr>
<tr>
<td>조회 속도</td>
<td>그나마 빠름</td>
<td>느림 (몇 시간)</td>
</tr>
<tr>
<td>용도</td>
<td>저장 + 분석</td>
<td>보관만</td>
</tr>
</tbody></table>
<p>단점은 데이터 꺼내는 데 몇 시간씩 걸린다는 것이다. 자주 꺼내볼 데이터엔 맞지 않고, <strong>법적으로 보관만 해야 하는</strong> 데이터에 적합하다.</p>
<br>

<hr>
<h2 id="🏗️-실제로-다-같이-쓰면-어떻게-될까">🏗️ 실제로 다 같이 쓰면 어떻게 될까</h2>
<p>보안 로그 시스템을 예시로 들면 아래와 같다.</p>
<pre><code>로그 발생
    ↓
[Kafka] → 대량 로그 안전하게 받아서 쌓아둠
    ↓
[Redis] → 중복 로그 체크 (멱등성 보장)
    ↓
[DB] → 최근 1달 로그 저장 (빠른 조회용)
[Elasticsearch] → 키워드 검색용
[S3 Glacier] → 1년 이상 로그 장기 보관</code></pre><p>각각 역할이 달라서 겹치지 않는다. 마트로 비유하면 아래와 같다.</p>
<ul>
<li><strong>DB</strong> = 냉장고 (자주 쓰는 것)</li>
<li><strong>Elasticsearch</strong> = 검색대 (빠르게 찾기)</li>
<li><strong>Hadoop / S3 Glacier</strong> = 창고 (오래된 것 보관)</li>
<li><strong>Redis</strong> = 계산대 옆 바구니 (잠깐 올려두는 곳)</li>
<li><strong>Kafka</strong> = 물류 트럭 (데이터 실어 나르는 것)</li>
</ul>
<br>

<hr>
<h2 id="공부하면서-헷갈렸던-것들">공부하면서 헷갈렸던 것들</h2>
<br>

<h3 id="hadoop이-대용량-데이터를-처리한다고-했는데-그러면-빠른-것일까">Hadoop이 대용량 데이터를 처리한다고 했는데, 그러면 빠른 것일까</h3>
<p>이 부분에 대해 고민해보았는데, Hadoop이 빠른 것이 아니었다.</p>
<p>빠른 게 아니라, 혼자였으면 불가능한 것을 여럿이 나눠서 그나마 할 수 있다는 개념이다.</p>
<pre><code>일반 DB 서버 1대 → 1억건 혼자 처리 → 불가능
Hadoop 서버 100대 → 각자 100만건씩 나눠서 동시 처리 → 가능</code></pre><br>

<h3 id="몇만건-데이터-조회가-30분-걸리면-hadoop을-써야-할까">몇만건 데이터 조회가 30분 걸리면 Hadoop을 써야 할까</h3>
<p>이에 대한 고민을 해보았는데, 정답은 아니었다. 몇만건은 Hadoop을 쓰기엔 너무 작은 규모이다.</p>
<p>진짜 원인은 따로 있을 가능성이 높다.</p>
<ul>
<li>인덱스가 없는 경우 → 인덱스 추가로 해결</li>
<li>쿼리가 비효율적인 경우 → 쿼리 튜닝으로 해결</li>
<li>서버 사양이 낮은 경우 → 서버 업그레이드</li>
</ul>
<p>Hadoop은 <strong>수억건 이상</strong>일 때 고려하는 것이다.</p>
<br>

<h3 id="zookeeper가-hadoop-서버를-감시하는-것일까">Zookeeper가 Hadoop 서버를 감시하는 것일까</h3>
<p>처음에 Zookeeper가 Hadoop 서버를 감시한다고 생각했는데, 이는 잘못된 이해였다.
Zookeeper는 <strong>Kafka</strong> 서버들을 관리한다. Hadoop과는 직접적인 관계가 없다.</p>
<br>

<h3 id="elasticsearch는-db에-붙이는-플러그인일까-1">Elasticsearch는 DB에 붙이는 플러그인일까</h3>
<p>처음에 이 부분에 대한 고민을 해보았는데, 정답은 아니었다.
Elasticsearch는 DB에 붙이는 것이 아닌 <strong>독립적인 저장소</strong>이다.
DB의 데이터를 복사해서 Elasticsearch에 따로 저장해두고, 검색할 땐 DB 말고 Elasticsearch에 물어보는 방식이다.</p>
<br>

<h3 id="저장소가-db-hadoop-elasticsearch-s3-glacier-다-따로-있으면-너무-많은-것-아닐까">저장소가 DB, Hadoop, Elasticsearch, S3 Glacier 다 따로 있으면 너무 많은 것 아닐까</h3>
<p>이에 대한 고민을 해보았는데, 다 역할이 다르기 때문에 어쩔 수 없다는 결론이 나왔다.
각자 잘하는 것이 다르기 때문에 용도에 맞게 골라 쓰는 것이다.</p>
<br>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[보안솔루션 엔지니어가 알아야 할 네트워크 장비와 보안 개념 정리]]></title>
            <link>https://velog.io/@lee_nah/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%A5%EB%B9%84-%EB%B3%B4%EC%95%88-%EA%B0%9C%EB%85%90-%EA%B3%B5%EB%B6%80</link>
            <guid>https://velog.io/@lee_nah/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%A5%EB%B9%84-%EB%B3%B4%EC%95%88-%EA%B0%9C%EB%85%90-%EA%B3%B5%EB%B6%80</guid>
            <pubDate>Tue, 09 Jun 2026 11:34:11 GMT</pubDate>
            <description><![CDATA[<h1 id="네트워크-장비와-보안-개념-정리">네트워크 장비와 보안 개념 정리</h1>
<blockquote>
<p>보안 솔루션 엔지니어로 새롭게 취업하게 되었다. 그동안 배워왔던 클라우드/인프라 개념과는 결이 조금 다른, 네트워크 장비와 보안 기술들을 이번 기회에 한번 제대로 정리해보았다.</p>
</blockquote>
<hr>
<h1 id="🌐-네트워크-장비">🌐 네트워크 장비</h1>
<h2 id="방화벽-firewall">방화벽 (Firewall)</h2>
<p>방화벽은 네트워크 트래픽을 <strong>허용(Allow)</strong> 또는 <strong>차단(Deny)</strong> 하는 보안 장비다.
미리 정의된 정책(Rule)에 따라 출발지 IP, 목적지 IP, 포트, 프로토콜을 기준으로 패킷을 필터링한다.</p>
<p>Linux 환경에서는 <code>iptables</code> 또는 <code>nftables</code>를 이용해 소프트웨어 방화벽을 구성할 수 있다.</p>
<pre><code class="language-bash"># 특정 포트 허용 예시 (iptables)
iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# 특정 IP 차단
iptables -A INPUT -s 192.168.1.100 -j DROP</code></pre>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>패킷 필터링</td>
<td>IP/Port 기반 단순 차단</td>
</tr>
<tr>
<td>상태 기반(Stateful)</td>
<td>세션 상태를 추적해 허용된 연결의 응답 트래픽은 자동 허용</td>
</tr>
<tr>
<td>애플리케이션 방화벽(WAF)</td>
<td>HTTP/HTTPS 레벨까지 분석, 웹 공격 차단</td>
</tr>
</tbody></table>
<hr>
<h2 id="ids--ips">IDS / IPS</h2>
<h3 id="ids-intrusion-detection-system--침입-탐지-시스템">IDS (Intrusion Detection System) — 침입 탐지 시스템</h3>
<p>IDS는 <strong>Intrusion Detection System</strong>의 약자로, 네트워크 또는 시스템에서 발생하는 <strong>비정상적인 행위를 탐지하고 관리자에게 알림</strong>을 주는 시스템이다.
직접 차단하지는 않고 <strong>탐지 및 경보</strong> 역할만 수행한다.</p>
<blockquote>
<p>🔍 비유하자면, CCTV처럼 이상한 행동을 감지하고 알려주지만 직접 막지는 않는다.</p>
</blockquote>
<h3 id="ips-intrusion-prevention-system--침입-차단-시스템">IPS (Intrusion Prevention System) — 침입 차단 시스템</h3>
<p>IPS는 <strong>Intrusion Prevention System</strong>의 약자로, IDS의 탐지 기능에 <strong>능동적인 차단 기능</strong>이 추가된 시스템이다.
이상 트래픽을 감지하면 <strong>실시간으로 세션을 끊거나 패킷을 드롭</strong>한다.</p>
<blockquote>
<p>🛡️ 비유하자면, 경비원처럼 이상한 행동을 감지하는 동시에 직접 제지한다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>구분</th>
<th>탐지</th>
<th>차단</th>
<th>배치 위치</th>
</tr>
</thead>
<tbody><tr>
<td>IDS</td>
<td>✅</td>
<td>❌</td>
<td>네트워크 미러링(Out-of-band)</td>
</tr>
<tr>
<td>IPS</td>
<td>✅</td>
<td>✅</td>
<td>트래픽 경로 상(In-line)</td>
</tr>
</tbody></table>
<hr>
<h2 id="l2--l3-스위치">L2 / L3 스위치</h2>
<h3 id="l2-스위치-데이터링크-계층">L2 스위치 (데이터링크 계층)</h3>
<p>L2 스위치는 <strong>MAC 주소 기반으로 동작</strong>하는 스위치다.
같은 네트워크(동일 서브넷) 내의 장비들이 통신할 수 있도록 프레임을 전달한다.
MAC 주소 테이블을 학습해 목적지 포트로만 트래픽을 전송하며, <strong>VLAN 구성</strong>이 가능하다.</p>
<h3 id="l3-스위치-네트워크-계층">L3 스위치 (네트워크 계층)</h3>
<p>L3 스위치는 <strong>IP 주소 기반으로 라우팅 기능까지 수행</strong>하는 스위치다.
서로 다른 서브넷(VLAN 간) 통신을 처리할 수 있어, 소규모~중규모 네트워크에서 라우터 역할을 대신하기도 한다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>기준</th>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>L2 스위치</td>
<td>MAC 주소</td>
<td>동일 서브넷 통신, VLAN</td>
</tr>
<tr>
<td>L3 스위치</td>
<td>IP 주소</td>
<td>VLAN 간 라우팅, 정적/동적 라우팅</td>
</tr>
</tbody></table>
<hr>
<h2 id="sip-session-initiation-protocol">SIP (Session Initiation Protocol)</h2>
<p>SIP는 <strong>VoIP(인터넷 전화), 영상통화</strong> 등 멀티미디어 세션을 개시·종료하는 <strong>애플리케이션 계층 프로토콜</strong>이다.
텍스트 기반 프로토콜로 HTTP와 구조가 유사하며, 기본 포트는 <strong>UDP/TCP 5060</strong>, TLS 사용 시 <strong>5061</strong>을 사용한다.</p>
<p><strong>기본 흐름 (콜 설정)</strong></p>
<pre><code>Client --INVITE--&gt;  Server
Client &lt;--100 Trying--
Client &lt;--180 Ringing--
Client &lt;--200 OK--
Client --ACK--&gt;     Server
         (통화 중)
Client --BYE--&gt;     Server
Client &lt;--200 OK--</code></pre><p>보안 관점에서는 SIP Flooding, SIP Spoofing 같은 공격이 발생할 수 있어, <strong>SBC(Session Border Controller)</strong> 장비로 보호하는 경우가 많다.</p>
<hr>
<h2 id="tap--포트-미러링">TAP / 포트 미러링</h2>
<p>보안 장비(IDS, 패킷 분석 등)가 트래픽을 <strong>수집</strong>하기 위해 사용하는 방법이다.</p>
<h3 id="network-tap-test-access-point">Network TAP (Test Access Point)</h3>
<p>물리 장비로, 네트워크 케이블 사이에 인라인으로 설치해 트래픽을 <strong>복사해서 별도 장비로 전달</strong>한다.
원본 트래픽에 영향을 주지 않으며, 전원이 꺼져도 통신이 유지된다는 장점이 있다.</p>
<h3 id="port-mirroring-span">Port Mirroring (SPAN)</h3>
<p>스위치 기능으로, 특정 포트의 트래픽을 <strong>다른 모니터링 포트로 복사</strong>한다.
별도 장비 없이 설정만으로 구성 가능하지만, 스위치 부하가 증가할 수 있다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Network TAP</th>
<th>Port Mirroring (SPAN)</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>물리 장비</td>
<td>스위치 소프트웨어 설정</td>
</tr>
<tr>
<td>트래픽 영향</td>
<td>없음</td>
<td>스위치 부하 발생 가능</td>
</tr>
<tr>
<td>비용</td>
<td>높음</td>
<td>낮음 (스위치 기능 활용)</td>
</tr>
</tbody></table>
<hr>
<h2 id="nas-network-attached-storage">NAS (Network Attached Storage)</h2>
<p>NAS는 <strong>네트워크에 연결된 파일 스토리지 장비</strong>다.
여러 서버/클라이언트가 네트워크를 통해 공유 스토리지에 접근할 수 있도록 한다.</p>
<p><strong>주요 프로토콜</strong></p>
<table>
<thead>
<tr>
<th>프로토콜</th>
<th>설명</th>
<th>주로 사용 환경</th>
</tr>
</thead>
<tbody><tr>
<td>NFS</td>
<td>Network File System</td>
<td>Linux/Unix 환경</td>
</tr>
<tr>
<td>SMB/CIFS</td>
<td>Server Message Block</td>
<td>Windows 환경</td>
</tr>
<tr>
<td>iSCSI</td>
<td>블록 스토리지 접근</td>
<td>DB 서버 등 고성능</td>
</tr>
</tbody></table>
<p><strong>SAN vs NAS</strong></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>NAS</th>
<th>SAN</th>
</tr>
</thead>
<tbody><tr>
<td>접근 방식</td>
<td>파일 단위</td>
<td>블록 단위</td>
</tr>
<tr>
<td>프로토콜</td>
<td>NFS, SMB</td>
<td>iSCSI, Fibre Channel</td>
</tr>
<tr>
<td>구축 비용</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>성능</td>
<td>보통</td>
<td>고성능</td>
</tr>
</tbody></table>
<p>보안 관점에서는 <strong>접근 제어(ACL), 스냅샷, 암호화</strong> 설정이 중요하다.</p>
<hr>
<h1 id="🔐-보안-기술">🔐 보안 기술</h1>
<h2 id="패킷-스니핑--스푸핑">패킷 스니핑 / 스푸핑</h2>
<h3 id="스니핑-sniffing">스니핑 (Sniffing)</h3>
<p>(이 개념은 조금 어렵게 느껴져서 ai의 도움을 많이 받았다.)
스니핑은 네트워크 상에서 <strong>자신에게 오지 않는 패킷을 몰래 엿보는 행위</strong>다.</p>
<p>네트워크에서 패킷은 목적지 주소가 적혀 있어서, 원래는 <strong>내 컴퓨터로 온 패킷만 수신</strong>한다.
그런데 랜카드(NIC)를 <strong>Promiscuous Mode(무차별 모드)</strong> 로 설정하면,
목적지가 나 아닌 패킷도 전부 수신하도록 필터를 꺼버릴 수 있다.</p>
<p>이를 이용해 같은 네트워크 대역에 흘러다니는 트래픽을 몽땅 캡처하는 것이 스니핑이다.
대표적인 도구로는 <code>tcpdump</code>, <code>Wireshark</code>가 있다.</p>
<blockquote>
<p>이 글로만 보면 tcpdump와 Wireshark는 나쁜 해킹 도구인가? 했는데, 찾아보니 <strong>양날의 검</strong>이라는 것을 알 수 있었다.
예를 들어, 다음과 같은 용도에서 tcpdump와 Wireshark는 사용이 가능하다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>상황</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>네트워크 디버깅</strong></td>
<td>&quot;왜 이 서버가 응답을 안 하지?&quot; 패킷 확인</td>
</tr>
<tr>
<td><strong>보안 모니터링</strong></td>
<td>비정상 트래픽 탐지, 침입 흔적 분석</td>
</tr>
<tr>
<td><strong>개발/테스트</strong></td>
<td>API 통신이 제대로 가는지 확인</td>
</tr>
<tr>
<td><strong>보안 공부</strong></td>
<td>패킷 구조 학습, 네트워크 흐름 이해</td>
</tr>
</tbody></table>
<p>반면, 공격자가 <strong>허가 없이</strong> 남의 네트워크에서 사용하면 스니핑 공격이 된다.
칼이 요리도구냐 흉기냐는 누가, 어디서, 어떤 목적으로 쓰느냐에 달린 것처럼 tcpdump와 Wireshark도 마찬가지인 것이다. </p>
<h3 id="스푸핑-spoofing">스푸핑 (Spoofing)</h3>
<p><strong>출발지 주소를 위조</strong>해 다른 시스템인 척 통신하는 공격이다.</p>
<table>
<thead>
<tr>
<th>종류</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>IP Spoofing</td>
<td>출발지 IP 위조</td>
</tr>
<tr>
<td>ARP Spoofing</td>
<td>ARP 응답을 위조해 트래픽을 가로챔 (MITM 공격의 기반)</td>
</tr>
<tr>
<td>DNS Spoofing</td>
<td>DNS 응답을 위조해 잘못된 IP로 유도</td>
</tr>
</tbody></table>
<h2 id="arp-스푸핑--mitm">ARP 스푸핑 / MITM</h2>
<p><strong>ARP(Address Resolution Protocol)</strong> 는 IP 주소를 MAC 주소로 변환하는 프로토콜이다.
ARP는 인증이 없기 때문에, 공격자가 <strong>위조된 ARP Reply를 브로드캐스트</strong>하면 피해자의 ARP 테이블을 오염시킬 수 있다.</p>
<p>이를 통해 <strong>MITM(Man In The Middle)</strong> 공격이 가능해진다.</p>
<pre><code>정상:  피해자 ---&gt; 게이트웨이
공격:  피해자 ---&gt; 공격자 ---&gt; 게이트웨이  (공격자가 중간에서 트래픽 감청)</code></pre><p><strong>대응 방법</strong></p>
<ul>
<li>스위치의 <strong>Dynamic ARP Inspection (DAI)</strong> 기능 활성화</li>
<li>정적 ARP 엔트리 설정</li>
<li>IDS/IPS로 비정상 ARP 탐지</li>
</ul>
<br>

<hr>
<h1 id="추가-글-패킷을-떠본다의-의미">추가 글 (패킷을 떠본다의 의미)</h1>
<p>스니핑 도구에서 tcpdump를 보면서 패킷을 떠본다는 것이 어떤 의미인지 잘 납득이 가질 않았다. 그래서 이해하기 위해 글을 작성하게 되었다.</p>
<br>



<h2 id="🔍-그래서-패킷을-떠본다는-게-뭔데">🔍 그래서 패킷을 &quot;떠본다&quot;는 게 뭔데?</h2>
<p>처음에 이 말이 와닿지 않았다. 패킷을 떠본다는 게 도대체 뭘 보겠다는 건지.</p>
<p>쉽게 말하면 이렇다.</p>
<blockquote>
<p>네트워크는 <strong>편지</strong>가 오가는 우체국 같은 곳이고,
패킷은 그 <strong>편지 한 장 한 장</strong>이다.
tcpdump는 그 편지들을 중간에서 <strong>복사해서 내용을 들여다보는 행위</strong>다.</p>
</blockquote>
<hr>
<h3 id="실제로-어떤-정보가-보이나">실제로 어떤 정보가 보이나?</h3>
<p>아래는 tcpdump를 실행했을 때 나오는 출력 예시다.</p>
<pre><code class="language-bash">$ sudo tcpdump -i eth0</code></pre>
<blockquote>
<p><code>-i</code>는 interface의 약자로, 어떤 네트워크 인터페이스(랜카드)에서 패킷을 캡처할지 지정하는 옵션이다.
<code>eth0</code>은 리눅스에서 첫 번째 유선 랜카드를 부르는 이름이다. (ethernet 0번)
쉽게 말해 <strong>&quot;현관문, 뒷문, 창문 중 어느 쪽을 감시할지&quot;</strong> 고르는 것과 같다.
요즘 리눅스는 <code>eth0</code> 대신 <code>ens33</code>, <code>enp0s3</code> 같은 이름을 쓰기도 하니, <code>ip a</code> 명령어로 먼저 내 인터페이스 이름을 확인한다.</p>
</blockquote>
<p>위 명령어를 실행하면 다음과 같이 나온다. </p>
<pre><code>14:23:01.123456 IP 192.168.1.10.54321 &gt; 142.250.196.142.443: Flags [S], seq 123456, win 64240
14:23:01.124000 IP 142.250.196.142.443 &gt; 192.168.1.10.54321: Flags [S.], seq 654321, ack 123457
14:23:01.124100 IP 192.168.1.10.54321 &gt; 142.250.196.142.443: Flags [.], ack 654322</code></pre><p>한 줄이 패킷 하나다. 처음 보면 암호처럼 느껴지는데, 구조를 알면 읽힌다.</p>
<pre><code>[시간]  [출발지IP.포트]  &gt;  [목적지IP.포트]  [플래그]
14:23:01  192.168.1.10.54321  &gt;  142.250.196.142.443  Flags [S]</code></pre><table>
<thead>
<tr>
<th>항목</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>192.168.1.10</code></td>
<td>내 컴퓨터 IP</td>
</tr>
<tr>
<td><code>142.250.196.142</code></td>
<td>목적지 서버 IP (구글 등)</td>
</tr>
<tr>
<td><code>.443</code></td>
<td>HTTPS 포트 → 웹 통신</td>
</tr>
<tr>
<td><code>Flags [S]</code></td>
<td>SYN → 연결 요청</td>
</tr>
<tr>
<td><code>Flags [S.]</code></td>
<td>SYN-ACK → 연결 수락</td>
</tr>
<tr>
<td><code>Flags [.]</code></td>
<td>ACK → 연결 완료</td>
</tr>
</tbody></table>
<p>위 3줄이 바로 <strong>TCP 3-way handshake</strong> 과정이다.</p>
<hr>
<h3 id="뭘-분석하려고-쓰는-건데">뭘 분석하려고 쓰는 건데?</h3>
<p>실무에서 tcpdump를 꺼내드는 상황은 보통 이렇다.</p>
<p><strong>1. 통신이 되는지 안 되는지 확인할 때</strong></p>
<pre><code class="language-bash">sudo tcpdump -i eth0 host 192.168.1.1</code></pre>
<p>특정 IP와 패킷이 오가는지 눈으로 확인한다. 패킷이 아예 안 잡히면 네트워크 단에서 막힌 것.</p>
<p><strong>2. 특정 포트로 트래픽이 들어오는지 확인할 때</strong></p>
<pre><code class="language-bash">sudo tcpdump -i eth0 port 443</code></pre>
<p>HTTPS 트래픽만 필터링해서 본다.</p>
<p><strong>3. 비정상적인 트래픽을 잡을 때</strong>
평소에 없던 IP로 대량의 패킷이 나가고 있다면? 악성코드나 공격 징후일 수 있다.</p>
<hr>
<h3 id="wireshark는-뭐가-다른데">Wireshark는 뭐가 다른데?</h3>
<p>tcpdump가 <strong>터미널에서 텍스트로</strong> 패킷을 보여준다면,
Wireshark는 같은 정보를 <strong>GUI로 시각화</strong>해서 보여준다.</p>
<p>초보자라면 Wireshark가 훨씬 보기 편하다. 색깔로 프로토콜을 구분해주고, 클릭하면 패킷 내부 구조를 트리 형태로 펼쳐볼 수 있다.</p>
<ul>
<li><strong>tcpdump</strong> → 서버에서 빠르게 확인할 때 (GUI 없는 환경)</li>
<li><strong>Wireshark</strong> → 로컬에서 자세히 분석할 때</li>
</ul>
<hr>
<p>패킷을 떠본다는 건 결국 <strong>&quot;지금 이 네트워크에서 무슨 대화가 오가고 있는지 들여다보는 것&quot;</strong> 이다.
장애가 났을 때, 보안 이슈가 생겼을 때, 통신 흐름을 이해하고 싶을 때 — 앞으로 실무에서 자주 쓰게 될 도구다.</p>
<br>

<hr>
<h2 id="🚨-실무에서-통신이-안-돼요-하면-어떻게-접근할까">🚨 실무에서 &quot;통신이 안 돼요&quot; 하면 어떻게 접근할까?</h2>
<p>보안솔루션 엔지니어로 일하다 보면 고객사에서 &quot;갑자기 통신이 안 된다&quot;는 연락이 많이 온다는 것을 주변 실무자들을 통해 알게되었다. 그래서 처음 연락을 받았을 때 어떻게 하면 될지 
이 추가 글도 작성하게 되었다. 배우면서 느낀것은, 
이때 무작정 tcpdump부터 뜨는 게 아니라, <strong>계층별로 좁혀가는 것</strong>이 핵심이라는 것을 알게되었다. </p>
<p>네트워크는 L1(물리) → L2(데이터링크) → L3(네트워크) → L4(전송) 순으로 쌓여있기 때문에,
<strong>아래 계층부터 위로 올라가며 하나씩 확인</strong>한다.</p>
<hr>
<h3 id="1단계--물리-연결-확인-l1">1단계 — 물리 연결 확인 (L1)</h3>
<p>가장 먼저, 케이블이 꽂혀있긴 한지부터 본다.</p>
<pre><code class="language-bash">ip link show          # 인터페이스 상태 확인</code></pre>
<pre><code>eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt;   ← UP이면 물리 연결 OK
eth0: &lt;BROADCAST,MULTICAST&gt;               ← LOWER_UP 없으면 케이블 문제</code></pre><table>
<thead>
<tr>
<th>확인 항목</th>
<th>의심 원인</th>
</tr>
</thead>
<tbody><tr>
<td>인터페이스가 DOWN</td>
<td>케이블 불량, 포트 비활성화</td>
</tr>
<tr>
<td>Link LED 꺼짐</td>
<td>물리적 단선, 스위치 포트 문제</td>
</tr>
</tbody></table>
<hr>
<h3 id="2단계--같은-네트워크-내-통신-확인-l2">2단계 — 같은 네트워크 내 통신 확인 (L2)</h3>
<p>물리 연결은 됐는데 통신이 안 된다면, <strong>같은 스위치에 연결된 장비끼리 통신</strong>이 되는지 확인한다.</p>
<pre><code class="language-bash">ping 192.168.1.1      # 같은 대역 게이트웨이에 ping
arp -n                # ARP 테이블 확인 (MAC 주소 학습 여부)</code></pre>
<pre><code>192.168.1.1   ether  aa:bb:cc:dd:ee:ff   ← ARP 있으면 L2 통신 OK
192.168.1.1   (incomplete)               ← ARP 없으면 L2 문제</code></pre><table>
<thead>
<tr>
<th>확인 항목</th>
<th>의심 원인</th>
</tr>
</thead>
<tbody><tr>
<td>ping 응답 없음</td>
<td>VLAN 설정 오류, 스위치 포트 문제</td>
</tr>
<tr>
<td>ARP incomplete</td>
<td>ARP Spoofing, 잘못된 IP 설정</td>
</tr>
</tbody></table>
<hr>
<h3 id="3단계--다른-네트워크와-통신-확인-l3">3단계 — 다른 네트워크와 통신 확인 (L3)</h3>
<p>게이트웨이까지는 되는데 외부가 안 된다면, <strong>라우팅</strong> 문제다.</p>
<pre><code class="language-bash">ping 8.8.8.8              # 외부 IP로 ping (구글 DNS)
traceroute 8.8.8.8        # 어디서 끊기는지 경로 추적
ip route show             # 라우팅 테이블 확인</code></pre>
<pre><code class="language-bash">$ traceroute 8.8.8.8
1  192.168.1.1    1ms       ← 게이트웨이 (L3 스위치/라우터)
2  10.0.0.1       5ms       ← ISP 구간
3  * * *                    ← 여기서 끊김 → 이 구간 문제</code></pre>
<table>
<thead>
<tr>
<th>확인 항목</th>
<th>의심 원인</th>
</tr>
</thead>
<tbody><tr>
<td>게이트웨이 ping 안 됨</td>
<td>라우팅 테이블 오류, 방화벽 차단</td>
</tr>
<tr>
<td>traceroute 중간 끊김</td>
<td>해당 구간 라우터/방화벽 문제</td>
</tr>
</tbody></table>
<hr>
<h3 id="4단계--포트서비스-통신-확인-l4">4단계 — 포트/서비스 통신 확인 (L4)</h3>
<p>IP는 되는데 특정 서비스만 안 된다면, <strong>포트 차단</strong> 문제다.</p>
<pre><code class="language-bash">telnet 192.168.1.10 443       # 특정 포트 열려있는지 확인
nc -zv 192.168.1.10 443       # netcat으로 포트 확인
curl -v https://example.com   # HTTP/HTTPS 응답 확인</code></pre>
<pre><code class="language-bash">$ telnet 192.168.1.10 443
Connected to 192.168.1.10     ← 포트 열려있음
Connection refused            ← 포트 막혀있음 또는 서비스 미실행</code></pre>
<table>
<thead>
<tr>
<th>확인 항목</th>
<th>의심 원인</th>
</tr>
</thead>
<tbody><tr>
<td>Connection refused</td>
<td>방화벽 정책 차단, 서비스 미실행</td>
</tr>
<tr>
<td>응답 없음(timeout)</td>
<td>방화벽이 패킷을 DROP</td>
</tr>
</tbody></table>
<hr>
<h3 id="5단계--그래도-모르겠으면-tcpdump">5단계 — 그래도 모르겠으면 tcpdump</h3>
<p>위 단계를 다 해도 원인을 못 잡겠을 때 tcpdump를 뜬다.
<strong>&quot;패킷이 실제로 오가고 있는지&quot;</strong> 를 눈으로 직접 확인하는 것이다.</p>
<pre><code class="language-bash"># 특정 호스트와의 패킷만 캡처
sudo tcpdump -i eth0 host 192.168.1.10

# 특정 포트만 캡처
sudo tcpdump -i eth0 port 443

# 패킷 내용까지 출력
sudo tcpdump -i eth0 -X port 80</code></pre>
<p>패킷이 아예 안 잡힌다 → 내 쪽에서 패킷을 못 보내고 있는 것
패킷은 나가는데 응답이 없다 → 상대방 쪽 또는 중간 경로 문제</p>
<hr>
<h3 id="전체-흐름-요약">전체 흐름 요약</h3>
<pre><code>통신 안 됨
│
├── 케이블/인터페이스 확인 (L1)  →  ip link show
│
├── 같은 대역 ping 확인 (L2)    →  ping 게이트웨이, arp -n
│
├── 외부 ping 확인 (L3)         →  ping 8.8.8.8, traceroute
│
├── 포트 확인 (L4)              →  telnet, nc, curl
│
└── 그래도 모르겠으면           →  tcpdump로 패킷 직접 확인</code></pre><p>계층을 건너뛰지 않고 <strong>L1부터 차근차근 올라가는 것</strong>이 핵심이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ingress 정책이 종료된다는데, Api Gateway랑 무슨 차이일까? ]]></title>
            <link>https://velog.io/@lee_nah/ingress-%EC%A0%95%EC%B1%85%EC%9D%B4-%EC%A2%85%EB%A3%8C%EB%90%9C%EB%8B%A4%EB%8A%94%EB%8D%B0-apigateway%EB%9E%91-%EB%AC%B4%EC%8A%A8-%EC%B0%A8%EC%9D%B4%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@lee_nah/ingress-%EC%A0%95%EC%B1%85%EC%9D%B4-%EC%A2%85%EB%A3%8C%EB%90%9C%EB%8B%A4%EB%8A%94%EB%8D%B0-apigateway%EB%9E%91-%EB%AC%B4%EC%8A%A8-%EC%B0%A8%EC%9D%B4%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Tue, 26 May 2026 13:10:48 GMT</pubDate>
            <description><![CDATA[<h1 id="kubernetes-ingress-nginx가-은퇴한다">Kubernetes Ingress NGINX가 은퇴한다?</h1>
<p>최근 cloudbro에서 Kubernetes Ingress NGINX 은퇴 관련 글을 보았다.</p>
<p>처음에는 “Ingress NGINX가 종료되는구나” 정도로 봤는데, 내용을 보니 단순히 특정 오픈소스 하나가 사라지는 이야기가 아니었다.<br>쿠버네티스에서 외부 트래픽을 클러스터 안으로 넣는 방식 자체가 <strong>Ingress 중심에서 Gateway API 중심으로 넘어가는 흐름</strong>에 가깝다고 볼 수 있었다. </p>
<p><a href="https://www.cloudbro.ai/t/kubernetes-ingress-nginx-retirement-gateway-api/3149/7">원문 링크</a></p>
<br>

<h2 id="ingress-nginx가-뭐길래-이게-이슈인가">Ingress NGINX가 뭐길래 이게 이슈인가?</h2>
<p>쿠버네티스에서 외부 사용자가 서비스에 접근하려면, 외부 트래픽을 클러스터 내부 서비스로 연결해주는 진입점이 필요하다.</p>
<p>이때 많이 쓰던 리소스가 <strong>Ingress</strong>였다. </p>
<p>그리고 그 Ingress를 실제로 동작하게 해주는 대표적인 컨트롤러가 바로 <code>ingress-nginx</code>다.</p>
<blockquote>
<p>Ingress는 “외부 요청을 어떤 서비스로 보낼지 정의하는 쿠버네티스 리소스”다.<br>다만 Ingress 리소스만 만든다고 트래픽이 자동으로 흐르는 것은 아니다.<br>이 규칙을 실제 NGINX 설정으로 바꾸고, 로드밸런서처럼 동작하게 만드는 컨트롤러가 필요하다. 그 대표적인 구현체가 <code>ingress-nginx</code>다.</p>
</blockquote>
<p><code>ingress-nginx</code>는 오랫동안 사실상 표준처럼 사용되어 왔다.<br>쿠버네티스를 조금이라도 운영해본 환경이라면 한 번쯤은 봤을 가능성이 높다.</p>
<p>그런데 이 프로젝트가 <strong>2026년 3월 이후로 공식 은퇴 예정</strong>이다.</p>
<p>은퇴 이후에는 다음 항목이 중단된다.</p>
<ul>
<li>신규 릴리스 중단</li>
<li>버그픽스 중단</li>
<li>보안 취약점 패치 중단</li>
<li>GitHub 저장소 read-only 전환 예정</li>
</ul>
<p>즉, 기존에 설치된 <code>ingress-nginx</code>가 갑자기 바로 죽는 것은 아니다.<br>하지만 인터넷에 직접 노출되는 컴포넌트를 더 이상 보안 패치 없이 운영해야 한다는 점이 문제다.</p>
<p>운영 관점에서는 꽤 큰 리스크다.</p>
<br>

<h2 id="그럼-이제-뭘-봐야-하나">그럼 이제 뭘 봐야 하나?</h2>
<p>쿠버네티스 진영에서 밀고 있는 다음 표준은 <strong>Gateway API</strong>다.</p>
<p>Gateway API는 기존 Ingress의 한계를 보완하기 위해 나온 네트워킹 API다.<br>단순히 “Ingress의 새 버전”이라기보다는, 외부 트래픽 관리 방식을 더 세분화하고 표준화한 구조에 가깝다.</p>
<blockquote>
<p>Gateway API는 Ingress처럼 HTTP 라우팅만 다루는 것이 아니라,<br>Gateway, HTTPRoute, TCPRoute, GRPCRoute 같은 리소스를 통해 다양한 트래픽 처리 방식을 표준화하려는 시도다.<br>쉽게 말하면 “각 컨트롤러마다 제각각 annotation으로 처리하던 기능을 쿠버네티스 표준 리소스로 끌어올리는 것”이다.</p>
</blockquote>
<br>

<h2 id="기존-ingress의-한계">기존 Ingress의 한계</h2>
<p>Ingress는 단순한 구조라서 처음 쓰기에는 편하다.<br>하지만 운영 규모가 커질수록 한계가 보인다.</p>
<p>예를 들면 다음과 같은 Ingress 리소스가 있다.</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80</code></pre>
<p>겉으로 보기에는 단순하다.<br><code>example.com/api</code>로 들어온 요청을 <code>api-service:80</code>으로 보내겠다는 의미다.</p>
<p>문제는 운영 환경에서는 이 정도로 끝나지 않는다는 점이다.</p>
<p>실제 서비스에서는 TLS 인증서, 리다이렉트, rewrite, rate limit, header 기반 라우팅, canary 배포, 트래픽 분할 같은 요구사항이 붙는다.</p>
<p>Ingress는 이런 고급 기능을 표준 필드로 충분히 표현하지 못했다.<br>그래서 각 컨트롤러가 annotation으로 기능을 확장했다.</p>
<p>예를 들면 NGINX에서는 이런 annotation을 쓴다.</p>
<pre><code class="language-yaml">nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: &quot;true&quot;
nginx.ingress.kubernetes.io/canary: &quot;true&quot;</code></pre>
<p>이 방식의 문제는 명확하다.</p>
<p><code>ingress-nginx</code>에서는 동작하지만, 다른 Ingress Controller에서는 안 될 수 있다.<br>즉, YAML은 쿠버네티스 리소스처럼 보이지만 실제로는 특정 컨트롤러에 종속된다.</p>
<blockquote>
<p>annotation은 쿠버네티스 리소스에 추가 메타데이터를 붙이는 방식이다.<br>원래는 보조 정보에 가깝지만, Ingress에서는 컨트롤러별 고급 기능을 켜는 용도로 많이 사용되었다.<br>이 때문에 같은 Ingress YAML이라도 NGINX, ALB, Traefik, HAProxy에서 다르게 동작할 수 있다.</p>
</blockquote>
<br>

<h2 id="또-다른-문제는-역할-분리다">또 다른 문제는 역할 분리다</h2>
<p>Ingress는 하나의 리소스 안에 너무 많은 책임이 들어간다.</p>
<ul>
<li>어떤 도메인을 받을지</li>
<li>어떤 경로를 어떤 서비스로 보낼지</li>
<li>TLS 인증서를 어떻게 쓸지</li>
<li>로드밸런서 동작을 어떻게 할지</li>
<li>컨트롤러별 세부 옵션을 어떻게 줄지</li>
</ul>
<p>이런 내용이 하나의 Ingress에 섞인다.</p>
<p>작은 팀에서는 괜찮을 수 있다.<br>하지만 인프라팀과 앱팀이 분리된 조직에서는 문제가 된다.</p>
<p>앱팀은 라우팅 규칙만 수정하고 싶은데, Ingress 안에는 TLS나 로드밸런서 설정까지 같이 들어 있다.<br>반대로 인프라팀은 공통 Gateway 정책을 관리하고 싶은데, 각 서비스 Ingress에 설정이 흩어져 있다.</p>
<p>결국 RBAC 분리도 애매해진다.</p>
<blockquote>
<p>RBAC는 Role-Based Access Control의 약자다.<br>쿠버네티스에서는 “누가 어떤 리소스를 조회·생성·수정·삭제할 수 있는지”를 제어하는 권한 관리 방식이다.<br>예를 들어 앱팀에는 HTTPRoute만 수정할 수 있게 하고, Gateway나 TLS 설정은 인프라팀만 수정하게 만드는 식의 분리가 가능하다.</p>
</blockquote>
<p>Ingress 구조에서는 이런 분리가 깔끔하지 않다.<br>Ingress 하나를 수정할 수 있으면 라우팅뿐 아니라 중요한 진입점 설정까지 같이 건드릴 수 있기 때문이다.</p>
<br>

<h2 id="gateway-api는-뭐가-다를까">Gateway API는 뭐가 다를까?</h2>
<p>Gateway API는 이 문제를 리소스 분리로 해결한다.</p>
<p>대표적인 리소스는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>리소스</th>
<th>주로 관리하는 팀</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>GatewayClass</code></td>
<td>클러스터 관리자 / 인프라팀</td>
<td>어떤 Gateway 컨트롤러를 사용할지 정의</td>
</tr>
<tr>
<td><code>Gateway</code></td>
<td>인프라팀</td>
<td>외부 진입점, 포트, 프로토콜, TLS 설정</td>
</tr>
<tr>
<td><code>HTTPRoute</code></td>
<td>앱팀</td>
<td>특정 서비스로 보내는 라우팅 규칙</td>
</tr>
</tbody></table>
<p>구조를 단순하게 보면 이렇다.</p>
<pre><code class="language-text">외부 사용자
   ↓
Gateway
   ↓
HTTPRoute
   ↓
Service
   ↓
Pod</code></pre>
<p>Ingress에서는 하나의 리소스에 섞여 있던 책임을 Gateway API에서는 나누어 가진다.</p>
<p>인프라팀은 <code>Gateway</code>를 관리하고, 앱팀은 <code>HTTPRoute</code>를 관리한다.<br>이 구조가 운영 조직과 잘 맞는다고 볼 수 있다.</p>
<br>

<h2 id="gateway-api-예시">Gateway API 예시</h2>
<p>예를 들어 인프라팀은 아래처럼 공통 Gateway를 만든다.</p>
<pre><code class="language-yaml">apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: infra
spec:
  gatewayClassName: nginx
  listeners:
  - name: https
    port: 443
    protocol: HTTPS
    hostname: example.com
    tls:
      mode: Terminate
      certificateRefs:
      - name: example-com-cert</code></pre>
<p>이 Gateway는 <code>example.com</code>의 HTTPS 요청을 받을 수 있는 진입점이다.</p>
<blockquote>
<p>TLS Terminate는 HTTPS 암호화 연결을 Gateway나 Load Balancer에서 종료한다는 의미다.<br>외부 사용자는 HTTPS로 접속하지만, Gateway 이후 내부 서비스로는 HTTP 또는 별도 정책에 따라 전달될 수 있다.<br>운영 환경에서는 인증서 관리 지점과 내부 통신 암호화 여부를 분리해서 설계해야 한다.</p>
</blockquote>
<p>그리고 앱팀은 자신의 서비스에 대한 라우팅만 정의한다.</p>
<pre><code class="language-yaml">apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-route
  namespace: app
spec:
  parentRefs:
  - name: main-gateway
    namespace: infra
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /api
    backendRefs:
    - name: api-service
      port: 80</code></pre>
<p>이렇게 하면 <code>/api</code>로 들어온 요청이 <code>api-service:80</code>으로 전달된다.</p>
<p>중요한 점은 앱팀이 Gateway 자체를 수정하지 않아도 된다는 것이다.<br>앱팀은 자신의 <code>HTTPRoute</code>만 관리하면 된다.</p>
<br>

<h2 id="ingress와-gateway-api-비교">Ingress와 Gateway API 비교</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/0a234a70-410f-4238-b0c5-a8522ed8d314/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Ingress</th>
<th>Gateway API</th>
</tr>
</thead>
<tbody><tr>
<td>구조</td>
<td>하나의 리소스에 대부분의 설정이 집중됨</td>
<td>GatewayClass, Gateway, Route로 역할 분리</td>
</tr>
<tr>
<td>고급 기능</td>
<td>annotation 의존도가 높음</td>
<td>표준 필드로 표현 가능</td>
</tr>
<tr>
<td>컨트롤러 종속성</td>
<td>컨트롤러마다 동작 차이가 큼</td>
<td>표준 스펙 기반</td>
</tr>
<tr>
<td>RBAC 분리</td>
<td>애매함</td>
<td>인프라팀/앱팀 권한 분리 쉬움</td>
</tr>
<tr>
<td>트래픽 분할</td>
<td>구현체별 annotation 의존</td>
<td>weight 기반 backendRefs로 표현 가능</td>
</tr>
<tr>
<td>헤더 기반 라우팅</td>
<td>컨트롤러별 차이 있음</td>
<td>HTTPRoute에서 표준화</td>
</tr>
<tr>
<td>gRPC/TCP 지원</td>
<td>제한적이거나 구현체별 상이</td>
<td>GRPCRoute, TCPRoute 등으로 확장</td>
</tr>
<tr>
<td>멀티 네임스페이스</td>
<td>관리가 까다로움</td>
<td>namespace 분리와 참조 구조 지원</td>
</tr>
<tr>
<td>서비스 메시 연계</td>
<td>별도 설정이 많음</td>
<td>Istio 등과 Gateway API 기반 통합 가능</td>
</tr>
</tbody></table>
<br>

<h2 id="트래픽-분할에서의-장점">트래픽 분할에서의 장점</h2>
<p>Gateway API에서는 트래픽을 여러 서비스로 나누는 것도 표준 방식으로 표현할 수 있다.</p>
<p>예를 들어 v1 서비스에 90%, v2 서비스에 10%만 보내는 구성이 가능하다.</p>
<pre><code class="language-yaml">apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-canary-route
spec:
  rules:
  - backendRefs:
    - name: api-v1
      port: 80
      weight: 90
    - name: api-v2
      port: 80
      weight: 10</code></pre>
<blockquote>
<p>Canary 배포는 새 버전을 전체 사용자에게 바로 배포하지 않고, 일부 트래픽만 먼저 보내서 안정성을 확인하는 방식이다.<br>예를 들어 기존 버전에 90%, 새 버전에 10%만 보내고 오류율이나 응답 시간을 확인한 뒤 점진적으로 비율을 높인다.</p>
</blockquote>
<p>Ingress에서도 비슷한 구성을 할 수는 있었다.<br>하지만 대부분 annotation이나 컨트롤러별 기능에 의존했다.</p>
<p>Gateway API는 이런 기능을 리소스 스펙 안에서 더 표준적인 방식으로 표현하려는 방향이다.</p>
<br>

<h2 id="csp들도-gateway-api-쪽으로-움직이고-있다">CSP들도 Gateway API 쪽으로 움직이고 있다</h2>
<p>여기서 말하는 <strong>CSP</strong>는 Cloud Service Provider의 약자다.<br>즉, AWS, Google Cloud, Microsoft Azure처럼 클라우드 인프라를 제공하는 회사를 의미한다.</p>
<blockquote>
<p>CSP는 클라우드 서비스를 제공하는 사업자를 말한다.<br>대표적으로 AWS, GCP, Azure가 있고, 이들은 Kubernetes를 직접 운영하지 않아도 사용할 수 있도록 관리형 Kubernetes 서비스를 제공한다.</p>
</blockquote>
<p>그리고 각 CSP는 자체 관리형 Kubernetes 서비스를 제공한다.</p>
<ul>
<li><strong>GKE</strong>: Google Kubernetes Engine, Google Cloud의 관리형 Kubernetes 서비스</li>
<li><strong>EKS</strong>: Elastic Kubernetes Service, AWS의 관리형 Kubernetes 서비스</li>
<li><strong>AKS</strong>: Azure Kubernetes Service, Microsoft Azure의 관리형 Kubernetes 서비스</li>
</ul>
<blockquote>
<p>관리형 Kubernetes 서비스는 사용자가 직접 Control Plane을 설치하고 운영하지 않아도, CSP가 Kubernetes 제어 영역을 관리해주는 서비스다.<br>사용자는 주로 워커 노드, 애플리케이션 배포, 네트워크, 보안 설정에 집중하면 된다.</p>
</blockquote>
<p>최근 흐름을 보면 주요 CSP들도 Gateway API 지원을 점점 확대하고 있다.</p>
<h3 id="gke">GKE</h3>
<p>Google Kubernetes Engine은 Gateway API 기반 L7 Load Balancer와 Multi-Cluster Gateway를 지원한다.<br>GKE는 Gateway API를 비교적 적극적으로 밀고 있는 편이다.</p>
<blockquote>
<p>L7 Load Balancer는 HTTP/HTTPS 같은 애플리케이션 계층 정보를 보고 트래픽을 분산하는 로드밸런서다.<br>예를 들어 URL 경로가 <code>/api</code>인지 <code>/web</code>인지, Host가 무엇인지에 따라 서로 다른 서비스로 요청을 보낼 수 있다.</p>
</blockquote>
<h3 id="eks">EKS</h3>
<p>AWS 쪽에서는 VPC Lattice와 연계되는 Gateway API Controller가 나왔다.<br>또 AWS Load Balancer Controller에서도 Gateway API 지원이 진행되고 있다.</p>
<p>다만 운영 환경에서는 각 컨트롤러의 지원 수준을 반드시 확인해야 한다.<br>“Gateway API를 지원한다”와 “프로덕션에서 안정적으로 모든 기능을 쓸 수 있다”는 같은 말이 아니다.</p>
<blockquote>
<p>VPC Lattice는 AWS에서 서비스 간 통신을 관리하기 위한 네트워킹 서비스다.<br>여러 VPC나 계정에 흩어진 서비스를 논리적으로 연결하고, 인증·인가·트래픽 제어를 붙일 수 있다.<br>Gateway API와 연결하면 Kubernetes의 HTTPRoute 같은 리소스를 AWS 네트워킹 리소스와 매핑하는 식으로 사용할 수 있다.</p>
</blockquote>
<h3 id="aks">AKS</h3>
<p>Azure는 Application Gateway for Containers를 통해 Gateway API 기반 구조를 강화하고 있다.<br>기존 AGIC의 후속 방향으로 볼 수 있다.</p>
<blockquote>
<p>AGIC는 Application Gateway Ingress Controller의 약자다.<br>AKS에서 Kubernetes Ingress 리소스를 Azure Application Gateway와 연결해주는 컨트롤러다.<br>Application Gateway for Containers는 이 흐름을 Gateway API 중심으로 발전시킨 후속 구조에 가깝다.</p>
</blockquote>
<p>결국 AWS, GCP, Azure 모두 Gateway API 흐름을 무시하기 어려운 상황이다.<br>Kubernetes 네트워킹 표준이 Ingress 중심에서 Gateway API 중심으로 이동하고 있기 때문이다.</p>
<h2 id="오픈소스-컨트롤러도-바뀌고-있다">오픈소스 컨트롤러도 바뀌고 있다</h2>
<p>Ingress NGINX 은퇴 이후 선택지는 하나만 있는 것이 아니다.<br>Gateway API를 구현하는 여러 컨트롤러가 있다.</p>
<ul>
<li><strong>NGINX Gateway Fabric</strong></li>
<li><strong>Envoy Gateway</strong></li>
<li><strong>Istio</strong></li>
<li><strong>Traefik</strong></li>
<li><strong>Kong</strong></li>
<li><strong>APISIX</strong></li>
<li><strong>HAProxy Kubernetes Ingress / Gateway 관련 구현체</strong></li>
</ul>
<p>각 컨트롤러마다 성격은 다르다.</p>
<p>NGINX Gateway Fabric은 NGINX 기반 Gateway API 구현체다.<br>Envoy Gateway는 Envoy 기반이고, Istio는 서비스 메시까지 함께 고려할 수 있다.<br>Kong이나 APISIX는 API Gateway 성격이 강하다.</p>
<blockquote>
<p>Envoy는 L7 프록시로 많이 사용되는 오픈소스다.<br>서비스 메시, API Gateway, 클라우드 네이티브 로드밸런싱에서 자주 쓰인다.<br>Istio, Envoy Gateway 같은 도구들이 Envoy를 데이터 플레인으로 사용한다.</p>
</blockquote>
<p>따라서 단순히 “Ingress NGINX가 끝났으니 NGINX Gateway Fabric으로 가면 된다”라고 보기보다는, 현재 운영 환경에 맞는 컨트롤러를 골라야 한다.</p>
<br>

<h2 id="지금-당장-다-바꿔야할까">지금 당장 다 바꿔야할까?</h2>
<p>그건 아니다.</p>
<p>기존 클러스터에서 <code>ingress-nginx</code>를 사용 중이라고 해서 당장 서비스가 중단되는 것은 아니다.<br>하지만 신규 릴리스와 보안 패치가 중단되는 시점 이후에는 리스크가 커진다.</p>
<p>그래서 현실적인 접근은 단계적 전환이다.</p>
<br>

<h2 id="전환-전략">전환 전략</h2>
<p>사실 이 부분은 내가 혼자 바로 떠올린 전략은 아니다.</p>
<p>Ingress NGINX 은퇴 소식을 보고 “그럼 실제 운영 환경에서는 어떻게 넘어가야 하지?”라는 생각이 들었고,<br>Claude와 ChatGPT에게도 질문해보면서 여러 전환 방식을 정리해봤다.</p>
<p>현재 Kubernetes 운영 관점에서 현실적으로 가능한 방향만 추려보았고 그 생각은 다음과 같다.</p>
<br>

<h3 id="1-현재-사용-중인-ingress부터-파악한다">1. 현재 사용 중인 Ingress부터 파악한다</h3>
<p>가장 먼저 해야 할 일은 현재 클러스터에서 Ingress를 어떻게 쓰고 있는지 확인하는 것이다.</p>
<pre><code class="language-bash">kubectl get ingress -A</code></pre>
<p>이 명령어를 사용하면 전체 네임스페이스에서 사용 중인 Ingress 목록을 확인할 수 있다.</p>
<p>하지만 단순히 Ingress 개수만 보는 것으로는 부족하다.<br>중요한 것은 각 Ingress가 어떤 annotation에 의존하고 있는지 확인하는 것이다.</p>
<pre><code class="language-bash">kubectl get ingress -A -o yaml | grep &quot;nginx.ingress.kubernetes.io&quot;</code></pre>
<p>예를 들어 아래와 같은 annotation을 많이 쓰고 있다면 전환 난이도가 올라갈 수 있다.</p>
<ul>
<li>rewrite</li>
<li>ssl-redirect</li>
<li>auth</li>
<li>canary</li>
<li>rate limit</li>
<li>configuration-snippet</li>
<li>server-snippet</li>
</ul>
<blockquote>
<p>여기서 중요한 점은 “Ingress를 몇 개 쓰고 있느냐”보다 “NGINX 전용 기능에 얼마나 의존하고 있느냐”다.<br>단순 path routing만 사용 중이라면 Gateway API로 옮기기 비교적 쉽지만, NGINX annotation을 많이 사용 중이라면 동일한 기능을 Gateway API에서 어떻게 구현할지 따로 검토해야 한다.</p>
</blockquote>
<br>

<h3 id="2-신규-서비스부터-gateway-api를-적용한다">2. 신규 서비스부터 Gateway API를 적용한다</h3>
<p>기존 서비스를 한 번에 전부 바꾸는 것은 위험하다.<br>Ingress는 외부 트래픽이 들어오는 진입점이기 때문에, 잘못 바꾸면 서비스 장애로 바로 이어질 수 있다.</p>
<p>그래서 가장 현실적인 방법은 <strong>신규 서비스나 신규 클러스터부터 Gateway API를 적용하는 것</strong>이라고 본다.</p>
<p>기존 서비스는 당분간 Ingress로 유지하고,<br>새로 만드는 서비스부터 <code>Gateway</code>, <code>HTTPRoute</code> 기반으로 구성해보는 방식이다.</p>
<p>이렇게 하면 운영 중인 서비스에 주는 영향을 줄이면서 Gateway API 사용 경험을 쌓을 수 있다.</p>
<blockquote>
<p>쉽게 말하면 “기존 도로를 바로 폐쇄하지 않고, 새 도로부터 Gateway API 방식으로 만들어보는 것”에 가깝다.<br>운영 환경에서는 한 번에 갈아엎는 방식보다, 신규 서비스부터 적용하고 검증하는 방식이 훨씬 안전하다.</p>
</blockquote>
<br>

<h3 id="3-ingress와-gateway-api를-병행-운영한다">3. Ingress와 Gateway API를 병행 운영한다</h3>
<p>전환 기간에는 Ingress와 Gateway API를 같이 운영할 수 있다.</p>
<p>예를 들면 다음과 같은 방식이다.</p>
<ul>
<li>기존 서비스: Ingress 유지</li>
<li>신규 서비스: Gateway API 적용</li>
<li>일부 경로: Gateway API로 먼저 이전</li>
<li>테스트 트래픽: 별도 hostname으로 검증</li>
<li>안정화 후 점진적으로 Gateway API 비중 확대</li>
</ul>
<p>이런 방식을 사용하면 한 번에 모든 트래픽을 Gateway API로 넘기지 않아도 된다.</p>
<p>예를 들어 기존에는 <code>example.com/api</code>를 Ingress가 처리했다면,<br>테스트 단계에서는 <code>gateway-test.example.com/api</code> 같은 별도 도메인을 만들어 Gateway API 동작을 검증할 수 있다.</p>
<blockquote>
<p>병행 운영의 핵심은 트래픽 진입점을 명확히 나누는 것이다.<br>Ingress와 Gateway API가 같은 host, 같은 path를 동시에 처리하게 만들면 오히려 혼란이 생길 수 있다.<br>따라서 어떤 요청은 Ingress가 받고, 어떤 요청은 Gateway가 받는지 기준을 명확히 잡아야 한다.</p>
</blockquote>
<br>

<h3 id="4-기능별로-마이그레이션-가능-여부를-확인한다">4. 기능별로 마이그레이션 가능 여부를 확인한다</h3>
<p>Ingress에서 쓰던 기능이 Gateway API에서 모두 똑같이 동작한다고 보면 안 된다.</p>
<p>특히 NGINX annotation으로 처리하던 기능은 Gateway API 표준 리소스로 바로 1:1 대응되지 않을 수 있다.</p>
<p>예를 들어 다음 항목은 따로 확인이 필요하다.</p>
<table>
<thead>
<tr>
<th>기존 Ingress 기능</th>
<th>확인할 내용</th>
</tr>
</thead>
<tbody><tr>
<td>rewrite</td>
<td>HTTPRoute filter로 대체 가능한지 확인</td>
</tr>
<tr>
<td>TLS 설정</td>
<td>Gateway listener와 certificateRefs로 분리 가능 여부 확인</td>
</tr>
<tr>
<td>canary</td>
<td>backendRefs weight로 대체 가능한지 확인</td>
</tr>
<tr>
<td>header 기반 라우팅</td>
<td>HTTPRoute matches 조건으로 표현 가능한지 확인</td>
</tr>
<tr>
<td>rate limit</td>
<td>Gateway API 표준만으로 가능한지, 컨트롤러 확장이 필요한지 확인</td>
</tr>
<tr>
<td>auth 처리</td>
<td>컨트롤러별 정책 리소스가 필요한지 확인</td>
</tr>
<tr>
<td>custom snippet</td>
<td>대체가 어려울 수 있으므로 별도 설계 필요</td>
</tr>
</tbody></table>
<blockquote>
<p>Gateway API가 표준을 지향한다고 해서 모든 기능이 표준 필드 하나로 해결되는 것은 아니다.<br>특히 rate limit, 인증, WAF, custom NGINX 설정처럼 컨트롤러 특화 기능에 가까운 부분은 사용하는 Gateway Controller의 지원 범위를 반드시 확인해야 한다.</p>
</blockquote>
<br>

<h3 id="5-컨트롤러를-먼저-정한다">5. 컨트롤러를 먼저 정한다</h3>
<p>Gateway API를 사용하려면 Gateway API를 실제로 처리할 컨트롤러가 필요하다.</p>
<p>Ingress 리소스만 만든다고 동작하지 않았던 것처럼,<br>Gateway API도 <code>Gateway</code>, <code>HTTPRoute</code> 리소스만 만든다고 자동으로 트래픽이 흐르지는 않는다.</p>
<p>대표적인 선택지는 다음과 같다.</p>
<ul>
<li>NGINX Gateway Fabric</li>
<li>Envoy Gateway</li>
<li>Istio</li>
<li>Traefik</li>
<li>Kong</li>
<li>APISIX</li>
<li>AWS Gateway API Controller for VPC Lattice</li>
<li>GKE Gateway Controller</li>
<li>Azure Application Gateway for Containers</li>
</ul>
<p>어떤 컨트롤러를 선택할지는 환경에 따라 다르다.</p>
<p>예를 들어 AWS EKS 환경이라면 AWS Load Balancer Controller나 VPC Lattice 연계를 검토할 수 있고,<br>서비스 메시까지 고려한다면 Istio 기반 Gateway API 구성을 볼 수 있다.<br>반대로 단순 L7 라우팅 중심이라면 Envoy Gateway나 NGINX Gateway Fabric도 후보가 될 수 있다.</p>
<blockquote>
<p>Gateway API는 표준 스펙이고, 컨트롤러는 그 스펙을 실제로 구현하는 구성요소다.<br>따라서 “Gateway API를 쓴다”는 말만으로는 부족하고, “어떤 Gateway Controller로 운영할 것인가”까지 정해야 한다.</p>
</blockquote>
<br>

<h3 id="서비스-메시란">서비스 메시란?</h3>
<p>여기서 서비스 메시라는 말이 나오는데, 처음 보면 조금 어렵게 느껴질 수 있다.</p>
<p>서비스 메시(Service Mesh)는 쉽게 말하면 <strong>서비스끼리 통신할 때 필요한 네트워크 기능을 애플리케이션 코드 밖에서 관리하는 구조</strong>다.</p>
<p>일반적으로 마이크로서비스 환경에서는 여러 서비스가 서로 호출한다.</p>
<p>예를 들면 다음과 같은 구조다.</p>
<pre><code class="language-text">사용자
  ↓
주문 서비스
  ↓
결제 서비스
  ↓
알림 서비스</code></pre>
<p>이때 서비스 간 통신에서는 여러 가지 문제가 생길 수 있다.</p>
<ul>
<li>결제 서비스가 느리면 어떻게 할 것인가?</li>
<li>요청이 실패하면 재시도할 것인가?</li>
<li>서비스 간 통신을 암호화할 것인가?</li>
<li>특정 버전으로 트래픽을 일부만 보낼 것인가?</li>
<li>어느 서비스에서 장애가 발생했는지 어떻게 추적할 것인가?</li>
</ul>
<p>이런 기능을 애플리케이션 코드마다 직접 구현하면 복잡해진다.</p>
<p>서비스 메시를 사용하면 이런 기능을 애플리케이션 코드가 아니라 <strong>프록시 계층</strong>에서 처리할 수 있다.</p>
<blockquote>
<p>프록시는 중간에서 요청을 대신 받아 전달해주는 구성요소다.<br>서비스 메시에서는 보통 각 서비스 옆에 프록시가 붙고, 이 프록시가 서비스 간 통신을 대신 제어한다.<br>Istio에서는 Envoy 프록시가 이 역할을 한다.</p>
</blockquote>
<p>서비스 메시를 사용하면 다음과 같은 기능을 구현하기 쉬워진다.</p>
<ul>
<li>서비스 간 mTLS 암호화</li>
<li>트래픽 분할</li>
<li>장애 시 재시도</li>
<li>타임아웃 설정</li>
<li>Circuit Breaker</li>
<li>요청 흐름 추적</li>
<li>서비스 간 접근 제어</li>
</ul>
<blockquote>
<p>mTLS는 mutual TLS의 약자다.<br>일반 TLS가 주로 클라이언트가 서버 인증서를 검증하는 구조라면, mTLS는 클라이언트와 서버가 서로를 인증한다.<br>서비스 메시 환경에서는 서비스 간 통신을 더 안전하게 만들기 위해 자주 사용된다.</p>
</blockquote>
<p>단순히 외부 트래픽을 내부 서비스로 보내는 정도라면 Gateway API만으로도 충분할 수 있다.<br>하지만 서비스가 많아지고, 서비스 간 통신까지 세밀하게 제어해야 한다면 서비스 메시까지 함께 검토할 수 있다.</p>
<br>

<h3 id="6-서비스-메시를-사용-중이라면-같이-검토한다">6. 서비스 메시를 사용 중이라면 같이 검토한다</h3>
<p>Istio 같은 서비스 메시를 이미 사용하고 있다면 Gateway API 전환을 따로 떼어놓고 보기 어렵다.</p>
<p>서비스 메시 환경에서는 외부에서 들어오는 North-South 트래픽뿐 아니라,<br>서비스 간 East-West 트래픽까지 함께 고려해야 하기 때문이다.</p>
<blockquote>
<p>North-South 트래픽은 외부 사용자와 클러스터 내부 서비스 사이의 트래픽을 말한다.<br>East-West 트래픽은 클러스터 내부 서비스끼리 주고받는 트래픽을 말한다.<br>예를 들어 사용자가 API 서버에 접근하는 것은 North-South 트래픽이고, API 서버가 내부 결제 서비스나 회원 서비스를 호출하는 것은 East-West 트래픽이다.</p>
</blockquote>
<p>Gateway API는 Istio 같은 서비스 메시와도 연결되는 방향으로 발전하고 있다.<br>다만 이 경우에는 단순 Ingress 교체가 아니라 네트워크 구조 전체를 다시 봐야 하므로 공수가 커질 수 있다.</p>
<p>따라서 서비스 메시를 이미 쓰고 있다면 Gateway API 전환을 네트워크 표준화 작업의 일부로 보는 것이 맞다.</p>
<br>

<h2 id="개인적으로-보는-현실적인-순서">개인적으로 보는 현실적인 순서</h2>
<p>정리하면, 지금 당장 모든 Ingress를 Gateway API로 바꾸는 것은 현실적이지 않다고 본다.</p>
<p>대신 아래 순서가 가장 안전해 보인다.</p>
<ol>
<li>현재 Ingress 목록과 annotation 사용 현황을 확인한다.</li>
<li>NGINX annotation 의존도가 높은 서비스를 따로 분류한다.</li>
<li>신규 서비스부터 Gateway API를 적용한다.</li>
<li>운영 영향이 적은 서비스부터 HTTPRoute로 이전해본다.</li>
<li>Ingress와 Gateway API를 일정 기간 병행 운영한다.</li>
<li>사용하는 CSP와 컨트롤러의 Gateway API 지원 수준을 확인한다.</li>
<li>2026년 3월 이후에도 <code>ingress-nginx</code>를 장기 운영하지 않도록 전환 계획을 세운다.</li>
</ol>
<br>

<h2 id="마무리">마무리</h2>
<p>이번 내용을 정리하면서 느낀 점은, Ingress NGINX 은퇴가 단순히 “컨트롤러 하나가 사라진다”는 문제가 아니라는 것이다.</p>
<p>기존에는 Ingress 하나에 라우팅, TLS, 로드밸런서 관련 설정을 몰아넣는 방식이 많았다.</p>
<p>예를 들면 아래처럼 하나의 Ingress 리소스 안에 host, path, backend service, TLS 설정이 같이 들어간다.</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: app
spec:
  tls:
  - hosts:
    - example.com
    secretName: example-com-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80</code></pre>
<p>이 설정만 보면 단순해 보인다.<br><code>example.com/api</code>로 들어온 요청을 <code>api-service:80</code>으로 보내고, TLS 인증서는 <code>example-com-tls</code>를 사용한다는 의미다.</p>
<p>문제는 실제 운영 환경에서는 여기서 끝나지 않는다는 점이다.</p>
<p>경로를 바꿔서 전달해야 하거나, HTTP 요청을 HTTPS로 강제 리다이렉트해야 하거나, 일부 트래픽만 새 버전으로 보내야 하는 요구사항이 생긴다.</p>
<p>Ingress 기본 스펙만으로 표현하기 어려운 기능은 보통 annotation으로 해결했다.</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: &quot;true&quot;
    nginx.ingress.kubernetes.io/proxy-body-size: &quot;20m&quot;
    nginx.ingress.kubernetes.io/canary: &quot;true&quot;
    nginx.ingress.kubernetes.io/canary-weight: &quot;10&quot;
spec:
  tls:
  - hosts:
    - example.com
    secretName: example-com-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service-v2
            port:
              number: 80</code></pre>
<p>위 설정은 예를 들면 이런 의미로 볼 수 있다.</p>
<ul>
<li><code>rewrite-target</code>: 요청 경로를 내부 서비스에 전달할 때 다시 작성한다.</li>
<li><code>ssl-redirect</code>: HTTP 요청을 HTTPS로 리다이렉트한다.</li>
<li><code>proxy-body-size</code>: 업로드 가능한 요청 body 크기를 조정한다.</li>
<li><code>canary</code>: 카나리 배포용 Ingress로 사용한다.</li>
<li><code>canary-weight</code>: 전체 트래픽 중 일부 비율만 이 서비스로 보낸다.</li>
</ul>
<p>이런 방식은 편하긴 하지만, NGINX Ingress Controller에 강하게 의존한다.<br>즉, 같은 Ingress YAML이라도 다른 컨트롤러에서는 annotation이 동작하지 않거나, 아예 다른 방식으로 설정해야 할 수 있다.</p>
<p>그래서 기존 방식은 운영 규모가 커질수록 컨트롤러 종속성이 강해지고, 조직이 커질수록 운영 권한 분리도 애매해진다.</p>
<p>Gateway API는 이 문제를 표준 리소스와 역할 분리로 풀려고 한다.</p>
<ul>
<li>인프라팀은 Gateway, TLS, Load Balancer를 관리한다.</li>
<li>앱팀은 HTTPRoute로 자신의 서비스 라우팅을 관리한다.</li>
<li>RBAC로 권한을 더 명확하게 나눌 수 있다.</li>
<li>컨트롤러별 annotation 의존도를 줄일 수 있다.</li>
</ul>
<p>물론 Gateway API가 모든 문제를 바로 해결해주는 것은 아니다.<br>컨트롤러 선택, 기존 annotation 대체, CSP 지원 수준, 운영 검증 등 확인해야 할 부분이 많다.</p>
<p>그래도 방향성은 명확해 보인다.</p>
<p>당장 운영 중인 서비스를 무리하게 바꿀 필요는 없지만,<br>신규 서비스나 신규 클러스터를 설계한다면 이제는 Ingress보다 Gateway API를 먼저 검토하는 것이 맞다고 본다.</p>
<br>

<h1 id="이-글이-어려운-사람을-위한-정리글">이 글이 어려운 사람을 위한 정리글</h1>
<p>사실 나도 처음에 이 내용을 봤을 때 바로 이해되지는 않았다.</p>
<p>Ingress, Ingress Controller, annotation, Gateway API 이런 단어가 한 번에 나오니까<br>“그래서 뭐가 문제고, 왜 Gateway API로 넘어가야 한다는 거지?”라는 생각이 들었다.</p>
<p>그래서 나처럼 처음 보는 사람 기준으로 다시 정리해보면 핵심은 이거다.</p>
<br>

<h2 id="1-ingress는-외부-요청을-내부-서비스로-보내는-규칙이다">1. Ingress는 외부 요청을 내부 서비스로 보내는 규칙이다</h2>
<p>쿠버네티스 안에는 여러 서비스와 Pod가 있다.<br>그런데 외부 사용자가 클러스터 내부의 서비스에 접근하려면 중간에 진입점이 필요하다.</p>
<p>이때 사용하는 리소스가 <strong>Ingress</strong>다.</p>
<p>예를 들면 이런 식이다.</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80</code></pre>
<p>이 설정은 쉽게 말하면 이런 뜻이다.</p>
<pre><code class="language-text">example.com/api 로 요청이 들어오면
api-service의 80번 포트로 보내라</code></pre>
<p>여기까지는 Kubernetes 표준 Ingress 설정이다.</p>
<br>

<h2 id="2-그런데-ingress만-있다고-실제로-동작하는-것은-아니다">2. 그런데 Ingress만 있다고 실제로 동작하는 것은 아니다</h2>
<p>Ingress는 “규칙”이다.<br>하지만 이 규칙을 실제로 읽고 동작시키는 프로그램이 따로 필요하다.</p>
<p>그게 <strong>Ingress Controller</strong>다.</p>
<p>대표적으로 많이 쓰인 것이 <code>ingress-nginx</code>다.</p>
<p>흐름은 이렇게 볼 수 있다.</p>
<pre><code class="language-text">사용자 요청
  ↓
Ingress Controller
  ↓
Ingress 규칙 확인
  ↓
Service
  ↓
Pod</code></pre>
<p>즉, Ingress는 설정이고,<br>Ingress Controller는 그 설정을 실제 트래픽 처리로 바꿔주는 역할이다.</p>
<br>

<h2 id="3-문제는-ingress-기본-기능만으로는-부족했다">3. 문제는 Ingress 기본 기능만으로는 부족했다</h2>
<p>Ingress 기본 설정만으로는 단순 라우팅 정도는 할 수 있다.</p>
<p>예를 들면 이런 것들이다.</p>
<ul>
<li>특정 도메인으로 들어온 요청 처리</li>
<li>특정 path로 들어온 요청을 특정 서비스로 전달</li>
<li>TLS 인증서 연결</li>
</ul>
<p>하지만 실제 운영 환경에서는 이것보다 더 많은 기능이 필요하다.</p>
<p>예를 들면 다음과 같다.</p>
<pre><code class="language-text">/api 경로를 내부 서비스에는 / 로 바꿔서 넘기고 싶다
HTTP로 들어온 요청을 HTTPS로 강제 전환하고 싶다
트래픽 10%만 새 버전으로 보내고 싶다
요청 body 크기 제한을 늘리고 싶다
특정 header가 있을 때만 다른 서비스로 보내고 싶다</code></pre>
<p>이런 세부 기능은 Ingress 기본 스펙만으로는 표현하기 어려운 경우가 많았다.</p>
<p>그래서 <code>ingress-nginx</code>에서는 annotation을 사용했다.</p>
<br>

<h2 id="4-annotation은-컨트롤러-전용-옵션에-가깝다">4. annotation은 컨트롤러 전용 옵션에 가깝다</h2>
<p>예를 들면 아래와 같은 설정이다.</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: &quot;true&quot;
    nginx.ingress.kubernetes.io/canary: &quot;true&quot;
    nginx.ingress.kubernetes.io/canary-weight: &quot;10&quot;
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80</code></pre>
<p>여기서 중요한 부분은 이거다.</p>
<pre><code class="language-yaml">annotations:
  nginx.ingress.kubernetes.io/rewrite-target: /
  nginx.ingress.kubernetes.io/ssl-redirect: &quot;true&quot;
  nginx.ingress.kubernetes.io/canary: &quot;true&quot;
  nginx.ingress.kubernetes.io/canary-weight: &quot;10&quot;</code></pre>
<p>이 annotation들은 Kubernetes 표준 기능이라기보다는<br><strong>NGINX Ingress Controller가 알아듣는 전용 설정</strong>에 가깝다.</p>
<p>각 설정은 대략 이런 의미다.</p>
<ul>
<li><code>rewrite-target</code>: 요청 경로를 바꿔서 내부 서비스로 전달한다.</li>
<li><code>ssl-redirect</code>: HTTP 요청을 HTTPS로 리다이렉트한다.</li>
<li><code>canary</code>: 카나리 배포용 Ingress로 사용한다.</li>
<li><code>canary-weight</code>: 전체 트래픽 중 일부 비율만 이 서비스로 보낸다.</li>
</ul>
<br>

<h2 id="5-그래서-nginx-ingress-controller에-의존한다는-말이-나온다">5. 그래서 NGINX Ingress Controller에 의존한다는 말이 나온다</h2>
<p>Ingress 자체는 Kubernetes 표준이다.<br>하지만 <code>nginx.ingress.kubernetes.io/...</code> annotation은 Kubernetes 공통 표준이 아니다.</p>
<p>이 annotation을 해석하는 주체는 Kubernetes가 아니라 <strong>NGINX Ingress Controller</strong>다.</p>
<p>흐름은 이렇게 된다.</p>
<pre><code class="language-text">Ingress YAML 작성
  ↓
Kubernetes API Server에 저장
  ↓
NGINX Ingress Controller가 Ingress를 감시
  ↓
nginx.ingress.kubernetes.io/... annotation을 읽음
  ↓
NGINX 설정으로 변환
  ↓
트래픽 처리</code></pre>
<p>그래서 NGINX Ingress Controller를 사용할 때는 잘 동작한다.</p>
<p>하지만 컨트롤러를 AWS ALB Controller, Traefik, HAProxy, Istio 같은 것으로 바꾸면<br><code>nginx.ingress.kubernetes.io/...</code> annotation은 동작하지 않을 수 있다.</p>
<p>왜냐하면 다른 컨트롤러는 NGINX 전용 annotation을 해석하지 않기 때문이다.</p>
<p>즉, 같은 Ingress YAML이라도 컨트롤러가 바뀌면 다시 수정해야 할 수 있다.</p>
<br>

<h2 id="6-한-줄로-말하면-ingress는-표준인데-annotation은-사투리다">6. 한 줄로 말하면 Ingress는 표준인데 annotation은 사투리다</h2>
<p>이렇게 이해하면 쉽다.</p>
<pre><code class="language-text">Ingress의 spec.rules, spec.tls
→ Kubernetes 표준어

nginx.ingress.kubernetes.io/rewrite-target
nginx.ingress.kubernetes.io/canary-weight
→ NGINX Ingress Controller가 알아듣는 사투리</code></pre>
<p>그래서 <code>nginx.ingress.kubernetes.io/...</code> annotation을 많이 쓰고 있다면<br>겉으로는 Kubernetes Ingress를 쓰는 것처럼 보여도<br>실제로는 NGINX Ingress Controller에 많이 묶여 있는 상태라고 볼 수 있다.</p>
<br>

<h2 id="7-gateway-api는-이-문제를-줄이려고-나온-흐름이다">7. Gateway API는 이 문제를 줄이려고 나온 흐름이다</h2>
<p>Gateway API는 Ingress의 한계를 보완하기 위해 나온 Kubernetes 네트워킹 API다.</p>
<p>Ingress에서는 하나의 리소스 안에 여러 역할이 섞여 있었다.</p>
<pre><code class="language-text">도메인 설정
TLS 설정
라우팅 설정
서비스 연결
컨트롤러별 세부 기능</code></pre>
<p>Gateway API는 이 역할을 나눈다.</p>
<table>
<thead>
<tr>
<th>리소스</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>GatewayClass</code></td>
<td>어떤 Gateway Controller를 사용할지 정의</td>
</tr>
<tr>
<td><code>Gateway</code></td>
<td>외부 진입점, 포트, 프로토콜, TLS 설정</td>
</tr>
<tr>
<td><code>HTTPRoute</code></td>
<td>어떤 경로를 어떤 서비스로 보낼지 정의</td>
</tr>
</tbody></table>
<p>쉽게 보면 이런 구조다.</p>
<pre><code class="language-text">외부 사용자
  ↓
Gateway
  ↓
HTTPRoute
  ↓
Service
  ↓
Pod</code></pre>
<p><code>Gateway</code>는 인프라팀이 관리하고,<br><code>HTTPRoute</code>는 앱팀이 관리하는 식으로 역할을 나눌 수 있다.</p>
<br>

<h2 id="8-gateway-api를-쓰면-뭐가-좋아지는가">8. Gateway API를 쓰면 뭐가 좋아지는가?</h2>
<p>핵심은 세 가지다.</p>
<h3 id="첫-번째-annotation-의존을-줄일-수-있다">첫 번째, annotation 의존을 줄일 수 있다</h3>
<p>Ingress에서는 트래픽 분할 같은 기능을 annotation으로 처리하는 경우가 많았다.</p>
<pre><code class="language-yaml">metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: &quot;true&quot;
    nginx.ingress.kubernetes.io/canary-weight: &quot;10&quot;</code></pre>
<p>Gateway API에서는 이런 트래픽 분할을 <code>HTTPRoute</code>에서 더 표준적인 방식으로 표현할 수 있다.</p>
<pre><code class="language-yaml">backendRefs:
- name: api-v1
  port: 80
  weight: 90
- name: api-v2
  port: 80
  weight: 10</code></pre>
<p>이 설정은 <code>api-v1</code>에 90%, <code>api-v2</code>에 10% 트래픽을 보내겠다는 의미다.</p>
<p>즉, 특정 NGINX annotation에 의존하는 것이 아니라<br>Gateway API의 표준 리소스 구조 안에서 표현할 수 있다.</p>
<br>

<h3 id="두-번째-역할-분리가-쉬워진다">두 번째, 역할 분리가 쉬워진다</h3>
<p>Ingress는 하나의 리소스 안에 많은 설정이 들어간다.</p>
<p>그래서 앱팀이 라우팅만 수정하고 싶어도 TLS나 Gateway 성격의 설정까지 같이 보게 된다.</p>
<p>Gateway API는 역할이 나뉘어 있다.</p>
<pre><code class="language-text">인프라팀
→ GatewayClass, Gateway 관리

앱팀
→ HTTPRoute 관리</code></pre>
<p>이렇게 되면 권한 분리도 쉬워진다.</p>
<p>예를 들어 앱팀에는 <code>HTTPRoute</code>만 수정할 수 있게 하고,<br>인프라팀만 <code>Gateway</code>와 TLS 설정을 수정할 수 있게 만들 수 있다.</p>
<br>

<h3 id="세-번째-앞으로의-표준-흐름에-맞다">세 번째, 앞으로의 표준 흐름에 맞다</h3>
<p>Kubernetes에서는 Ingress보다 Gateway API를 더 확장된 네트워킹 표준으로 가져가려는 흐름이다.</p>
<p>Ingress는 주로 HTTP/HTTPS 라우팅 중심이었다.<br>Gateway API는 HTTP뿐 아니라 gRPC, TCP 같은 트래픽까지 더 넓게 다루려는 구조다.</p>
<p>예를 들면 이런 리소스들이 있다.</p>
<ul>
<li><code>HTTPRoute</code></li>
<li><code>GRPCRoute</code></li>
<li><code>TCPRoute</code></li>
<li><code>TLSRoute</code></li>
</ul>
<p>즉, Gateway API는 단순히 Ingress를 이름만 바꾼 것이 아니라<br>쿠버네티스 네트워킹 구조를 더 세분화하고 표준화하려는 방향이라고 볼 수 있다.</p>
<br>

<h2 id="9-그래도-gateway-api가-모든-의존성을-없애는-것은-아니다">9. 그래도 Gateway API가 모든 의존성을 없애는 것은 아니다</h2>
<p>여기서 오해하면 안 되는 부분이 있다.</p>
<p>Gateway API를 쓴다고 해서 특정 컨트롤러 의존성이 완전히 사라지는 것은 아니다.</p>
<p>Gateway API도 실제로 동작하려면 컨트롤러가 필요하다.</p>
<p>예를 들면 다음과 같은 것들이 있다.</p>
<ul>
<li>NGINX Gateway Fabric</li>
<li>Envoy Gateway</li>
<li>Istio</li>
<li>Traefik</li>
<li>Kong</li>
<li>APISIX</li>
<li>AWS Gateway API Controller</li>
<li>GKE Gateway Controller</li>
<li>Azure Application Gateway for Containers</li>
</ul>
<p>즉, Gateway API도 컨트롤러가 있어야 실제 트래픽을 처리한다.</p>
<p>다만 Ingress 시절보다 공통 표준 스펙이 더 넓어졌기 때문에,<br>컨트롤러별 annotation에 의존하던 부분을 줄일 수 있다.</p>
<p>정리하면 이렇다.</p>
<pre><code class="language-text">Ingress
→ 기본 기능은 표준이지만, 실무 고급 기능은 컨트롤러별 annotation에 많이 의존했다.

Gateway API
→ 라우팅, Gateway, TLS, 트래픽 분할 같은 기능을 더 표준화된 리소스로 나누어 관리하려는 방식이다.</code></pre>
<br>

<h2 id="10-내가-이해한-최종-결론">10. 내가 이해한 최종 결론</h2>
<p>내가 이해한 핵심은 이렇다.</p>
<p>Ingress 자체가 나쁜 것은 아니다.<br>문제는 실제 운영에서 필요한 세부 기능을 Ingress 기본 스펙만으로 다 표현하기 어려웠고,<br>그래서 NGINX 같은 특정 Ingress Controller의 annotation에 많이 의존하게 되었다는 점이다.</p>
<p>그러다 보니 컨트롤러를 바꾸거나, 운영 구조가 커질수록 관리가 어려워질 수 있다.</p>
<p>Gateway API는 이 문제를 줄이기 위해 나온 흐름이다.</p>
<ul>
<li>라우팅 설정은 <code>HTTPRoute</code>로 분리한다.</li>
<li>외부 진입점과 TLS 설정은 <code>Gateway</code>로 분리한다.</li>
<li>어떤 컨트롤러를 쓸지는 <code>GatewayClass</code>로 정의한다.</li>
<li>트래픽 분할 같은 기능도 표준 리소스 안에서 표현하려고 한다.</li>
<li>인프라팀과 앱팀의 역할 분리도 더 쉬워진다.</li>
</ul>
<p>결국 이 글에서 말하려는 핵심은<br><strong>“Ingress NGINX가 은퇴하니까 당장 모든 것을 바꿔야 한다”</strong>가 아니다.</p>
<p>더 정확히는<br><strong>“기존 Ingress + NGINX annotation 중심의 운영 방식에서, Gateway API 기반의 표준화된 네트워킹 구조로 넘어가는 흐름을 이해해야 한다”</strong>에 가깝다.</p>
<p>그래서 신규 서비스나 신규 클러스터를 설계한다면<br>이제는 Ingress만 당연하게 선택하기보다는 Gateway API도 같이 검토하는 것이 맞다고 본다.</p>
<br>

<h2 id="참고">참고</h2>
<ul>
<li><a href="https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/">Kubernetes 공식 블로그 - Ingress NGINX Retirement</a></li>
<li><a href="https://gateway-api.sigs.k8s.io/">Kubernetes Gateway API 공식 문서</a></li>
<li><a href="https://kubernetes.io/blog/2026/03/20/ingress2gateway-1-0-release/">Ingress2Gateway 1.0 Release</a></li>
<li><a href="https://github.com/nginxinc/nginx-gateway-fabric">NGINX Gateway Fabric</a></li>
<li><a href="https://www.cloudbro.ai/t/kubernetes-ingress-nginx-retirement-gateway-api/3149/7">cloudbro 원문 토론</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[구름 서포터즈] 구름 부트캠픙와 뽀시래기 프로젝트 회고]]></title>
            <link>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EB%BD%80%EC%8B%9C%EB%9E%98%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EA%B4%80%ED%95%9C-%EB%AA%A8%EB%93%A0-%EA%B2%83</link>
            <guid>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EB%BD%80%EC%8B%9C%EB%9E%98%EA%B8%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EA%B4%80%ED%95%9C-%EB%AA%A8%EB%93%A0-%EA%B2%83</guid>
            <pubDate>Wed, 20 May 2026 00:34:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 콘텐츠는 구름 서포터즈 활동으로 지원을 받아 작성된 교육생의 실제 경험 후기입니다.</p>
</blockquote>
<p>2026년 05월 20일. 오늘을 기점으로 6개월간의 구름 부트캠프를 수료하게 되었다.</p>
<br>

<p>깃허브 주소 : <a href="https://github.com/Goorm4I/pposiraegi-ecommerce">https://github.com/Goorm4I/pposiraegi-ecommerce</a>
회의록 노션 주소 : <a href="https://www.notion.so/goormkdx/Four-I-2fec0ff4ce3180838568e13dbd3f8bac">https://www.notion.so/goormkdx/Four-I-2fec0ff4ce3180838568e13dbd3f8bac</a></p>
<br>

<h1 id="뽀시래기-프로젝트란">뽀시래기 프로젝트란?</h1>
<p>뽀시래기는 <strong>반려동물 용품을 타임딜(시간 한정 특가) 방식으로 판매하는 이커머스 플랫폼</strong>이다.
타임딜 특성상 특정 시간대에 트래픽이 폭발적으로 몰리고, 재고가 초과 판매되지 않도록 정확하게 제어해야 한다.
이런 요구사항 때문에 단순한 웹 서비스가 아니라, <strong>클라우드 네이티브 인프라</strong> 위에서 운영되는 서비스로 설계했다.</p>
<br>

<p>팀명은 <strong>포아이팀</strong>이고, 이나형(팀장/인프라), 박지훈(인프라/프론트엔드), 서주원(Git관리/백엔드) 3명으로 구성되었다.
프로젝트는 총 3개 Phase로 나뉘어 진행되었으며, 이 글은 최종 단계인 <strong>Phase 3 (EKS v3.5)</strong> 를 중심으로 한 회고이다.</p>
<br>
<br>

<h1 id="뽀시래기의-역사--ecs에서-eks까지">뽀시래기의 역사 — ECS에서 EKS까지</h1>
<h2 id="phase-1-·-2--ecs-fargate-기반-msa">Phase 1 · 2 — ECS Fargate 기반 MSA</h2>
<p>처음에는 <strong>AWS ECS Fargate</strong> 위에 MSA(마이크로서비스 아키텍처) 구조로 서비스를 구축했다.
api-gateway, user-service, order-service, product-service 4개의 서비스를 각각 컨테이너로 분리하고,
RDS PostgreSQL과 ElastiCache Redis를 데이터 레이어로 사용했다.</p>
<br>

<p>ECS는 관리 오버헤드가 적고 빠르게 구축할 수 있다는 장점이 있었지만,
운영을 하면서 다음과 같은 한계에 부딪혔다.</p>
<ul>
<li><strong>트래픽 제어의 한계</strong> — 타임딜처럼 특정 서비스에 트래픽이 몰릴 때 서비스 간 세밀한 라우팅이나 서킷브레이킹이 ECS로는 어려웠다.</li>
<li><strong>수동 작업 잔존</strong> — 인프라 변경 시 콘솔에서 직접 수정해야 하는 부분이 남아있었다.</li>
<li><strong>모니터링 한계</strong> — CloudWatch 수준으로는 Pod·Node 단위의 세밀한 지표 수집이 불가능했다.</li>
</ul>
<br>

<h2 id="phase-3--eks로의-전환">Phase 3 — EKS로의 전환</h2>
<p>이러한 한계를 극복하기 위해 Phase 3에서는 <strong>Amazon EKS(Elastic Kubernetes Service)</strong> 로 전환을 결정했다.
ECS보다 복잡도는 높지만, IAM·네트워크·스토리지·배포·관측성의 경계를 명확히 컨트롤할 수 있다는 점이 핵심이었다.</p>
<br>

<p>단순히 EKS를 띄우는 것이 목표가 아니었다.
<strong>각 도구가 맡는 책임 경계를 명확히 정리하고, 재현 가능한 운영 흐름을 만드는 것</strong>이 이번 Phase 3의 진짜 목표였다.</p>
<br>
<br>

<h1 id="phase-3-기술-스택--무엇을-왜-썼나">Phase 3 기술 스택 — 무엇을, 왜 썼나</h1>
<h2 id="terraform--인프라-전체를-코드로">Terraform — 인프라 전체를 코드로</h2>
<p>VPC, EKS 클러스터, RDS, ElastiCache, ALB, IAM, IRSA까지 모든 AWS 인프라를 <strong>Terraform</strong>으로 코드화했다.
코드로 관리하면 팀원 누구든 동일한 순서로 실행하면 동일한 인프라를 재현할 수 있고,
변경 이력도 Git에 남아 언제 무엇이 바뀌었는지 추적할 수 있다.</p>
<br>

<h2 id="bootstrap-script--플랫폼-컴포넌트-1회-설치-자동화">Bootstrap Script — 플랫폼 컴포넌트 1회 설치 자동화</h2>
<p>Terraform이 AWS 리소스를 만들고 나면, EKS 위에 올라가는 플랫폼 컴포넌트들을 설치해야 한다.
이를 위해 <code>bootstrap-platform.sh</code> 쉘 스크립트를 직접 작성했다.
metrics-server → ArgoCD → Karpenter → AWS LBC → Istio → Prometheus+Grafana → ESO 순서로
<strong>한 번의 실행으로 전체 플랫폼을 구성</strong>할 수 있도록 자동화했다.
<code>--only monitoring</code>, <code>--from eso</code> 같은 옵션으로 특정 단계만 재실행하는 것도 가능하다.</p>
<br>

<h2 id="github-actions--argocd--gitops-cicd">GitHub Actions + ArgoCD — GitOps CI/CD</h2>
<p><strong>GitHub Actions</strong>는 CI를 담당한다. main 브랜치에 코드가 push되면 자동으로 Docker 이미지를 빌드하고 ECR에 push한다.
이때 이미지 태그는 <code>latest</code> 대신 <strong>Git commit SHA</strong>를 사용한다. 어떤 코드가 배포됐는지 정확히 추적하고, 문제가 생기면 이전 SHA 태그로 즉시 롤백할 수 있기 때문이다.</p>
<br>

<p><strong>ArgoCD</strong>는 CD를 담당한다. <code>infrastructure/kubernetes/</code> 폴더를 지속적으로 감시하다가
변경이 감지되면 자동으로 EKS 클러스터에 sync한다. 이것이 <strong>GitOps</strong> 방식이다.
Git이 단일 진실의 원천(Single Source of Truth)이 되어, 배포 상태가 항상 코드와 일치한다.</p>
<br>

<h2 id="karpenter--워크로드-특성별-노드-프로비저닝">Karpenter — 워크로드 특성별 노드 프로비저닝</h2>
<p>Karpenter를 단순히 &quot;비용 절감 도구&quot;로 보면 안 된다.
<strong>워크로드 특성에 맞는 노드 프로비저닝 정책을 실행하는 운영 정책 실행기</strong>가 더 정확한 표현이다.</p>
<br>

<ul>
<li><strong>모니터링 스택(Prometheus, Grafana, Loki)</strong> → On-Demand 고정. 안정성이 중요해서 Spot이 적합하지 않다.</li>
<li><strong>앱 워크로드(api-gateway, order-service 등)</strong> → Karpenter 탄력 확장. 타임딜 트래픽 폭증 시 Spot + On-Demand 자동 혼합으로 대응한다.</li>
</ul>
<br>

<h2 id="istio-ambient--서비스-메시">Istio Ambient — 서비스 메시</h2>
<p>서비스 간 통신에 <strong>mTLS 암호화</strong>를 적용하고, Waypoint + AuthorizationPolicy로 접근 제어를 구현했다.
<code>/actuator/prometheus</code> 같은 내부 엔드포인트를 외부에서 직접 접근하지 못하도록 경계를 만들었다.</p>
<br>

<h2 id="prometheus--grafana--loki--tempo--완전한-observability">Prometheus + Grafana + Loki + Tempo — 완전한 Observability</h2>
<p>모니터링은 &quot;설치했다&quot;가 아니라 <strong>실제 병목을 찾기 위해 사용</strong>했다.</p>
<ul>
<li><strong>Prometheus</strong> — Pod/Node 단위 메트릭 수집. Spring Boot actuator <code>/actuator/prometheus</code> 엔드포인트 스크래핑</li>
<li><strong>Grafana</strong> — JVM, Kubernetes 클러스터, Karpenter 대시보드 통합 시각화</li>
<li><strong>Loki</strong> — 서비스 로그 수집 및 검색 (S3에 IRSA로 저장)</li>
<li><strong>Tempo + OTEL Collector</strong> — 분산 트레이싱. 서비스 간 지연 구간 추적</li>
<li><strong>Alertmanager → Discord</strong> — 이상 징후 발생 시 Discord 웹훅으로 즉시 알림</li>
</ul>
<br>

<p>실제로 k6 부하 테스트(300 VU, 30초)를 통해 order-service의 HikariCP 커넥션 풀이 병목임을 발견했고,
<code>maximum-pool-size</code>를 10 → 30으로 조정해서 <strong>평균 응답시간을 4.15초 → 1.69초로 59% 개선</strong>했다.</p>
<p>해당 내용은 <a href="https://velog.io/@lee_nah/Hikari-Connection-Pool-%ED%8A%9C%EB%8B%9D%EC%9C%BC%EB%A1%9C-p95-%EC%9D%91%EB%8B%B5%EC%8B%9C%EA%B0%84-60-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0">링크</a>에서 확인해볼 수 있다.
<br></p>
<h2 id="eso--aws-ssm--secret-자동-관리">ESO + AWS SSM — Secret 자동 관리</h2>
<p><strong>External Secrets Operator(ESO)</strong> 를 통해 AWS SSM Parameter Store에 저장된 DB 비밀번호, JWT Secret 등을
Kubernetes Secret으로 자동 주입한다. 민감한 정보를 Git에 올리지 않고도 안전하게 관리할 수 있다.
<img src="https://velog.velcdn.com/images/lee_nah/post/63f73d86-09cc-4736-a188-dc7e82f14e77/image.png" alt="">
이런식으로 깃허브 settings에서 관리하고 있다. </p>
<br>

<h2 id="irsa--pod-단위-최소-권한-보안">IRSA — Pod 단위 최소 권한 보안</h2>
<p>기존에는 노드 전체에 IAM 권한을 부여했지만, <strong>IRSA(IAM Roles for Service Accounts)</strong> 를 도입해
Pod 단위로 필요한 권한만 부여하도록 바꿨다. Loki가 S3에 접근하거나 ESO가 SSM에 접근할 때도
각각의 Service Account에 딱 필요한 권한만 가진 IAM Role을 연결했다.</p>
<br>
<br>

<h1 id="뽀시래기-프로젝트-소개">뽀시래기 프로젝트 소개</h1>
<h2 id="뽀시래기-메인페이지-제품-페이지">뽀시래기 메인페이지, 제품 페이지</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/abd51870-e103-419e-819f-039cfbc508c9/image.png" alt="">
뽀시래기는 다음과 같이 반려동물을 파는 이커머스 플랫폼이다.
원래 금액과 할인된 금액을 찾아볼 수 있고, 남는시간과 남은 재고를 한눈에 살펴볼 수 있다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/lee_nah/post/0dbd78be-aa02-4a60-91a3-069e8b3f9ea7/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/b4a5e824-a146-40a3-9f38-4c437ec0b1c9/image.png" alt="">
그런다음, 상품을 누르면 상세페이지로 들어갈 수 있는데 밑으로 스크롤을 내리면 인기 있는 다른 제품을 구매할 수 있게 된다.</p>
<br>

<h2 id="회원가입--로그인-화면">회원가입 &amp; 로그인 화면</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/83850328-872b-4678-9afe-baa252dd6976/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/6702a711-8b3f-4843-9e3b-48342b684446/image.png" alt="">
이렇게 회원가입을 할 수 있게 되고, 로그인을 통해 서비스를 이용할 수 있다.
그러나 물품을 구매하기 위해서는 배송지를 등록을 먼저 해야한다.
<img src="https://velog.velcdn.com/images/lee_nah/post/6306eddd-ff26-4d38-8b0f-ca2fa7ec934f/image.png" alt="">
다음과 같이 마이페이지를 클릭하게 되면 배송지관리, 전화번호 관리 등 기본적인 설정을 할 수 있게되고 구매한 물품들의 목록을 확인할 수도 있다.</p>
<br>

<h2 id="찜-기능">찜 기능</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/577af14e-90dc-4189-8fe5-fb96e9a64677/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/526cb948-13b6-4a7b-9d89-c939f4dc429d/image.png" alt="">
다음과 같이 찜을 하게 되면 상단에 하트를 눌러 어느 제품들을 찜했는지도 살펴볼 수 있다.</p>
<br>

<h2 id="관리자-페이지-구현">관리자 페이지 구현</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b0b1cf69-5185-44ae-bce0-0384d97bee46/image.png" alt="">
관리자로 로그인하게 되면 상단과 하단에서 관리자 버튼과 모니터링 버튼이 생긴다.
<img src="https://velog.velcdn.com/images/lee_nah/post/1b0b137a-feac-4734-80e7-e28b859cb59f/image.png" alt="">
관리자같은 경우에는 현재 진행중인 딜을 수정하거나 새로운 딜을 만들 수 있다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/lee_nah/post/96b01db5-4656-405b-891b-4a62f29259a7/image.png" alt="">
또한 모니터링 버튼을 눌러 현재 서비스별 리소스가 어떻게 동작하는지 확인할 수 있다.</p>
<br>

<h1 id="후기">후기</h1>
<p>6개월동안, 거의 7개월이 되는 기간동안 구름과 kt에 지원을 받아서 인프런 무료수강을 하면서 팀원들과 스터디를 매일매일 하면서 kubernetes, docker에 대한 트러블슈팅하던 것이 며칠 전 같은데 벌써 하나의 프로젝트를 완성하면서 우수상까지 받으며 수료할 수 있어서 감회가 깊게 느껴졌다.</p>
<p>짧지 않은 기간이었지만 6개월동안 몰입할 수 있는 환경덕에 1년 이상의 성장을 이룰 수 있었다고 생각이 들며 퇴사를 하면서 이 부트캠프를 선택했던 내 선택에 후회하지 않는다고 자신할 수 있다. 
주변에 인프라나 클라우드에 관심이 있는 후배가 있다면 당당하게 이 구름의 클라우드 네이티브 부트캠프를 추천하고 싶다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hikari Connection Pool 튜닝으로 p95 응답시간 60% 개선하기]]></title>
            <link>https://velog.io/@lee_nah/Hikari-Connection-Pool-%ED%8A%9C%EB%8B%9D%EC%9C%BC%EB%A1%9C-p95-%EC%9D%91%EB%8B%B5%EC%8B%9C%EA%B0%84-60-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/Hikari-Connection-Pool-%ED%8A%9C%EB%8B%9D%EC%9C%BC%EB%A1%9C-p95-%EC%9D%91%EB%8B%B5%EC%8B%9C%EA%B0%84-60-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 13:19:27 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@lee_nah/k6%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0">전 포스팅</a>에서 k6로 300 VUs 부하테스트를 돌렸더니 평균 응답시간이 4초가 넘게 나왔다. 원인을 찾아보니 <code>application-prod.yaml</code>에 Hikari 설정 자체가 없어서 기본값인 커넥션 풀 10개로 돌아가고 있었다.</p>
<br>

<h1 id="문제">문제</h1>
<pre><code>avg=4.15s / p(90)=5.73s / p(95)=6.09s</code></pre><p>300명이 동시에 주문 요청을 보내면 DB 커넥션 풀이 금방 고갈된다. 커넥션을 얻지 못한 요청들은 줄을 서서 대기하게 되고, 그게 응답 지연으로 이어지는 구조였다.</p>
<blockquote>
<p><strong>HikariCP란?</strong> Spring Boot에서 기본으로 사용하는 DB 커넥션 풀 라이브러리다. 커넥션 풀이란 DB 연결을 미리 여러 개 만들어두고 요청이 들어올 때마다 재사용하는 방식인데, 풀 사이즈가 너무 작으면 동시 요청이 몰릴 때 병목이 생긴다. 기본값은 <code>maximum-pool-size: 10</code>으로, 동시에 10개의 DB 연결만 허용한다.</p>
</blockquote>
<br>

<h1 id="해결">해결</h1>
<p><code>application-prod.yaml</code>에 Hikari 설정이 아예 없었기 때문에 아래 설정을 추가해주었다.</p>
<pre><code class="language-yaml">hikari:
  maximum-pool-size: 30      # 최대 커넥션 수 (기본값 10 → 30으로 증가)
  minimum-idle: 10           # 유휴 상태에서 유지할 최소 커넥션 수
  connection-timeout: 30000  # 커넥션 획득 대기 최대 시간 (30초)
  idle-timeout: 600000       # 유휴 커넥션 유지 시간 (10분)
  max-lifetime: 1800000      # 커넥션 최대 수명 (30분)</code></pre>
<p>핵심은 <code>maximum-pool-size</code>를 10에서 30으로 늘린 것이다. 동시에 처리할 수 있는 DB 연결이 3배로 늘어나니 대기 줄이 줄어들 수밖에 없다.</p>
<br>

<h1 id="결과">결과</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/3dab1576-c9bf-4c64-b0ec-37f4a4fb0106/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/c4b389d2-e2d1-4089-960e-dc58baa02810/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>튜닝 전</th>
<th>튜닝 후</th>
<th>개선율</th>
</tr>
</thead>
<tbody><tr>
<td>avg</td>
<td>4.15s</td>
<td>1.69s</td>
<td>59% ↓</td>
</tr>
<tr>
<td>p(90)</td>
<td>5.73s</td>
<td>3.4s</td>
<td>41% ↓</td>
</tr>
<tr>
<td>p(95)</td>
<td>6.09s</td>
<td>4.62s</td>
<td>24% ↓</td>
</tr>
</tbody></table>
<p>설정 몇 줄 추가했을 뿐인데 평균 응답시간이 4.15초 → 1.69초로 약 60% 개선됐다.</p>
<p>코드 한 줄 안 바꾸고 설정만으로 이 정도 성능 차이가 난다는 게 인상적이었다. 기본값이 항상 최선은 아니다. 특히 트래픽이 몰리는 서비스라면 커넥션 풀 사이즈는 반드시 확인하고 튜닝해야 한다는 걸 이번에 직접 체감하게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[k6로 재고 부하테스트하기]]></title>
            <link>https://velog.io/@lee_nah/k6%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/k6%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 12:16:55 GMT</pubDate>
            <description><![CDATA[<p>현재 부트캠프에서 <strong>뽀시래기</strong>라는 프로젝트를 진행하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/888d5a39-1880-4054-91e5-9a5d6f60a6cd/image.png" alt=""></p>
<p>도메인은 <a href="https://pposiraegi.cloud/login">https://pposiraegi.cloud/login</a> 이며, 타임딜 서비스 특성상 특정 시간대에 트래픽이 순간적으로 폭증하는 상황이 발생한다. 이 트래픽을 실제로 버틸 수 있는지 검증하기 위해 부하 테스트 도구인 <strong>k6</strong>를 사용해보기로 했다.</p>
<blockquote>
<p>k6란? JavaScript로 테스트 스크립트를 작성해 HTTP 요청을 대량으로 보내고, 응답 시간·성공률 등을 측정할 수 있는 오픈소스 부하 테스트 도구다.</p>
</blockquote>
<br>

<h1 id="k6-설치">k6 설치</h1>
<p>공식 설치 문서: <a href="https://grafana.com/docs/k6/latest/set-up/install-k6/">https://grafana.com/docs/k6/latest/set-up/install-k6/</a></p>
<p>Windows 환경이므로 PowerShell에서 아래 명령어를 실행한다.</p>
<pre><code class="language-bash">winget install k6 --source winget</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/44cd0d66-63b1-4e5d-8239-bdff09df42ca/image.png" alt=""></p>
<br>

<h1 id="k6-테스트-파일-작성">k6 테스트 파일 작성</h1>
<p><code>test.js</code> 파일을 생성하고 아래 스크립트를 작성했다.</p>
<pre><code class="language-javascript">import http from &#39;k6/http&#39;;
import { sleep, check } from &#39;k6&#39;;

export const options = {
  vus: 300,       // 동시 접속 가상 유저 300명
  duration: &#39;30s&#39;, // 30초 동안 테스트 실행
};

// 테스트 시작 전 1회 실행 - 로그인해서 토큰을 가져옴
export function setup() {
  const res = http.post(
    &#39;https://pposiraegi.cloud/api/v1/auth/login&#39;,
    JSON.stringify({ email: &#39;test@test.com&#39;, password: &#39;123456&#39; }),
    { headers: { &#39;Content-Type&#39;: &#39;application/json&#39; } }
  );
  console.log(&#39;로그인 응답:&#39;, res.status, res.body);
  const token = res.json(&#39;data.accessToken&#39;);
  console.log(&#39;토큰:&#39;, token);
  return { token };
}

// 가상 유저 300명이 30초 동안 반복 실행하는 시나리오
export default function (data) {
  const body = `{&quot;orderItems&quot;:[{&quot;skuId&quot;:839470480585919574,&quot;quantity&quot;:1}]}`;

  const res = http.post(
    &#39;https://pposiraegi.cloud/api/v1/orders&#39;,
    body,
    {
      headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
        &#39;Authorization&#39;: `Bearer ${data.token}`,
      }
    }
  );
  console.log(&#39;주문 응답:&#39;, res.status, res.body);
  check(res, {
    &#39;status is 200&#39;: (r) =&gt; r.status === 200,
  });
  sleep(1);
}</code></pre>
<p>작성 후 아래 명령어로 실행한다.</p>
<pre><code class="language-bash">k6 run test.js</code></pre>
<br>

<h1 id="실행-결과">실행 결과</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/066c5dcc-45a9-42ed-988d-115c0dc06971/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/2d3b87b3-09b3-40cb-ae70-f03730f9dc24/image.png" alt=""></p>
<p>결과를 해석하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>checks_succeeded</td>
<td>100% (1856개 요청 전부 성공)</td>
</tr>
<tr>
<td>평균 응답 시간 (avg)</td>
<td>4.15초</td>
</tr>
<tr>
<td>90% 응답 시간 (p90)</td>
<td>5.73초 이내</td>
</tr>
<tr>
<td>95% 응답 시간 (p95)</td>
<td>6.09초 이내</td>
</tr>
<tr>
<td>최대 응답 시간 (max)</td>
<td>9.7초</td>
</tr>
</tbody></table>
<p>요청 자체는 전부 성공했지만 응답 속도가 너무 느리다. 평균 4초, 최대 약 10초는 타임딜 서비스에서는 치명적인 수치다. DB 커넥션 풀을 관리하는 <strong>HikariCP 튜닝</strong>이 필요할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD 삽질기3] ArgoCD CD 고치기]]></title>
            <link>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B03-ArgoCD-CD-%EA%B3%A0%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B03-ArgoCD-CD-%EA%B3%A0%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 11:23:42 GMT</pubDate>
            <description><![CDATA[<p>GitHub Actions도 고치고, 부트스트랩도 정상 실행됐는데 이번엔 ArgoCD가 CD(배포) 과정에서 문제가 생겼다. ArgoCD는 Git 저장소를 바라보며 Kubernetes 클러스터에 자동으로 배포해주는 도구인데, 이 단계에서 또 막혔다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d13ea4bc-1670-41a1-8e78-4c5070812721/image.png" alt=""></p>
<p>위처럼 <strong>OutOfSync + Degraded</strong> 상태가 떠서 로그를 확인해보기로 했다.</p>
<blockquote>
<ul>
<li><strong>OutOfSync</strong>: Git에 있는 설정과 실제 클러스터 상태가 다르다는 뜻</li>
<li><strong>Degraded</strong>: 배포된 애플리케이션이 정상 동작하지 않는다는 뜻</li>
</ul>
</blockquote>
<br>

<h1 id="로그-확인">로그 확인</h1>
<pre><code class="language-bash">kubectl describe application pposiraegi -n argocd</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/607157f1-020b-47da-9996-d34bd3c53bcb/image.png" alt=""></p>
<p>로그에서 눈에 띄는 내용은 다음과 같았다.</p>
<pre><code class="language-bash">external-secrets.io/ClusterSecretStore CRD가 없음
external-secrets.io/ExternalSecret CRD가 없음</code></pre>
<p><code>ClusterSecretStore</code>와 <code>ExternalSecret</code> CRD가 없다는 에러였다.</p>
<blockquote>
<p>CRD(Custom Resource Definition)란 Kubernetes에 기본으로 없는 리소스 타입을 사용자가 직접 정의해서 추가하는 것이다. ESO를 설치하면 이 CRD들이 함께 등록된다.</p>
</blockquote>
<p>원인은 이전 bootstrap 스크립트 실행 중 Loki 설치 단계에서 Windows 환경 문제로 오류가 발생했고, 그 이후 단계인 ESO(External Secrets Operator) 설치까지 진행되지 못했기 때문이었다. ESO는 AWS Secrets Manager 같은 외부 시크릿 저장소의 값을 Kubernetes Secret으로 자동으로 동기화해주는 도구다.</p>
<p>따라서 ESO만 따로 설치해주기로 했다.</p>
<br>

<h1 id="eso-따로-설치">ESO 따로 설치</h1>
<pre><code class="language-bash">./scripts/bootstrap-platform.sh --from eso</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/4b7a1f38-b9fc-4c8d-9e9d-cec8f89e116d/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/feb172a6-ec2f-4481-9881-ebcf774a7cac/image.png" alt=""></p>
<p>ESO 설치는 완료됐는데 여전히 OutOfSync 상태였다. Pod 상태를 확인해봤다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/6dbf3023-d6c2-458e-a487-4ef7051d576d/image.png" alt=""></p>
<p>waypoint만 떠 있고 나머지 서비스들이 올라오지 않은 상태였다. ArgoCD가 자동으로 sync를 하지 못하고 있어서 강제로 sync를 실행해보기로 했다.</p>
<br>

<h1 id="argocd-sync-강제-실행">ArgoCD sync 강제 실행</h1>
<pre><code class="language-bash">kubectl -n argocd exec -it $(kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o jsonpath=&#39;{.items[0].metadata.name}&#39;) -- argocd app sync pposiraegi --insecure --server argocd-server:80</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/f017812e-4d65-407b-9089-524cd9946bcd/image.png" alt=""></p>
<p>ArgoCD CLI를 사용하려면 먼저 로그인이 필요하다고 떴다. 초기 admin 비밀번호를 확인한 뒤, port-forward로 ArgoCD 서버에 접근해서 로그인했다.</p>
<pre><code class="language-bash"># 초기 admin 비밀번호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | base64 -d

# port-forward 후 로그인
kubectl port-forward svc/argocd-server -n argocd 8080:80 &amp;
sleep 3
kubectl -n argocd exec -it $(kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o jsonpath=&#39;{.items[0].metadata.name}&#39;) -- argocd login localhost:8080 --insecure --username admin --password [위에서 확인한 비밀번호]</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/1199e592-d15e-48cd-b2c3-ac35cafe0734/image.png" alt=""></p>
<p>로그인 성공. 이제 sync를 다시 실행했다.</p>
<pre><code class="language-bash">kubectl -n argocd exec -it $(kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o jsonpath=&#39;{.items[0].metadata.name}&#39;) -- argocd app sync pposiraegi --insecure --server localhost:8080</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/34246581-66da-4474-88c0-a2dbd7d896a2/image.png" alt=""></p>
<p>sync가 정상적으로 진행되는 것을 확인했다. 이제 production 네임스페이스의 Pod 상태를 확인해봤다.</p>
<br>

<h1 id="production-pod-확인">production Pod 확인</h1>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d67b91d6-135c-4301-9cb4-652388305426/image.png" alt=""></p>
<p>모든 서비스의 Pod가 정상적으로 올라왔다. 이제 실제 서비스에 접속해봤다.</p>
<br>

<h1 id="서비스-확인">서비스 확인</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/7d63db05-b1d5-49fc-8e73-658edc55c4b1/image.png" alt=""></p>
<p>pposiraegi.cloud에 정상적으로 접속되는 것을 확인했다. 길고 긴 삽질이 드디어 끝났다. </p>
<br>

<h1 id="마무리-정리">마무리 정리</h1>
<p>팀원은 MacOS 환경이라 bootstrap 스크립트가 한 번에 실행됐지만, Windows 환경에서는 여러 문제가 연달아 발생했다.</p>
<ul>
<li>Git Bash에서 <code>sudo</code> 권한 없음 → helm 수동 설치</li>
<li>helm 실행 경로 문제 → PATH 직접 설정</li>
<li>Windows에 <code>/proc</code> 경로가 없음 → Loki 설치 실패</li>
<li>kubectl이 aws CLI 경로를 인식하지 못함 → 심볼릭 링크 + kubeconfig 수동 수정</li>
<li>ESO 미설치로 인한 CRD 누락 → ESO 단독 설치</li>
</ul>
<p>같은 스크립트라도 OS 환경에 따라 전혀 다르게 동작할 수 있다는 걸 몸소 경험했다. 다음에 스크립트를 작성할 때는 OS 환경 분기 처리를 꼭 고려해야겠다고 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Windows에서 EKS 붙이기 — Git Bash, helm, kubeconfig 삽질 총정리 (경로/인증/권한)]]></title>
            <link>https://velog.io/@lee_nah/Windows%EC%97%90%EC%84%9C-EKS-%EB%B6%99%EC%9D%B4%EA%B8%B0-Git-Bash-helm-kubeconfig-%EC%82%BD%EC%A7%88-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EA%B2%BD%EB%A1%9C%EC%9D%B8%EC%A6%9D%EA%B6%8C%ED%95%9C</link>
            <guid>https://velog.io/@lee_nah/Windows%EC%97%90%EC%84%9C-EKS-%EB%B6%99%EC%9D%B4%EA%B8%B0-Git-Bash-helm-kubeconfig-%EC%82%BD%EC%A7%88-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EA%B2%BD%EB%A1%9C%EC%9D%B8%EC%A6%9D%EA%B6%8C%ED%95%9C</guid>
            <pubDate>Tue, 05 May 2026 10:57:23 GMT</pubDate>
            <description><![CDATA[<p>Terraform apply와 GitHub Actions CI/CD까지 성공한 후, 이제 EKS 클러스터에 플랫폼 컴포넌트를 설치할 차례였다. 팀원이 만들어준 <code>bootstrap-platform.sh</code> 스크립트를 실행하면 ArgoCD, Karpenter, Istio 등을 한 번에 설치할 수 있다.</p>
<br>

<h1 id="powershell-및-vs-code에서-실행-불가">PowerShell 및 VS Code에서 실행 불가</h1>
<p>처음에는 VS Code 터미널에서 그냥 실행하려 했다.</p>
<pre><code class="language-bash">./scripts/bootstrap-platform.sh</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e0e129ec-b249-4da6-b86b-e73e3469897b/image.png" alt=""></p>
<p>당연히 실패했다. <code>.sh</code> 파일은 Linux/Mac 환경의 bash 스크립트이기 때문에, Windows의 PowerShell이나 VS Code 기본 터미널에서는 실행할 수 없다. bash를 실행할 수 있는 환경이 필요했다.</p>
<br>

<h1 id="git-bash-설치--helm-설치">Git Bash 설치 + helm 설치</h1>
<p><a href="https://git-scm.com/download/win">https://git-scm.com/download/win</a> 에서 Git을 설치하면 Git Bash가 함께 설치된다. Git Bash는 Windows에서 bash 명령어를 쓸 수 있게 해주는 터미널 환경이다.</p>
<p>설치 후 Git Bash를 열고 다시 시도했다.</p>
<pre><code class="language-bash">cd /c/pposiraegi-ecommerce
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm
./scripts/bootstrap-platform.sh</code></pre>
<p>이번엔 <code>helm</code>이 설치되어 있지 않다는 오류가 떴다.</p>
<pre><code class="language-bash">[ERROR] helm 미설치</code></pre>
<p>helm은 Kubernetes 패키지 매니저로, 차트(chart)라는 단위로 애플리케이션을 클러스터에 쉽게 배포할 수 있게 해준다. bootstrap 스크립트 내부에서 helm을 사용하기 때문에 반드시 설치가 필요했다.</p>
<p>Linux라면 <code>sudo</code> 명령어로 간단히 설치할 수 있지만, Git Bash 환경에서는 sudo 권한이 없어서 수동으로 다운받아 설치했다.</p>
<pre><code class="language-bash">curl -L https://get.helm.sh/helm-v3.16.0-windows-amd64.zip -o helm.zip
unzip helm.zip
mv windows-amd64/helm.exe ./helm.exe
export PATH=$PATH:/c/pposiraegi-ecommerce</code></pre>
<br>

<h1 id="eks-인증-문제">EKS 인증 문제</h1>
<p>helm까지 설치하고 다시 실행했더니 이번엔 EKS 인증 오류가 떴다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/bbba661c-aa3b-467a-8907-44d3f6e49ef7/image.png" alt=""></p>
<p><code>kubectl get nodes</code>도 동일한 오류가 발생했다. <code>aws eks get-token</code>은 정상적으로 토큰을 반환하는데, kubectl이 이를 제대로 활용하지 못하는 상황이었다.</p>
<p>kubeconfig를 확인해보니 <code>command: aws</code>로 설정되어 있었는데, Git Bash 환경에서 aws CLI의 경로를 제대로 찾지 못하는 것이 원인이었다.</p>
<blockquote>
<p>kubeconfig란? kubectl이 어떤 클러스터에 어떻게 접속할지 정의해둔 설정 파일이다. 보통 <code>~/.kube/config</code> 경로에 위치한다.</p>
</blockquote>
<br>

<h1 id="kubeconfig-초기화-및-경로-문제-해결">kubeconfig 초기화 및 경로 문제 해결</h1>
<p>전체 경로로 바꿔봤지만 경로에 공백이 포함되어 있어 또 실패했다. kubeconfig를 완전히 초기화하고 다시 시도했다.</p>
<pre><code class="language-bash">rm ~/.kube/config
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/1901e0ef-a642-446b-8de8-76c8288ad9ef/image.png" alt=""></p>
<p>여전히 안 됐다. kubeconfig 내용을 직접 확인해봤다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e771f43c-cd5c-4f65-ad73-bee888a936f4/image.png" alt=""></p>
<p><code>command: aws</code> 부분이 문제였다. aws CLI의 전체 경로로 바꿔야 하는데, Windows 특성상 경로에 공백이 포함되어 있어(<code>C:\Program Files\...</code>) 그대로 쓸 수가 없었다.</p>
<p>먼저 aws CLI의 실제 경로를 확인했다.</p>
<pre><code class="language-bash">which aws</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/1e582c44-39e6-4421-b7e1-7816f36ed032/image.png" alt=""></p>
<p>공백 문제를 우회하기 위해 심볼릭 링크를 만들기로 했다.</p>
<br>

<h1 id="심볼릭-링크-생성">심볼릭 링크 생성</h1>
<p>심볼릭 링크란 특정 파일이나 경로를 가리키는 바로가기 파일이다. 공백 없는 경로에 링크를 걸어두면 경로 문제를 우회할 수 있다.</p>
<pre><code class="language-bash">ln -s &quot;/c/Program Files/Amazon/AWSCLIV2/aws.exe&quot; ~/aws.exe
sed -i &#39;s|command: aws|command: /c/Users/gkthf/aws.exe|g&#39; ~/.kube/config
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/96960368-a431-44d0-88c9-c310cd7b3d36/image.png" alt=""></p>
<p>kubectl이 Windows 네이티브 바이너리라 Windows 경로 형식으로 바꿔서 다시 시도했다.</p>
<pre><code class="language-bash">sed -i &#39;s|command: /c/Users/gkthf/aws.exe|command: C:\\Program Files\\Amazon\\AWSCLIV2\\aws.exe|g&#39; ~/.kube/config
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/9a42c4ea-5342-4d04-b52b-bab60b81e2c8/image.png" alt=""></p>
<p>경로는 이제 제대로 찾는데, 이번엔 인증 문제가 또 떴다. kubeconfig 상태를 다시 확인했다.</p>
<pre><code class="language-bash">cat ~/.kube/config | grep &quot;command:&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/5b062948-42ac-4ec0-8f9b-6d1fc14c9570/image.png" alt=""></p>
<p>경로는 맞는데 AWS_PROFILE 환경변수가 제대로 전달되지 않는 것 같아서, kubeconfig에 profile 설정이 있는지 확인했다.</p>
<pre><code class="language-bash">cat ~/.kube/config | grep -A3 &quot;env:&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/71c05580-00f6-4507-b00f-184df7171c06/image.png" alt=""></p>
<p>profile은 존재했다. 그렇다면 AWS 자체 권한 문제일 수 있겠다 싶어 AWS 콘솔에 접속해서 확인해봤다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/306ad71a-dbae-4312-a62d-e2f3a7102bf4/image.png" alt=""></p>
<p>IAM 사용자 목록을 보니 <code>nahyung</code>과 <code>jihoon</code>은 있는데 <code>terraform-user</code>가 없었다. kubeconfig에는 <code>terraform-user</code> 프로파일로 인증하도록 설정되어 있었으니 당연히 권한 오류가 날 수밖에 없었다.</p>
<p><code>terraform-user</code>를 IAM에 생성한 뒤 다시 <code>kubectl get nodes</code>를 실행했다.</p>
<br>

<h1 id="권한-할당">권한 할당</h1>
<p>연결은 됐는데 이번엔 권한이 없다는 오류가 떴다. EKS는 IAM 사용자가 존재하더라도 클러스터 내부에 별도로 접근 권한을 부여해야 한다.</p>
<p>AWS 콘솔에서 다음 순서로 권한을 추가했다.</p>
<p><strong>EKS → 해당 클러스터 → Access entries → terraform-user 선택 → Add access policy → AmazonEKSClusterAdminPolicy 추가</strong></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/c8716bf1-b0e5-48e7-a390-ee578222fe07/image.png" alt=""></p>
<p>추가 완료 후 다시 실행했다.</p>
<pre><code class="language-bash">kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/7fd3a31d-6318-4b05-a84b-c02ec30d5df3/image.png" alt=""></p>
<p>노드 목록이 정상적으로 출력됐다.</p>
<br>

<h1 id="bootstrap-실행">bootstrap 실행</h1>
<p>경로 문제와 권한 문제가 모두 해결되어 드디어 bootstrap 스크립트를 실행할 수 있었다.</p>
<pre><code class="language-bash">export PATH=$PATH:/c/pposiraegi-ecommerce
helm version
./scripts/bootstrap-platform.sh</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/5f84aacf-f441-45c4-8883-0fe056d54e10/image.png" alt=""></p>
<p>bootstrap이 정상적으로 실행되며 ArgoCD, Karpenter, Istio 등 플랫폼 컴포넌트들이 클러스터에 설치되기 시작했다. 삽질이 길었지만 결국 해결됐다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD 삽질기2] GitHub Actions - backend쪽 고치기]]></title>
            <link>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B02-GitHub-Actions-backend%EC%AA%BD-%EA%B3%A0%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B02-GitHub-Actions-backend%EC%AA%BD-%EA%B3%A0%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 09:50:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lee_nah/post/d67f5871-6c9e-435a-98d1-0cc33881f11f/image.png" alt="">
저번 포스팅에서 프론트엔드 CI 쪽은 수정했지만, 백엔드 4개 서비스는 해결하지 못했다. 이번 포스팅에서는 백엔드 쪽 오류를 수정한 과정을 정리해보려 한다.</p>
<br>

<h1 id="production-not-found-에러">&quot;production&quot; not found 에러</h1>
<p>이 에러는 production 네임스페이스가 존재하지 않아 발생하는 오류다.
원인을 파악해보니, terraform apply만 완료된 상태에서 아직 부트스트랩을 실행하지 않았기 때문에 발생하는 문제로 보였다.
기존 워크플로우에는 ECR에 이미지가 푸시된 이후 kubectl rollout restart를 실행하는 단계가 있었는데, ArgoCD가 자동으로 Sync를 수행하므로 해당 스텝은 사실상 불필요했다. 따라서 deploy-all.yml에서 아래 구간을 제거했다.</p>
<pre><code class="language-yaml">- name: Update kubeconfig
        run: aws eks update-kubeconfig --region ${{ env.AWS_REGION }} --name ${{ env.EKS_CLUSTER }}

      - name: Rollout restart (ArgoCD sync fallback)
        run: |
          kubectl rollout restart deployment/${{ matrix.service }} -n production</code></pre>
<p>그런 다음 자동으로 Actions가 실행되기 때문에 기다려주었다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/52a93c72-fbfe-4b86-83d0-4898345a4ba0/image.png" alt=""></p>
<p>수정 후 GitHub Actions가 자동으로 트리거되었고, 결과는 성공이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD 삽질기1] GitHub Actions로 EKS 자동 배포 구축하며 만난 오류들]]></title>
            <link>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B01-GitHub-Actions</link>
            <guid>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B01-GitHub-Actions</guid>
            <pubDate>Sun, 03 May 2026 06:48:51 GMT</pubDate>
            <description><![CDATA[<h1 id="github-actions-조직-정책으로-서드파티-액션-차단">GitHub Actions 조직 정책으로 서드파티 액션 차단</h1>
<p>처음에 깃허브 argocd가 안되어서 왜 그런가 봤더니</p>
<p>조직 레벨에서 외부 액션 사용이 막혀있어서 pnpm/action-setup@v3 같은 액션을 못 쓰는 문제가 생겼다.
이거를 프로젝트 레포에서 보니 변경이 막혀있어서</p>
<p>조직 Settings에서 &quot;Allow all actions&quot; 으로 변경해서 해결하였다.</p>
<br>


<h1 id="terraformtfvars가-gitignore에-막혀서-변수-못-읽는-문제">terraform.tfvars가 .gitignore에 막혀서 변수 못 읽는 문제</h1>
<p>보안상 tfvars 파일은 git에 올리지 않는데, GitHub Actions는 로컬 파일을 못 읽는 문제도 발생했다. 이는 CI 실행 시 GitHub Secrets에서 값을 주입해서 tfvars를 동적으로 생성하는 방식으로 해결하였다.</p>
<br>

<h1 id="dockerfile-못찾는-오류">DockerFile 못찾는 오류</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/8f18acde-bd3d-4c2e-9c36-e5366f275be4/image.png" alt=""></p>
<p>이렇게 apply까진 했는데 오류가 떴다.
에러를 확인해보니 DockerFIle을 못찾는 오류이다.</p>
<p>워크플로우에서는 working-directory: ./backend 로 설정했는데 실제 Dockerfile 위치가 다른것이다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b63cf111-595f-4862-8ca7-782f896b85a2/image.png" alt=""></p>
<p>이렇게 백엔드 경로에 DockerFile이 없어서, </p>
<p>deploy-all.yml에서 </p>
<pre><code class="language-yaml">      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: pposiraegi-${{ matrix.service }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build --build-arg MODULE_NAME=${{ matrix.service }} -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest</code></pre>
<p>working-directory: ./backend 이 줄을 삭제해준다.</p>
<p>그리고나서 다시 Actions를 실행해본다.</p>
<br>

<h1 id="프론트엔드-오류">프론트엔드 오류</h1>
<h2 id="패키지-이름-오류">패키지 이름 오류</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/cb920342-7396-4c6e-b118-d7e28545a83a/image.png" alt="">
이번에는 프론트엔드쪽이 오류가 떴다.</p>
<p>프론트엔드 패키지 이름이 frontend가 아닌 것이다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b1285820-298f-4c27-a9be-154735907ba6/image.png" alt=""></p>
<p>Get-Content해서 보니 timedeal-front라고 뜬다.</p>
<p>따라서 deploy-all.yml에서 패키지이름을 timedeal-front로 바꿔준다
(json을 frontend로 바꾸는게 낫지 않나 싶었는데 다른곳에서 참조할 경우가 있기 때문에 json에 맞추어서 depoly-all 파일을 바꿔준다.)</p>
<pre><code class="language-yaml">##기존
      - name: Build Frontend
        run: pnpm turbo run build --filter=frontend
##바꾼 코드
      - name: Build Frontend
        run: pnpm turbo run build --filter=timedeal-front</code></pre>
<p>이렇게 코드를 바꿔준다. </p>
<br>

<h2 id="eslint-오류로-빌드-실패">ESLint 오류로 빌드 실패</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/cc26345a-83db-4d56-a607-4f5e037b642a/image.png" alt=""></p>
<p>이렇게 오류가 또 생겼는데 찾아보니, React useEffect missing dependencies, 미사용 변수 선언 등 ESLint 규칙 위반으로 빌드 실패한 것이었다.</p>
<p>프론트엔드도 내 담당이기 때문에 코드를 직접 수정해준다.</p>
<pre><code class="language-bash">Run pnpm turbo run build --filter=timedeal-front

Attention:
Turborepo now collects completely anonymous telemetry regarding usage.
This information is used to shape the Turborepo roadmap and prioritize features.
You can learn more, including how to opt-out if you&#39;d not like to participate in this anonymous program, by visiting the following URL:
https://turborepo.dev/docs/telemetry


   • Packages in scope: timedeal-front
   • Running build in 1 packages
   • Remote caching disabled

timedeal-front:build
cache miss, executing b851ac8de420a9b8

&gt; timedeal-front@0.1.0 build /home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend
&gt; react-scripts build

Creating an optimized production build...

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.

Failed to compile.

[eslint] 
src/api/auth.js
  Line 167:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/api/config.js
  Line 7:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/api/order.js
  Line 118:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/api/timedeal.js
  Line 182:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/pages/AddressManager.jsx
  Line 30:6:  React Hook useEffect has missing dependencies: &#39;navigate&#39; and &#39;user&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps

src/pages/AdminPage.jsx
  Line 57:6:  React Hook useEffect has a missing dependency: &#39;fetchDeals&#39;. Either include it or remove the dependency array  react-hooks/exhaustive-deps

src/pages/MyPage.jsx
  Line 24:6:    React Hook useEffect has missing dependencies: &#39;navigate&#39; and &#39;user&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps
  Line 227:28:  &#39;v&#39; is assigned to itself                                                                                                 no-self-assign

src/pages/OrderResult.jsx
  Line 26:6:  React Hook useEffect has missing dependencies: &#39;fetchOrder&#39; and &#39;order&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps

src/pages/TimeDealDetail.jsx
  Line 41:6:  React Hook useEffect has missing dependencies: &#39;fetchDeal&#39;, &#39;fetchRelatedDeals&#39;, and &#39;user&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps
  Line 47:6:  React Hook useEffect has a missing dependency: &#39;fetchDeal&#39;. Either include it or remove the dependency array                                     react-hooks/exhaustive-deps

src/pages/TimeDealList.jsx
  Line 6:26:   &#39;logout&#39; is defined but never used            no-unused-vars
  Line 12:16:  &#39;setUser&#39; is assigned a value but never used  no-unused-vars

src/pages/WishList.jsx
  Line 18:6:  React Hook useEffect has a missing dependency: &#39;navigate&#39;. Either include it or remove the dependency array  react-hooks/exhaustive-deps


 ELIFECYCLE  Command failed with exit code 1.
Error: timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)
 ERROR  timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)

 Tasks:    0 successful, 1 total
Cached:    0 cached, 1 total
  Time:    13.829s 
Failed:    timedeal-front#build

 ERROR  run failed: command  exited (1)
Error: Process completed with exit code 1.</code></pre>
<p>일단 오류 로그는 이렇게 떴다. </p>
<ul>
<li>frontend/src/api/auth.js</li>
<li>frontend/src/api/config.js</li>
<li>frontend/src/api/order.js</li>
<li>frontend/src/api/timedeal.js</li>
<li>frontend/src/pages/AddressManager.jsx</li>
<li>frontend/src/pages/AdminPage.jsx</li>
<li>frontend/src/pages/MyPage.jsx</li>
<li>frontend/src/pages/OrderResult.jsx</li>
<li>frontend/src/pages/TimeDealDetail.jsx</li>
<li>frontend/src/pages/TimeDealList.jsx</li>
<li>frontend/src/pages/WishList.jsx
이렇게, 
고쳐야할 파일이 11개나 되기 때문에 아찔했다.
그래도 임시방편으로 deploy-all에서 CI: false 하는것보단 근본적으로 고치는게 내 성격에 맞기 때문에 고쳐준다.</li>
</ul>
<p>일단 코드가 frontend/src/api/auth.js같은 경우에는</p>
<pre><code class="language-js">##중략
// 401 인터셉터: 토큰 만료 시 자동 로그아웃 + 로그인 페이지 이동
axios.interceptors.response.use(
  res =&gt; res,
  err =&gt; {
    if (err.response?.status === 401) {
      localStorage.removeItem(&#39;accessToken&#39;);
      localStorage.removeItem(&#39;refreshToken&#39;);
      localStorage.removeItem(&#39;user&#39;);
      sessionStorage.removeItem(&#39;user&#39;);
      // 현재 페이지가 /login이 아닐 때만 리다이렉트
      if (!window.location.pathname.startsWith(&#39;/login&#39;)) {
        window.location.href = &#39;/login?expired=1&#39;;
      }
    }
    return Promise.reject(err);
  }
);

export default { login, register, logout, getCurrentUser, saveAddress, getAddress };
</code></pre>
<p>이렇게 되어있는데 맨 마지막줄을</p>
<pre><code class="language-js">const authAPI = { login, register, logout, getCurrentUser, saveAddress, getAddress };
export default authAPI;</code></pre>
<p>이런식으로 고쳐준다. 
(거의 모든 파일이 저렇게 되어있어서 저런식으로 고쳐준다.)</p>
<p>또는 
useEffect에서 발생하는 오류가 있는데, 이는 dependency array 문제다.
React의 useEffect는 두 번째 인자로 dependency array를 받는데, useEffect 내부에서 사용하는 변수나 함수가 dependency array에 없으면 ESLint가 경고를 띄운다.</p>
<p>예를 들어 AddressManager.jsx의 경우</p>
<pre><code class="language-js">jsuseEffect(() =&gt; {
    if (!user) { navigate(&#39;/login&#39;); return; }
    ...
}, []); // ← navigate, user를 사용하는데 dependency array가 비어있음</code></pre>
<p>이런 경우 navigate와 user를 dependency array에 추가해줘야 한다.</p>
<pre><code class="language-js">jsuseEffect(() =&gt; {
    if (!user) { navigate(&#39;/login&#39;); return; }
    ...
}, [navigate, user]); // ← 추가</code></pre>
<p>단, TimeDealDetail.jsx처럼 fetchDeal, fetchRelatedDeals 같은 함수를 dependency에 넣으면 무한루프가 발생할 수 있다. 이런 경우 해당 함수를 useCallback으로 감싸서 함수 참조가 변경되지 않도록 처리해준다.</p>
<br>

<p>그리고나서 Actions가 잘 돌아가는지 확인한다.</p>
<br>

<br>

<h2 id="fetchdeals-fetchorder를-usecallback으로-감싸-무한루프-방지">fetchDeals, fetchOrder를 useCallback으로 감싸 무한루프 방지</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/7d8988f4-0332-4966-8f20-02a45b21add9/image.png" alt="">
Actions를 돌렸더니 이러한 오류가 떴다. </p>
<pre><code class="language-bash">Run pnpm turbo run build --filter=timedeal-front

Attention:
Turborepo now collects completely anonymous telemetry regarding usage.
This information is used to shape the Turborepo roadmap and prioritize features.
You can learn more, including how to opt-out if you&#39;d not like to participate in this anonymous program, by visiting the following URL:
https://turborepo.dev/docs/telemetry


   • Packages in scope: timedeal-front
   • Running build in 1 packages
   • Remote caching disabled

timedeal-front:build
cache miss, executing 381ed73731ab9079

&gt; timedeal-front@0.1.0 build /home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend
&gt; react-scripts build

Creating an optimized production build...

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.

Failed to compile.

[eslint] 
src/pages/AdminPage.jsx
  Line 57:7:  &#39;fetchDeals&#39; was used before it was defined                                                                                                                                             no-use-before-define
  Line 59:9:  The &#39;fetchDeals&#39; function makes the dependencies of useEffect Hook (at line 57) change on every render. To fix this, wrap the definition of &#39;fetchDeals&#39; in its own useCallback() Hook  react-hooks/exhaustive-deps

src/pages/OrderResult.jsx
  Line 26:6:  React Hook useEffect has a missing dependency: &#39;fetchOrder&#39;. Either include it or remove the dependency array  react-hooks/exhaustive-deps
Error: timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)
 ERROR  timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)


 ELIFECYCLE  Command failed with exit code 1.

 Tasks:    0 successful, 1 total
Cached:    0 cached, 1 total
  Time:    13.493s 
Failed:    timedeal-front#build

 ERROR  run failed: command  exited (1)
Error: Process completed with exit code 1.</code></pre>
<p>아까보다는 나아졌다.
이제는 AdminPage.jsx랑 OrderResult.jsx만 에러가 뜨고 있다.</p>
<p>AdminPage.jsx - import에 useCallback 추가하고 fetchDeals를 useCallback으로 감싸고 useEffect 위로 올려 수정해준다.</p>
<p>OrderResult.jsx - import에 useCallback 추가하고 fetchOrder를 useCallback으로 감싸 수정해준다.</p>
<p>그리고 다시 Actions를 실행해준다.</p>
<br>

<h2 id="fetchdeals-fetchorder-함수-누락으로-인한-not-defined-오류-수정">fetchDeals, fetchOrder 함수 누락으로 인한 not defined 오류 수정</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/3a868f00-36ad-42c2-9570-49252aab4a4a/image.png" alt=""></p>
<p>방금 고친 AdminPage랑 OrderResult에서 여러가지를 수정하며, 쓰지 않는 함수를 삭제했는데 잘못 삭제하여 함수를 찾지 못한다고 에러가 떠서 다시 추가해준다. </p>
<p>그리고 또 Actions를 기대해준다.</p>
<br>

<h2 id="프론트엔드-마침내-성공">프론트엔드 마침내 성공</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/ba67560e-54f4-4f7b-a3cc-e2fd9c043c62/image.png" alt=""></p>
<p>마침내 프론트엔드 빌드가 성공했다. 길고 길었던 ESLint 오류와의 싸움이 끝났다. </p>
<p>이제 4개의 백엔드와의 긴 싸움이 남았다.</p>
<br>




]]></description>
        </item>
        <item>
            <title><![CDATA[ArgoCD sync는 됐는데 Pod가 안 떠요 — 원인 추적기]]></title>
            <link>https://velog.io/@lee_nah/%EC%99%9C%EC%A3%BD%EB%8A%94%EC%A7%80-%EB%AA%A8%EB%A5%B4%EA%B2%A0%EB%8A%94-production-pod%EA%B3%A0%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/%EC%99%9C%EC%A3%BD%EB%8A%94%EC%A7%80-%EB%AA%A8%EB%A5%B4%EA%B2%A0%EB%8A%94-production-pod%EA%B3%A0%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Thu, 30 Apr 2026 06:28:06 GMT</pubDate>
            <description><![CDATA[<h1 id="crashloopbackoff-해결기--eks-pod-살리기-feat-argocd-재설치">CrashLoopBackOff 해결기 — EKS Pod 살리기 (feat. ArgoCD 재설치)</h1>
<p><a href="https://velog.io/@lee_nah/ArgoCD-Sync-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A0%88%ED%8F%AC-%EC%98%A4%ED%83%80%EB%B6%80%ED%84%B0-RDS-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9%EA%B9%8C%EC%A7%80">저번 포스팅</a>에서 ArgoCD 연결까지는 성공했는데, production Pod를 띄우는 과정에서 <code>CrashLoopBackOff</code> 오류가 발생했다.</p>
<p>이번 포스팅에서는 원인을 추적하고 해결하는 과정을 기록한다.</p>
<blockquote>
<p><strong>CrashLoopBackOff란?</strong><br>컨테이너가 시작됐다가 바로 죽는 걸 반복하는 상태다.<br>쿠버네티스가 &quot;계속 죽으니까 잠깐 기다렸다가 다시 시작할게&quot; 하면서 점점 재시작 간격을 늘린다.<br>보통 앱 설정 오류, DB 연결 실패, 환경변수 누락 등이 원인이다.</p>
</blockquote>
<hr>
<h2 id="1-eks-연결">1. EKS 연결</h2>
<p>먼저 로컬에서 EKS 클러스터에 접근할 수 있도록 kubeconfig를 설정한다.</p>
<pre><code class="language-bash"># kubeconfig 연결
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm

# 노드 상태 확인
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/4f64912b-9503-4533-8685-36b04d318f65/image.png" alt=""></p>
<p>노드가 <code>Ready</code> 상태로 뜨면 클러스터 연결은 정상이다.</p>
<blockquote>
<p><strong>노드(Node)란?</strong><br>쿠버네티스에서 실제로 컨테이너가 실행되는 서버(가상머신)다.<br>EKS에서는 EC2 인스턴스가 노드 역할을 한다.</p>
</blockquote>
<hr>
<h2 id="2-어떤-pod가-죽었는지-확인">2. 어떤 Pod가 죽었는지 확인</h2>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/288f7dc3-c5d5-49b1-9420-297400efd655/image.png" alt=""></p>
<p>아무것도 뜨지 않았다.</p>
<blockquote>
<p><strong><code>-n production</code>이란?</strong><br><code>-n</code>은 네임스페이스(namespace)를 지정하는 옵션이다.<br>쿠버네티스는 리소스를 네임스페이스라는 공간으로 분리해서 관리한다.<br><code>production</code>은 실제 서비스가 올라가는 공간이다.</p>
</blockquote>
<p>production 네임스페이스에 Pod가 하나도 없다는 건, <strong>ArgoCD가 아직 sync를 하지 않은 것</strong>이다.<br>즉, GitHub 레포에 있는 매니페스트 파일들이 클러스터에 아직 반영되지 않은 상태라는 뜻이다.</p>
<p>ArgoCD부터 다시 설치하고 연결해야 한다.</p>
<hr>
<h2 id="3-argocd-재설치">3. ArgoCD 재설치</h2>
<pre><code class="language-bash"># argocd 네임스페이스 생성
kubectl create namespace argocd

# 공식 매니페스트 적용
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/0afaadbd-cab8-4f1e-9315-702d3f885b50/image.png" alt=""></p>
<p><code>created</code> 메시지들이 쭉 뜨면 설치가 진행 중인 것이다.</p>
<p>설치 완료 후 Pod 상태를 확인한다:</p>
<pre><code class="language-bash">kubectl get pods -n argocd</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d01b43f8-f768-482b-b528-237f3b8dcb48/image.png" alt=""></p>
<p>모든 Pod의 STATUS가 <code>Running</code>으로 바뀌면 다음 단계로 넘어간다.<br>처음엔 <code>ContainerCreating</code>이나 <code>Pending</code>으로 뜨는 게 정상이니 잠깐 기다리면 된다.</p>
<hr>
<h2 id="4-argocd-ui-접속">4. ArgoCD UI 접속</h2>
<p>ArgoCD는 웹 UI를 제공한다. 포트 포워딩으로 로컬에서 접속한다.</p>
<pre><code class="language-bash">kubectl port-forward svc/argocd-server -n argocd 8080:443</code></pre>
<blockquote>
<p><strong>포트 포워딩이란?</strong><br>클러스터 내부 서비스를 외부에 노출하지 않고, 내 PC에서만 접근할 수 있도록 통로를 만드는 것이다.<br>위 명령어는 &quot;내 PC의 8080번 포트로 들어오는 요청을 ArgoCD 서버의 443 포트로 전달해줘&quot;라는 뜻이다.</p>
</blockquote>
<p>브라우저에서 <code>https://localhost:8080</code> 접속 후 로그인한다.</p>
<p>초기 비밀번호 확인 (PowerShell):</p>
<pre><code class="language-powershell">kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | ForEach-Object { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_)) }</code></pre>
<ul>
<li>아이디: <code>admin</code></li>
<li>비밀번호: 위 명령어 출력값</li>
</ul>
<blockquote>
<p>⚠️ 브라우저에서 &quot;안전하지 않은 연결&quot; 경고가 뜨면 <strong>고급 → 계속 진행</strong>을 누르면 된다.<br>자체 서명 인증서라서 뜨는 경고로, 로컬 개발 환경에서는 정상이다.</p>
</blockquote>
<hr>
<h2 id="5-namespace-및-secret-생성">5. namespace 및 Secret 생성</h2>
<p>ArgoCD가 배포할 <code>production</code> 네임스페이스와 DB/Redis 접속 정보를 미리 만들어줘야 한다.<br>이 작업을 먼저 해두지 않으면 Pod가 뜨다가 환경변수를 못 찾아서 바로 죽는다.</p>
<pre><code class="language-bash"># namespace, configmap 먼저 생성
kubectl apply -f kubernetes/base/namespace.yaml
kubectl apply -f kubernetes/base/configmap.yaml</code></pre>
<p>RDS, Redis 엔드포인트를 확인한다:</p>
<pre><code class="language-bash">terraform output rds_endpoint
terraform output elasticache_endpoint</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b9d9297d-043d-44f4-8a9e-f0362887a125/image.png" alt=""></p>
<p>확인한 엔드포인트로 Secret을 생성한다:</p>
<pre><code class="language-bash">kubectl create secret generic app-secret \
  --namespace=production \
  --from-literal=DB_HOST=RDS엔드포인트 \
  --from-literal=DB_USERNAME=pposiraegi \
  --from-literal=DB_PASSWORD=DB비밀번호 \
  --from-literal=REDIS_HOST=Redis엔드포인트 \
  --from-literal=JWT_SECRET=JWT시크릿</code></pre>
<blockquote>
<p><strong>Secret이란?</strong><br>비밀번호, API 키처럼 코드에 직접 넣으면 안 되는 민감한 값을 쿠버네티스 내부에서 안전하게 관리하는 리소스다.<br><code>--from-literal</code>로 값을 넣으면 자동으로 base64로 인코딩되어 저장된다.<br>Pod에서는 환경변수 형태로 이 값을 꺼내 쓸 수 있다.</p>
</blockquote>
<hr>
<h2 id="6-argocd-app-연결-및-sync">6. ArgoCD App 연결 및 Sync</h2>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml</code></pre>
<p>터미널에서 직접 sync한다:</p>
<pre><code class="language-bash"># argocd-server Pod 이름 먼저 확인
kubectl get pods -n argocd

# ArgoCD 서버에 로그인
kubectl -n argocd exec -it [argocd-server-Pod이름] -- \
  argocd login localhost:8080 --insecure --username admin --password [비밀번호]

# Sync 실행
kubectl -n argocd exec -it [argocd-server-Pod이름] -- \
  argocd app sync pposiraegi --insecure</code></pre>
<blockquote>
<p><strong>exec -it란?</strong><br>실행 중인 Pod 안으로 직접 들어가서 명령어를 실행하는 것이다.<br>마치 서버에 SSH로 접속해서 명령어를 치는 것과 같다.</p>
</blockquote>
<hr>
<h2 id="7-pod-상태-확인-및-로그-분석">7. Pod 상태 확인 및 로그 분석</h2>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p>Pod들이 뜨기 시작하면 각 Pod의 로그를 확인한다:</p>
<pre><code class="language-bash"># 현재 로그 확인
kubectl logs -n production [죽은Pod이름]

# 이미 죽은 컨테이너의 직전 로그 확인
kubectl logs -n production [죽은Pod이름] --previous</code></pre>
<p><code>order-service</code> Pod의 로그를 먼저 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/15c56d60-b3e4-42c7-a191-64df9bd89c69/image.png" alt=""></p>
<pre><code>Connect timed out</code></pre><p><strong>원인 분석:</strong></p>
<table>
<thead>
<tr>
<th>에러</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>Connect timed out</code></td>
<td>RDS(데이터베이스)에 접속 요청을 보냈는데 응답이 없음</td>
</tr>
<tr>
<td>원인</td>
<td>EKS 노드 → RDS 보안 그룹이 막혀있음</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>보안 그룹(Security Group)이란?</strong><br>AWS에서 서버 간 트래픽을 허용/차단하는 방화벽 역할을 한다.<br>RDS의 보안 그룹에 EKS 노드가 포함되어 있지 않으면 아무리 올바른 주소로 접속해도 연결이 차단된다.</p>
</blockquote>
<hr>
<h2 id="8-eks-노드-보안-그룹을-동적으로-추가">8. EKS 노드 보안 그룹을 동적으로 추가</h2>
<p>EKS는 배포할 때마다 보안 그룹 ID가 바뀐다.<br>그래서 ID를 하드코딩하지 않고, <strong>Terraform이 자동으로 가져오도록</strong> 동적으로 연결했다.</p>
<blockquote>
<p><strong>왜 동적으로 해야 할까?</strong><br><code>terraform apply</code>를 할 때마다 EKS가 새 보안 그룹을 만든다.<br>매번 수동으로 ID를 복사해서 넣으면 실수가 생기고 유지보수가 어렵다.<br>Terraform 모듈끼리 연결해두면 항상 최신 ID를 자동으로 참조한다.</p>
</blockquote>
<h3 id="1-modulessecurityvariablestf에-변수-추가">1) <code>modules/security/variables.tf</code>에 변수 추가</h3>
<pre><code class="language-hcl">variable &quot;eks_cluster_sg_id&quot; {
  description = &quot;EKS cluster security group ID&quot;
  default     = &quot;&quot;
}</code></pre>
<h3 id="2-modulessecuritymaintf-수정">2) <code>modules/security/main.tf</code> 수정</h3>
<p>RDS와 Redis 보안 그룹에 EKS 노드 접근 허용 규칙을 추가한다.</p>
<pre><code class="language-hcl"># RDS 보안 그룹 — PostgreSQL 5432 포트
ingress {
  from_port       = 5432
  to_port         = 5432
  protocol        = &quot;tcp&quot;
  security_groups = [var.eks_cluster_sg_id]
}

# Redis 보안 그룹 — Redis 6379 포트
ingress {
  from_port       = 6379
  to_port         = 6379
  protocol        = &quot;tcp&quot;
  security_groups = [var.eks_cluster_sg_id]
}</code></pre>
<h3 id="3-moduleseksoutputstf에-출력값-추가">3) <code>modules/eks/outputs.tf</code>에 출력값 추가</h3>
<p>EKS 모듈이 보안 그룹 ID를 외부로 내보낼 수 있도록 output을 추가한다.</p>
<pre><code class="language-hcl">output &quot;cluster_security_group_id&quot; {
  value = aws_eks_cluster.main.vpc_config[0].cluster_security_group_id
}</code></pre>
<h3 id="4-루트-maintf에서-모듈-간-연결">4) 루트 <code>main.tf</code>에서 모듈 간 연결</h3>
<pre><code class="language-hcl">module &quot;security&quot; {
  source = &quot;./modules/security&quot;

  project_name      = var.project_name
  vpc_id            = module.networking.vpc_id
  eks_cluster_sg_id = module.eks.cluster_security_group_id  # EKS에서 자동으로 가져옴
}</code></pre>
<p>이렇게 연결해두면 <code>terraform apply</code>를 할 때마다 EKS가 새로 만드는 보안 그룹 ID를 자동으로 참조한다.</p>
<p>수정 후 적용:</p>
<pre><code class="language-bash">terraform apply -var-file=&quot;terraform.tfvars&quot;</code></pre>
<hr>
<h2 id="9-terraform-apply-후-argocd-재sync">9. Terraform apply 후 ArgoCD 재sync</h2>
<p>apply 후 ArgoCD 상태를 확인했는데 뭔가 이상해 보였다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/f334aa05-572c-4390-a7b1-d1864ee77fe6/image.png" alt=""></p>
<p>혹시 Secret이나 엔드포인트가 잘못됐나 싶어서 다시 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/fb794773-7bbe-4047-a8f8-701b3756363e/image.png" alt=""></p>
<p>Secret과 엔드포인트는 정상적으로 등록된 것을 확인했다. 그래서 다시 sync를 시도했다.</p>
<pre><code class="language-bash"># ArgoCD 서버에 로그인
kubectl -n argocd exec -it argocd-server-78f5bb67d5-xt9g6 -- \
  argocd login localhost:8080 --insecure --username admin --password [패스워드]

# Sync 실행
kubectl -n argocd exec -it argocd-server-78f5bb67d5-xt9g6 -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p>각 명령어의 의미를 정리하면:</p>
<p><strong>첫 번째 명령어 — ArgoCD 로그인</strong></p>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>kubectl -n argocd exec -it [Pod이름]</code></td>
<td>argocd 네임스페이스의 argocd-server Pod 안으로 접속</td>
</tr>
<tr>
<td><code>argocd login localhost:8080</code></td>
<td>Pod 안에서 ArgoCD에 로그인</td>
</tr>
<tr>
<td><code>--insecure</code></td>
<td>HTTPS 인증서 검증 건너뛰기</td>
</tr>
<tr>
<td><code>--username admin --password [패스워드]</code></td>
<td>admin 계정으로 로그인</td>
</tr>
</tbody></table>
<p><strong>두 번째 명령어 — App Sync</strong></p>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>kubectl -n argocd exec -it [Pod이름]</code></td>
<td>argocd-server Pod 안으로 접속</td>
</tr>
<tr>
<td><code>argocd app sync pposiraegi</code></td>
<td>pposiraegi 앱을 GitHub 레포와 동기화</td>
</tr>
<tr>
<td><code>--insecure</code></td>
<td>HTTPS 인증서 검증 건너뛰기</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/8cd3cb2d-d65c-475d-a8c4-d2e56317921a/image.png" alt=""></p>
<hr>
<h2 id="10-그런데-또-문제가--kubeconfig와-rbac">10. 그런데 또 문제가 — kubeconfig와 RBAC</h2>
<p>여기까지 했는데도 production Pod가 여전히 뜨지 않았다.</p>
<p>팀원과 공유하는 과정에서 원인이 두 가지 더 있다는 걸 알게 됐다.</p>
<p><strong>원인 1 — kubeconfig가 예전 엔드포인트를 가리키고 있음</strong></p>
<blockquote>
<p><code>terraform apply</code>를 다시 하면 EKS 클러스터가 재생성되면서 API 서버 엔드포인트 주소가 바뀐다.<br>그런데 로컬의 kubeconfig는 예전 주소를 그대로 들고 있기 때문에, <code>kubectl</code> 명령어가 존재하지 않는 클러스터에 계속 요청을 보내고 있던 것이다.<br>해결 방법은 간단하다. <code>aws eks update-kubeconfig</code>를 다시 실행해서 최신 엔드포인트로 갱신해주면 된다.</p>
</blockquote>
<pre><code class="language-bash">aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm</code></pre>
<p><strong>원인 2 — RBAC 권한 문제</strong></p>
<blockquote>
<p><strong>RBAC(Role-Based Access Control)란?</strong><br>쿠버네티스에서 &quot;누가 무엇을 할 수 있는지&quot; 권한을 관리하는 시스템이다.<br>ArgoCD가 production 네임스페이스에 Pod를 배포하려면 그에 맞는 권한이 부여되어 있어야 한다.<br>권한이 없으면 sync는 성공해도 실제 리소스가 생성되지 않거나 오류가 발생한다.</p>
</blockquote>
<p>이 두 가지 문제가 복합적으로 작용해서 production Pod가 계속 뜨지 않았던 것이다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 포스팅에서 겪은 문제들을 정리하면:</p>
<table>
<thead>
<tr>
<th>순서</th>
<th>문제</th>
<th>원인</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>CrashLoopBackOff</code></td>
<td>EKS → RDS 보안 그룹 차단</td>
<td>Terraform으로 동적 보안 그룹 연결</td>
</tr>
<tr>
<td>2</td>
<td>kubectl 연결 안 됨</td>
<td>kubeconfig가 예전 엔드포인트를 가리킴</td>
<td><code>update-kubeconfig</code> 재실행</td>
</tr>
<tr>
<td>3</td>
<td>Pod 배포 안 됨</td>
<td>ArgoCD RBAC 권한 누락</td>
<td>다음 포스팅에서 해결 예정</td>
</tr>
</tbody></table>
<p>트러블슈팅을 하다 보면 문제 하나를 해결하면 또 다른 문제가 나오는 게 일상이다.<br>RBAC 권한 설정과 부트스트랩 스크립트 적용 과정은 다음 포스팅에서 이어서 정리할 예정이다. 😅</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ArgoCD Sync 트러블슈팅 — 레포 오타부터 RDS 보안 그룹까지]]></title>
            <link>https://velog.io/@lee_nah/ArgoCD-Sync-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A0%88%ED%8F%AC-%EC%98%A4%ED%83%80%EB%B6%80%ED%84%B0-RDS-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@lee_nah/ArgoCD-Sync-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A0%88%ED%8F%AC-%EC%98%A4%ED%83%80%EB%B6%80%ED%84%B0-RDS-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 30 Apr 2026 04:58:53 GMT</pubDate>
            <description><![CDATA[<h1 id="argocd-sync-트러블슈팅--레포-경로-오류부터-rds-보안-그룹까지">ArgoCD Sync 트러블슈팅 — 레포 경로 오류부터 RDS 보안 그룹까지</h1>
<p>이번 포스팅은 ArgoCD Sync를 시도하다가 연속으로 두 가지 문제를 맞닥뜨린 트러블슈팅 기록이다.<br>결론부터 말하면 <strong>레포 이름 오타 → 폴더 경로 오류 → RDS 보안 그룹 차단</strong> 순서로 문제가 터졌고, 아직 완전히 해결 전 단계다.</p>
<hr>
<h2 id="사전-준비--엔드포인트-확인-및-리소스-생성">사전 준비 — 엔드포인트 확인 및 리소스 생성</h2>
<p>Sync 전에 RDS, ElastiCache 엔드포인트를 확인하고 쿠버네티스 리소스를 미리 만들어둔다.</p>
<pre><code class="language-bash"># RDS, ElastiCache 엔드포인트 확인
terraform output rds_endpoint
terraform output elasticache_endpoint</code></pre>
<pre><code class="language-bash"># namespace, configmap 생성
kubectl apply -f kubernetes/base/namespace.yaml
kubectl apply -f kubernetes/base/configmap.yaml</code></pre>
<pre><code class="language-bash"># DB, Redis, JWT 정보를 Secret으로 등록
kubectl create secret generic app-secret \
  --namespace=production \
  --from-literal=DB_HOST=pposiraegi-db.c5wkmcaauwn2.ap-northeast-2.rds.amazonaws.com \
  --from-literal=DB_USERNAME=pposiraegi \
  --from-literal=DB_PASSWORD=DB비밀번호 \
  --from-literal=REDIS_HOST=pposiraegi-redis.qka9g8.0001.apn2.cache.amazonaws.com \
  --from-literal=JWT_SECRET=JWT시크릿</code></pre>
<blockquote>
<p><strong>Secret이란?</strong><br>비밀번호, API 키처럼 외부에 노출되면 안 되는 값을 쿠버네티스 안에서 안전하게 관리하는 리소스다.<br><code>--from-literal</code>로 값을 직접 넣으면 자동으로 base64 인코딩되어 저장된다.</p>
</blockquote>
<hr>
<h2 id="트러블슈팅-1--argocd-sync-실패-레포경로-문제">트러블슈팅 1 — ArgoCD Sync 실패 (레포/경로 문제)</h2>
<h3 id="sync-시도">Sync 시도</h3>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml

# UI에서 SYNC → SYNCHRONIZE 클릭 후 반응 없음
# 터미널로 직접 해결하기로 함

# ArgoCD 파드 확인
kubectl get pods -n argocd

# ArgoCD 서버에 직접 로그인
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd login localhost:8080 --insecure --username admin --password NDiUuc1t8XJKmF2O

# 수동 Sync
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p>UI에서 Sync 버튼을 눌렀는데 아무 반응이 없어서, ArgoCD 서버 파드에 직접 exec로 들어가서 CLI로 진행했다.</p>
<hr>
<h3 id="문제-1--feateks-migration-브랜치를-못-찾음">문제 1 — <code>feat/eks-migration</code> 브랜치를 못 찾음</h3>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/05cba9d5-1ad9-4c60-8ce5-3fbaf2bb7e45/image.png" alt=""></p>
<p>브랜치를 찾지 못한다는 에러가 발생했다. 하나씩 확인했다.</p>
<pre><code class="language-bash"># 원격 브랜치 목록 확인
git branch -r</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e416a982-25da-494a-a53f-cd78f8374654/image.png" alt=""></p>
<p>→ 브랜치는 실제로 존재함.</p>
<pre><code class="language-bash"># 해당 브랜치에 커밋이 있는지 확인
git log --oneline origin/feat/eks-migration</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/0cc7fa60-d5b4-4639-ab90-b053f845e787/image.png" alt=""></p>
<p>→ 커밋도 있음.</p>
<pre><code class="language-bash"># argocd-app.yaml 내용 확인
cat argocd-app.yaml</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/51d96ba5-531d-4b6e-b474-e07dcdf15b51/image.png" alt=""></p>
<p>→ path 설정은 이상 없음.</p>
<pre><code class="language-bash"># 실제 레포 이름 확인
git remote -v</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/81c98362-0171-4fb0-80c3-5de59213a653/image.png" alt=""></p>
<p><strong>원인 발견 — 레포 이름 오타</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>URL</th>
</tr>
</thead>
<tbody><tr>
<td>argocd-app.yaml에 입력된 값</td>
<td><code>https://github.com/Goorm4I/pposiraegi-ecommerce-msa</code> ❌</td>
</tr>
<tr>
<td>실제 레포 주소</td>
<td><code>https://github.com/Goorm4I/pposiraegi-ecommerce</code> ✅</td>
</tr>
</tbody></table>
<p><code>argocd-app.yaml</code>의 <code>repoURL</code>을 올바른 주소로 수정 후 재적용했다.</p>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<hr>
<h3 id="문제-2--app-path-does-not-exist">문제 2 — <code>app path does not exist</code></h3>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/25748db8-8b0d-491f-a216-f3095455f65d/image.png" alt=""></p>
<p>레포 이름은 고쳤는데 이번엔 경로 문제가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/af76d346-6011-43fd-8419-1286e7182649/image.png" alt=""></p>
<p><strong>원인</strong></p>
<p>ArgoCD는 레포 루트에서 <code>kubernetes/</code> 폴더를 찾는데,<br>실제 폴더 구조는 <code>infrastructure/kubernetes/</code> 였다.</p>
<pre><code>pposiraegi-ecommerce/
└── infrastructure/
    └── kubernetes/   ← 실제 위치</code></pre><p><code>argocd-app.yaml</code>의 <code>path</code>를 <code>infrastructure/kubernetes</code>로 수정 후 재적용했다.</p>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/503bb0e6-9847-4d7f-baf5-6638b81a178e/image.png" alt=""></p>
<p>→ 연결 성공 🎉</p>
<hr>
<h2 id="트러블슈팅-2--pod-crashloopbackoff-rds-보안-그룹-차단">트러블슈팅 2 — Pod CrashLoopBackOff (RDS 보안 그룹 차단)</h2>
<h3 id="다시-sync-후-pod-상태-확인">다시 Sync 후 Pod 상태 확인</h3>
<pre><code class="language-bash">kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/2aec0896-3995-491b-9253-5e4929dc4a1f/image.png" alt=""></p>
<p>→ <code>successfully synced</code></p>
<p>그런데 Pod 상태를 확인하니 문제가 있었다.</p>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e0337797-400c-4b66-a3a4-5fd930ccd030/image.png" alt=""></p>
<p>처음엔 Running처럼 보였는데...</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/af5fc35c-beb3-49ac-acf8-b1aa2c74e185/image.png" alt=""></p>
<p>→ <code>CrashLoopBackOff</code> 발생</p>
<blockquote>
<p><strong>CrashLoopBackOff란?</strong><br>컨테이너가 시작됐다가 바로 죽는 걸 반복하는 상태다.<br>보통 앱 내부 에러, 설정 오류, 외부 서비스 연결 실패 등이 원인이다.</p>
</blockquote>
<hr>
<h3 id="no-more-tasks-문제">no more tasks 문제</h3>
<p>Sync 중 <code>no more tasks</code> 에러도 함께 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/65c8155a-e914-4991-8cd0-6b8fe218ffd3/image.png" alt=""></p>
<p>ArgoCD가 <code>infrastructure/kubernetes/</code> 하위 폴더를 재귀적으로 탐색하지 못해서 production Pod를 찾지 못하는 문제였다.</p>
<p><strong>해결 — <code>directory.recurse: true</code> 추가</strong></p>
<p><code>argocd-app.yaml</code>에 재귀 탐색 옵션을 추가했다.</p>
<pre><code class="language-yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: pposiraegi
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Goorm4I/pposiraegi-ecommerce
    targetRevision: feat/eks-migration
    path: infrastructure/kubernetes
    directory:
      recurse: true   # 하위 폴더까지 재귀적으로 탐색
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true</code></pre>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/410c661b-2050-4b15-b617-2fe1089f5184/image.png" alt=""></p>
<p>Pod는 뜨기 시작했지만... CrashLoopBackOff는 계속됐다.</p>
<hr>
<h3 id="로그로-원인-파악">로그로 원인 파악</h3>
<pre><code class="language-bash">kubectl logs -n production order-service-88547fbf4-vcl4k</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/bb9b4c9e-1b30-4b50-a88f-6411ef00701f/image.png" alt=""></p>
<pre><code>Connect timed out</code></pre><p><strong>원인 — EKS 노드 → RDS 보안 그룹이 막혀있음</strong></p>
<p>기존 RDS 보안 그룹 인바운드 규칙이 <code>api_gateway_sg</code>, <code>internal_msa_sg</code>만 허용하고 있었는데, EKS 노드는 다른 보안 그룹을 사용하고 있어서 접근이 차단된 상태였다.</p>
<hr>
<h3 id="해결--eks-노드-보안-그룹-id-확인-후-rdsredis-보안-그룹에-추가">해결 — EKS 노드 보안 그룹 ID 확인 후 RDS/Redis 보안 그룹에 추가</h3>
<pre><code class="language-bash"># EKS 클러스터 보안 그룹 ID 확인
aws eks describe-cluster \
  --name pposiraegi-cluster \
  --query &quot;cluster.resourcesVpcConfig.clusterSecurityGroupId&quot; \
  --output text \
  --profile goorm</code></pre>
<p>→ <code>sg-0aa36452edaf69dd9</code> 확인</p>
<p><code>modules/security/main.tf</code>에서 <code>rds_sg</code>와 <code>redis_sg</code>에 EKS 노드 보안 그룹을 추가했다.</p>
<p><strong>rds_sg — PostgreSQL 5432 포트 허용 추가</strong></p>
<pre><code class="language-hcl">resource &quot;aws_security_group&quot; &quot;rds_sg&quot; {
  vpc_id      = var.vpc_id
  name        = &quot;${var.project_name}-rds-sg&quot;
  description = &quot;RDS PostgreSQL security group&quot;

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = &quot;tcp&quot;
    security_groups = [aws_security_group.api_gateway_sg.id, aws_security_group.internal_msa_sg.id]
  }

  # EKS 노드 접근 허용 추가
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = &quot;tcp&quot;
    security_groups = [&quot;sg-0aa36452edaf69dd9&quot;]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = &quot;-1&quot;
    cidr_blocks = [&quot;0.0.0.0/0&quot;]
  }

  tags = { Name = &quot;${var.project_name}-rds-sg&quot; }
}</code></pre>
<p><strong>redis_sg — Redis 6379 포트 허용 추가</strong></p>
<pre><code class="language-hcl"># EKS 노드 접근 허용 추가
ingress {
  from_port       = 6379
  to_port         = 6379
  protocol        = &quot;tcp&quot;
  security_groups = [&quot;sg-0aa36452edaf69dd9&quot;]
}</code></pre>
<pre><code class="language-bash"># Terraform으로 보안 그룹 변경 적용
terraform apply -var-file=&quot;terraform.tfvars&quot;</code></pre>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>순서</th>
<th>문제</th>
<th>원인</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>브랜치 못 찾음</td>
<td><code>repoURL</code> 레포 이름 오타</td>
<td>yaml 수정 후 재적용</td>
</tr>
<tr>
<td>2</td>
<td><code>app path does not exist</code></td>
<td><code>path</code>가 실제 폴더 위치와 다름</td>
<td><code>infrastructure/kubernetes</code>로 수정</td>
</tr>
<tr>
<td>3</td>
<td><code>no more tasks</code></td>
<td>하위 폴더 탐색 안 됨</td>
<td><code>directory.recurse: true</code> 추가</td>
</tr>
<tr>
<td>4</td>
<td><code>CrashLoopBackOff</code></td>
<td>EKS → RDS 보안 그룹 차단</td>
<td>Terraform으로 인바운드 규칙 추가</td>
</tr>
</tbody></table>
<p>Terraform 적용 후 Pod가 정상적으로 뜨는지는 다음 포스팅에서 이어서 정리할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EKS + ArgoCD로 자동 배포 파이프라인 만들기]]></title>
            <link>https://velog.io/@lee_nah/kubeconfig-%EC%97%B0%EA%B2%B0-%EB%B0%8F-ArgoCD-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@lee_nah/kubeconfig-%EC%97%B0%EA%B2%B0-%EB%B0%8F-ArgoCD-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Thu, 30 Apr 2026 04:52:58 GMT</pubDate>
            <description><![CDATA[<h1 id="eks에-argocd-설치하고-gitops-배포-환경-구축하기">EKS에 ArgoCD 설치하고 GitOps 배포 환경 구축하기</h1>
<p>이번 포스팅에서는 AWS EKS 클러스터에 ArgoCD를 설치하고, GitHub 레포지토리와 연동해서 자동 배포까지 설정하는 과정을 정리했다.</p>
<blockquote>
<p><strong>ArgoCD란?</strong><br>Kubernetes 환경에서 <strong>GitOps</strong> 방식의 배포를 도와주는 툴이다.<br>쉽게 말하면, GitHub에 코드를 올리면 ArgoCD가 자동으로 감지해서 클러스터에 배포해주는 자동화 배포 도구다.</p>
</blockquote>
<hr>
<h2 id="1-kubeconfig-연결">1. kubeconfig 연결</h2>
<p>먼저 로컬 환경에서 EKS 클러스터에 접근할 수 있도록 kubeconfig를 설정해야 한다.<br><code>kubectl</code> 명령어가 어느 클러스터를 바라볼지 알려주는 작업이라고 생각하면 된다.</p>
<pre><code class="language-bash"># kubeconfig 연결
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm

# 클러스터 연결 확인
kubectl get nodes</code></pre>
<ul>
<li><code>--region</code>: 클러스터가 위치한 AWS 리전</li>
<li><code>--name</code>: EKS 클러스터 이름</li>
<li><code>--profile</code>: AWS CLI에 등록된 프로파일 이름</li>
</ul>
<p><code>kubectl get nodes</code>로 노드 목록이 출력되면 연결 성공이다.</p>
<hr>
<h2 id="2-argocd-설치">2. ArgoCD 설치</h2>
<p>ArgoCD를 위한 네임스페이스를 먼저 만들고, 공식 매니페스트를 적용해서 설치한다.</p>
<pre><code class="language-bash"># ArgoCD 전용 네임스페이스 생성
kubectl create namespace argocd

# ArgoCD 공식 매니페스트 적용
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 설치 완료 확인 (모든 Pod가 Running 상태여야 함)
kubectl get pods -n argocd</code></pre>
<blockquote>
<p><strong>네임스페이스(namespace)란?</strong><br>쿠버네티스 안에서 리소스를 논리적으로 분리하는 공간이다.<br>ArgoCD 관련 리소스만 모아두기 위해 별도 네임스페이스를 만든다.</p>
</blockquote>
<p>설치 후 <code>kubectl get pods -n argocd</code>를 실행했을 때, 모든 Pod의 STATUS가 <code>Running</code>으로 바뀌면 설치 완료다. (처음엔 <code>ContainerCreating</code>으로 뜨는 게 정상이니 잠깐 기다리자.)</p>
<hr>
<h2 id="3-argocd-ui-접속">3. ArgoCD UI 접속</h2>
<p>ArgoCD는 웹 UI를 제공한다. 외부에 바로 노출하지 않고 <strong>포트 포워딩</strong>으로 로컬에서 접속할 수 있다.</p>
<pre><code class="language-bash"># 로컬 8080 포트 → ArgoCD 서버 443 포트로 포워딩
kubectl port-forward svc/argocd-server -n argocd 8080:443</code></pre>
<blockquote>
<p><strong>포트 포워딩이란?</strong><br>클러스터 내부 서비스를 외부로 노출하지 않고, 내 로컬 PC에서만 접근할 수 있도록 터널을 뚫어주는 것이다.</p>
</blockquote>
<p>이 명령어를 실행한 상태로 브라우저에서 <a href="https://localhost:8080">https://localhost:8080</a> 에 접속하면 ArgoCD 로그인 화면이 나온다.</p>
<blockquote>
<p>⚠️ 브라우저에서 &quot;안전하지 않은 연결&quot;이라고 경고가 뜰 수 있는데, 자체 서명 인증서라서 그렇다. 고급 → 계속 진행을 누르면 된다.</p>
</blockquote>
<hr>
<h2 id="4-초기-비밀번호-확인">4. 초기 비밀번호 확인</h2>
<p>초기 관리자 계정은 <code>admin</code>이고, 비밀번호는 쿠버네티스 시크릿에 저장되어 있다.</p>
<p><strong>Linux / macOS:</strong></p>
<pre><code class="language-bash">kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath=&quot;{.data.password}&quot; | base64 -d</code></pre>
<p><strong>Windows (PowerShell):</strong></p>
<pre><code class="language-powershell">kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | ForEach-Object { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_)) }</code></pre>
<p>출력된 값이 초기 비밀번호다. 로그인 후에는 보안을 위해 비밀번호를 변경하는 걸 추천한다.</p>
<hr>
<h2 id="5-argocd-app-설정-gitops-연동">5. ArgoCD App 설정 (GitOps 연동)</h2>
<p>이제 핵심이다. ArgoCD가 GitHub 레포지토리를 감시하고 자동으로 배포하도록 Application 리소스를 생성한다.</p>
<pre><code class="language-bash">kubectl apply -f - &lt;&lt;EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: pposiraegi
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Goorm4I/pposiraegi-ecommerce-msa  # 감시할 GitHub 레포
    targetRevision: feat/eks-migration                             # 대상 브랜치
    path: kubernetes                                               # 매니페스트 파일 경로
  destination:
    server: https://kubernetes.default.svc
    namespace: production                                          # 배포할 네임스페이스
  syncPolicy:
    automated:
      prune: true      # 레포에서 삭제된 리소스는 클러스터에서도 삭제
      selfHeal: true   # 클러스터가 레포와 달라지면 자동으로 복구
    syncOptions:
    - CreateNamespace=true  # production 네임스페이스 없으면 자동 생성
EOF</code></pre>
<p>yaml 파일로 따로 저장했다면 이렇게 적용할 수도 있다:</p>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml</code></pre>
<p><strong>각 설정의 의미:</strong></p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>repoURL</code></td>
<td>ArgoCD가 감시할 GitHub 레포 주소</td>
</tr>
<tr>
<td><code>targetRevision</code></td>
<td>배포 기준이 될 브랜치 또는 태그</td>
</tr>
<tr>
<td><code>path</code></td>
<td>해당 브랜치 내에서 K8s 매니페스트가 있는 폴더</td>
</tr>
<tr>
<td><code>prune: true</code></td>
<td>레포에서 파일 삭제 시 클러스터에서도 리소스 삭제</td>
</tr>
<tr>
<td><code>selfHeal: true</code></td>
<td>누군가 클러스터를 직접 수정해도 레포 기준으로 자동 복구</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-상태-확인">6. 상태 확인</h2>
<p>ArgoCD UI에서 App이 정상적으로 생성된 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/142cd02c-8494-4303-a90e-770bb29a80bf/image.png" alt="ArgoCD UI 화면"></p>
<p><code>Synced</code> + <code>Healthy</code> 상태가 뜨면 GitHub 레포와 클러스터가 정상적으로 연동된 것이다. 이제 해당 브랜치에 커밋을 푸시하면 ArgoCD가 자동으로 감지해서 클러스터에 반영해준다. </p>
<hr>
<h2 id="마무리">마무리</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>kubeconfig 연결</td>
<td>로컬에서 EKS 클러스터 접근 설정</td>
</tr>
<tr>
<td>ArgoCD 설치</td>
<td>argocd 네임스페이스에 공식 매니페스트 적용</td>
</tr>
<tr>
<td>UI 접속</td>
<td>포트 포워딩으로 localhost:8080 접속</td>
</tr>
<tr>
<td>App 생성</td>
<td>GitHub 레포와 클러스터 자동 동기화 설정</td>
</tr>
</tbody></table>
<p>GitOps를 도입하면 배포 이력이 GitHub 커밋 히스토리에 그대로 남고, 문제가 생겼을 때 revert만으로 롤백이 가능해서 운영이 훨씬 편해진다. 처음 설정이 좀 번거롭지만 한 번 해두면 그 이후엔 정말 편하다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Terraform state가 어디갔지? S3 backend 설정 삽질 해결기]]></title>
            <link>https://velog.io/@lee_nah/Terraform-state%EA%B0%80-%EC%96%B4%EB%94%94%EA%B0%94%EC%A7%80-S3-backend-%EC%84%A4%EC%A0%95-%EC%82%BD%EC%A7%88-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/Terraform-state%EA%B0%80-%EC%96%B4%EB%94%94%EA%B0%94%EC%A7%80-S3-backend-%EC%84%A4%EC%A0%95-%EC%82%BD%EC%A7%88-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Wed, 29 Apr 2026 06:20:24 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-발생">문제 발생</h1>
<p>팀원이 tfstate s3 버킷을 지정하면서 오류가 생겨서 이거때문에 terraform apply가 안되는 오류가 발생했다.</p>
<p>팀원이 보내준 사유는 다음과 같다</p>
<blockquote>
<p>현재 pposiraegi-ecommerce repo에서 Terraform/EKS 접근 문제가 있습니다.<br>
중요:
파일 수정하지 말고 먼저 진단만 해주세요.
terraform apply 금지.
terraform import 금지.
destructive command 금지.<br>
현재 확인된 상태:
AWS_PROFILE=goorm 계정: 779846782353
EKS cluster: pposiraegi-cluster, ACTIVE, version 1.32
NodeGroup: pposiraegi-node-group, ACTIVE, t3.medium 2대 healthy
Terraform backend:
bucket = pposiraegi-tf-state-779846782353
key    = ecommerce/terraform.tfstate
해당 backend key에 state object가 없는 것으로 보임
terraform plan 실행 시 기존 리소스를 인식하지 못하고 86 to add
따라서 현재 apply하면 중복 생성/충돌 위험
kubectl은 kubeconfig 생성은 되지만 Kubernetes API 접근 실패:&quot;the server has asked for the client to provide credentials&quot;
EKS authenticationMode는 CONFIG_MAP
현재 IAM user arn:aws:iam::779846782353:user/jihoon이 aws-auth에 매핑되지 않은 것으로 추정<br>
해야 할 일:
실제 Terraform state 위치를 찾기
backend.tf와 실제 state 위치 불일치 여부 확인
state 유실이면 import 대상 목록 정리만 하기
aws-auth 또는 EKS access 권한 복구 방안 제안
팀원이 공통으로 사용할 AWS_PROFILE/backend/tfvars 절차 문서화 제안</p>
</blockquote>
<br>

<h1 id="진단하기">진단하기</h1>
<p>이거에 대한 해결방법은 사실 아직까지 잘 모르겠다.
차근차근 진단해보기로 했다.</p>
<h2 id="1-terraform-state-위치-찾기">1. Terraform state 위치 찾기</h2>
<pre><code class="language-bash"># S3 버킷 목록 전체 확인
aws s3 ls --profile goorm

# 팀원이 말한 버킷 확인
aws s3 ls s3://pposiraegi-tf-state-779846782353 --profile goorm

# 버킷 안에 뭐가 있는지
aws s3 ls s3://pposiraegi-tf-state-779846782353 --recursive --profile goorm</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/f7225022-131f-499f-a1cf-793ba7413b90/image.png" alt=""></p>
<p>이렇게 가장 최근에 만든 프로필인 pposiraegi-tf-state-779846782353에는 아무것도 없고</p>
<p>pposiraegi-tfstate-779846782353에 infrastructure/terraform.tfstate 파일이 들어있는 것을 확인할 수 있다.</p>
<br>

<h2 id="2-원인-파악">2. 원인 파악</h2>
<p>S3 버킷이 두 개 존재하는 것을 확인했다.</p>
<table>
<thead>
<tr>
<th>버킷 이름</th>
<th>생성일</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td>pposiraegi-tf-state-779846782353</td>
<td>2026-04-29</td>
<td>비어있음 ❌</td>
</tr>
<tr>
<td>pposiraegi-tfstate-779846782353</td>
<td>2026-04-17</td>
<td>state 파일 존재 ✅</td>
</tr>
</tbody></table>
<p>팀원이 설정한 backend 버킷 이름은 <code>pposiraegi-tf-state-779846782353</code>이었는데,
실제 state 파일은 <code>pposiraegi-tfstate-779846782353</code>에 있었다.
버킷 이름이 미묘하게 달랐던 것이 원인이었다.</p>
<br>

<p>팀원 backend 설정:
bucket = &quot;pposiraegi-tf-state-779846782353&quot;   ← 없는 버킷 (tf-state)
key    = &quot;ecommerce/terraform.tfstate&quot;         ← 경로도 다름
실제 state 위치:
bucket = &quot;pposiraegi-tfstate-779846782353&quot;    ← 진짜 버킷 (tfstate)
key    = &quot;infrastructure/terraform.tfstate&quot;   ← 실제 경로</p>
<br>

<h2 id="3-backendtf-파일-없는-것-확인">3. backend.tf 파일 없는 것 확인</h2>
<p>팀원 backend 설정:
bucket = &quot;pposiraegi-tf-state-779846782353&quot;   ← 없는 버킷 (tf-state)
key    = &quot;ecommerce/terraform.tfstate&quot;         ← 경로도 다름
실제 state 위치:
bucket = &quot;pposiraegi-tfstate-779846782353&quot;    ← 진짜 버킷 (tfstate)
key    = &quot;infrastructure/terraform.tfstate&quot;   ← 실제 경로</p>
<p>따라서, 중복생성과 충돌이 발생할 위험이 있었다.</p>
<br>

<h2 id="4-해결-방법-backendtf-생성">4. 해결 방법: backend.tf 생성</h2>
<p><code>infrastructure/backend.tf</code> 파일을 새로 만들어서 S3 backend를 명시적으로 설정했다.</p>
<pre><code class="language-bash">terraform {
  backend &quot;s3&quot; {
    bucket  = &quot;pposiraegi-tfstate-779846782353&quot;
    key     = &quot;infrastructure/terraform.tfstate&quot;
    region  = &quot;ap-northeast-2&quot;
    profile = &quot;goorm&quot;
  }
}</code></pre>
<p>이렇게 하면 뭐가 좋아지냐면:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>backend.tf 없을 때</th>
<th>backend.tf 있을 때</th>
</tr>
</thead>
<tbody><tr>
<td>state 저장 위치</td>
<td>로컬 파일</td>
<td>S3 버킷</td>
</tr>
<tr>
<td>팀원 공유</td>
<td>불가능</td>
<td>가능 ✅</td>
</tr>
<tr>
<td>충돌 위험</td>
<td>높음</td>
<td>낮음 ✅</td>
</tr>
<tr>
<td>분실 위험</td>
<td>높음 (로컬 삭제 시)</td>
<td>낮음 ✅</td>
</tr>
</tbody></table>
<h2 id="5-terraform-init-재실행">5. terraform init 재실행</h2>
<p>backend.tf 생성 후 terraform init을 다시 실행해서 S3 backend를 인식시켜줬다.</p>
<pre><code class="language-bash">terraform init -reconfigure</code></pre>
<p><code>-reconfigure</code> 옵션은 기존 backend 설정을 무시하고 새로운 backend로 다시 초기화하는 옵션이다.</p>
<p>이후 terraform plan을 실행하면 기존 리소스를 정상적으로 인식하고
<code>No changes</code> 또는 실제 변경사항만 표시되어야 한다.</p>
<br>

<h2 id="정리">정리</h2>
<p>이번 문제의 핵심은 두 가지였다.</p>
<ol>
<li><strong>버킷 이름 불일치</strong>: 팀원이 설정한 버킷 이름과 실제 state가 있는 버킷 이름이 달랐다.</li>
<li><strong>backend.tf 파일 부재</strong>: 팀 전체가 공유해야 하는 backend 설정이 코드로 관리되지 않고 있었다.</li>
</ol>
<p>앞으로는 backend.tf를 Git에 포함해서 팀원 모두가 동일한 S3 backend를 바라보도록 관리하도록 변경하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[구름 서포터즈] bastion 서버, 진짜 필요한가? SSM으로 갈아탄 이유]]></title>
            <link>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-bastion-%EC%84%9C%EB%B2%84-%EC%A7%84%EC%A7%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80-SSM%EC%9C%BC%EB%A1%9C-%EA%B0%88%EC%95%84%ED%83%84-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-bastion-%EC%84%9C%EB%B2%84-%EC%A7%84%EC%A7%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80-SSM%EC%9C%BC%EB%A1%9C-%EA%B0%88%EC%95%84%ED%83%84-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Mon, 30 Mar 2026 02:59:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 콘텐츠는 구름 서포터즈 활동으로 지원을 받아 작성된 교육생의 실제 경험 후기입니다.</p>
</blockquote>
<br>

<p><a href="https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-%EB%93%9C%EB%94%94%EC%96%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4">이전 게시물</a>에서 프로젝트를 시작했을 때 포스팅을 남겨놓았다.
그때는 기획 수준이었는데, 지금은 그때와는 다른 고민들을 하기 시작했다.
CI/CD 파이프라인 구성이라든지, 자동 배포 전략은 어떻게 할 것인지 등등.
아직은 반 수동 배포인 상황이지만, 앱이 잘 돌아가는 상태로는 만들었다.
일단 이전 게시물과 가장 크게 달라진 점은 인프라 구성도인 것 같다.</p>
<br>

<h1 id="기존과-가장-달라진-점">기존과 가장 달라진 점</h1>
<p>기존엔 bastion 서버를 두어서 private 서브넷에 있는 자원들에 접근하게 했는데,
지금은 SSM 서비스를 이용해 콘솔에 로그인하면 바로 EC2에 접근할 수 있도록 변경했다.
(현재 이 아키텍처도 수정해야 할 부분이 많다는 건 인지하고 있다. 백엔드 EC2를 private에 두어야 한다는 점 등.)
이 아래는 프로젝트 발표 때 사용했던 PPT다.
우리가 고려한 백엔드 엣지케이스라든가, 바뀐 인프라 아키텍처를 확인해볼 수 있다.</p>
<br>

<h1 id="백엔드-엣지케이스-고려-및-바뀐-인프라-전략">백엔드 엣지케이스 고려 및 바뀐 인프라 전략</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/79e750c7-4c2e-4791-9966-40926ea7147f/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/ecb320c2-3b2f-44ca-b96f-38d7079e2fbc/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/c65376ad-df24-4fe7-86ab-197aff42f2cd/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/271c0fee-bcb6-4994-942c-b964049682c5/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/eda7b3cc-9ebc-4dff-a4e5-d7e73dc6b1b9/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/e7631b09-9e1c-4e86-9c6f-a83121f74ab9/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/e3df713c-ffab-4707-8239-435fd163e47e/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/f4cb79dc-afcb-4ce5-9c7b-2bebf52be2e6/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/df9505c7-8109-4869-8e36-1a22c1ba1e96/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/c53d7642-6568-4fd0-af32-a9640c407811/image.png" alt=""></p>
<br>

<h1 id="bastion에서-ssm으로-전환한-이유">bastion에서 SSM으로 전환한 이유</h1>
<p>프로젝트 발표 중에 가장 첫번째로 받은 질문이 왜 bastion을 두지 않았느냐 인 것 같다.
ssm으로 전환 시 가장 팀원들이랑 많이 얘기했던 부분들이기도 하고. 
왜 bastion에서 ssm으로 전환하였냐면 다음과 같은 이유들 때문이다.</p>
<ul>
<li>보안 측면에서 더 유리하다. bastion을 운영하려면 SSH 포트(22번)를 외부에 열어둬야 하는데, 이 자체가 공격 표면이 된다. SSM은 인바운드 포트를 아예 열 필요가 없고, IAM 기반으로 접근을 제어하기 때문에 보안상 더 낫다고 판단했다.</li>
<li>bastion 서버 자체의 유지비용과 관리 포인트가 줄어든다. bastion도 결국 EC2 인스턴스라 비용이 나가고, 패치나 키 관리 같은 운영 부담도 생긴다. SSM으로 전환하면 그 오버헤드를 없앨 수 있다.</li>
<li>접근 이력 추적이 편하다. SSM Session Manager는 세션 로그를 CloudWatch나 S3에 자동으로 남길 수 있어서, 누가 언제 어떤 인스턴스에 접근했는지 감사(audit) 추적이 훨씬 수월하다. bastion 방식에서는 이걸 별도로 구성해야 한다.</li>
</ul>
<p>다만 bastion을 완전히 버려야 하나에 대해선 아직도 고민 중이다. 지금은 서비스 규모가 작아서 SSM으로도 충분하지만, EC2 수가 늘어나거나 현업 환경처럼 여러 인스턴스를 빠르게 오가며 관리해야 하는 상황이라면 SSH로 직접 붙는 게 더 편할 수 있기 때문이다.</p>
<br>




]]></description>
        </item>
    </channel>
</rss>