<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hyeongjun-hub.log</title>
        <link>https://velog.io/</link>
        <description>DevOps Engineer</description>
        <lastBuildDate>Wed, 11 Feb 2026 04:47:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hyeongjun-hub.log</title>
            <url>https://velog.velcdn.com/images/hyeongjun-hub/profile/b7eb9ef1-e082-4b44-aa7e-974ad09c99c3/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hyeongjun-hub.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hyeongjun-hub" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[on-prem + GCP 하이브리드 클러스터 구축기 [4] - VPN HA 구성]]></title>
            <link>https://velog.io/@hyeongjun-hub/on-prem-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-4-VPN-HA-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@hyeongjun-hub/on-prem-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-4-VPN-HA-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Wed, 11 Feb 2026 04:47:03 GMT</pubDate>
            <description><![CDATA[<h2 id="0-이전-글-요약">0. 이전 글 요약</h2>
<blockquote>
<p>운영 환경에서 VPN 게이트웨이가 단일 장애 지점(SPOF)이 되면 어떻게 될까?</p>
</blockquote>
<p><a href="https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-3">이전 글</a>에선 PoC 과정에서의 세 가지 어려웠던 점을 어떻게 풀어나갔는지에 대해 설명했습니다.</p>
<ol>
<li><strong>Cluster Autoscaler 전용 노드 필요성</strong>: GCP Metadata API 접근을 위해 클라우드 내 전용 CPU 노드 배치</li>
<li><strong>GPU 노드 중복 스케일업 방지</strong>: Startup Taint + 커스텀 Taint Remover DaemonSet으로 드라이버 초기화 시간 동안의 중복 생성 차단</li>
<li><strong>MIG kube-env 동기화</strong>: 템플릿 노드 시뮬레이션의 기준이 되는 kube-env를 실제 노드 설정과 100% 일치시켜 스케일링 안정성 확보</li>
</ol>
<p>이 글에선 <strong>&quot;실제 운영에 필요한 HA 구성을 어떻게 했는가&quot;</strong>에 대해 다룰게요.</p>
<hr>
<h2 id="1-vpn-ha-왜-필요한가">1. VPN HA, 왜 필요한가?</h2>
<p>하이브리드 클러스터 [2]에서 구축한 strongSwan 기반 Site-to-Site VPN은 하나의 게이트웨이 노드로 운영되고 있었어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/50a99d51-d0c8-466e-b705-7701d3391d5d/image.png" alt="단일 VPN 게이트웨이 구조"></p>
<p>이 구조에는 치명적인 문제가 있었습니다.</p>
<table>
<thead>
<tr>
<th><strong>장애 시나리오</strong></th>
<th><strong>영향</strong></th>
</tr>
</thead>
<tbody><tr>
<td>게이트웨이 노드 다운</td>
<td>전체 하이브리드 통신 중단</td>
</tr>
<tr>
<td>VPN 프로세스 장애</td>
<td>Cloud GPU 노드 접근 불가</td>
</tr>
<tr>
<td>네트워크 인터페이스 장애</td>
<td>서비스 연속성 상실</td>
</tr>
</tbody></table>
<p>특히 <strong>Cloud GPU가 실제 워크로드를 처리하는 중</strong>이라면, VPN 게이트웨이 장애는 곧 서비스 장애를 의미해요. GPU 노드에서 학습이나 추론이 진행되고 있는데 VPN이 끊기면, 그 작업은 그대로 중단되는 거예요.</p>
<h3 id="11-운영-환경에서-요구되는-안정성-수준">1.1 운영 환경에서 요구되는 안정성 수준</h3>
<p>이를 해결하기 위해 아래와 같은 요구사항을 정의했습니다.</p>
<table>
<thead>
<tr>
<th><strong>요구사항</strong></th>
<th><strong>목표</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>자동 장애 복구</strong></td>
<td>수동 개입 없이 자동 Failover</td>
</tr>
<tr>
<td><strong>복구 시간</strong></td>
<td>10초 이내 통신 재개</td>
</tr>
<tr>
<td><strong>투명한 복구</strong></td>
<td>애플리케이션 레벨에서 인지 불가</td>
</tr>
<tr>
<td><strong>운영 단순성</strong></td>
<td>복잡한 관리 컴포넌트 최소화</td>
</tr>
</tbody></table>
<blockquote>
<p>핵심은 &quot;장애가 발생해도 사람이 개입하지 않아도 되는 구조&quot;를 만드는 것이었어요.</p>
</blockquote>
<hr>
<h2 id="2-ha-구성-방안-두-가지-후보">2. HA 구성 방안: 두 가지 후보</h2>
<p>VPN HA를 구성하는 방법은 여러 가지가 있지만, 우리 환경에 적용 가능한 두 가지 방안을 후보로 선정했습니다.</p>
<h3 id="21-방안-a-bird--bgp-기반-active-active">2.1 방안 A: BIRD + BGP 기반 (Active-Active)</h3>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/528253fe-3153-4161-965f-2492c4c463f5/image.png" alt="HA VPN + BGP 구성도"></p>
<p>첫 번째 방안은 <strong>GCP HA VPN + Cloud Router + BIRD</strong>를 조합하는 거예요.</p>
<p><strong>핵심 구성 요소:</strong></p>
<ul>
<li><strong>GCP HA VPN</strong>: 이중 터널을 자동으로 제공하며 99.99% SLA를 보장합니다</li>
<li><strong>Cloud Router</strong>: BGP 기반 동적 라우팅을 처리합니다</li>
<li><strong>BIRD</strong>: On-prem 측에서 BGP 세션을 관리하는 데몬입니다 (ASN: 65001)</li>
<li><strong>xfrm 인터페이스</strong>: 라우트 기반 VPN을 위한 가상 인터페이스예요</li>
</ul>
<p>두 개의 게이트웨이가 동시에 트래픽을 처리하는 <strong>Active-Active</strong> 구조로, BGP를 통해 동적으로 라우팅 경로를 관리해요.</p>
<h3 id="22-방안-b-keepalived--vrrp-기반-active-standby">2.2 방안 B: Keepalived + VRRP 기반 (Active-Standby)</h3>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/4b2686f0-2c4e-40ba-870c-0b32c860afa7/image.png" alt="기본 VPN + VRRP 구성도"></p>
<p>두 번째 방안은 <strong>GCP Classic VPN + Keepalived + strongSwan HA Plugin</strong>을 조합하는 거예요.</p>
<p><strong>핵심 구성 요소:</strong></p>
<ul>
<li><strong>GCP Classic VPN</strong>: 정적 라우팅 기반으로 99.9% SLA를 보장해요</li>
<li><strong>Keepalived</strong>: VRRP 프로토콜로 VIP(Virtual IP)를 관리해요</li>
<li><strong>strongSwan HA Plugin</strong>: IKE/CHILD SA를 실시간으로 복제해요</li>
<li><strong>내부망 VIP</strong>: <code>192.168.1.47</code> (워커 노드 게이트웨이)</li>
<li><strong>외부망 VIP</strong>: <code>203.251.205.87</code> (GCP VPN 피어 주소)</li>
</ul>
<p>하나의 게이트웨이가 트래픽을 처리하고, 장애 시 대기 노드가 즉시 승격하는 <strong>Active-Standby</strong> 구조예요.</p>
<hr>
<h2 id="3-두-방안의-상세-비교">3. 두 방안의 상세 비교</h2>
<h3 id="31-기술적-비교">3.1 기술적 비교</h3>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>방안 A: BIRD + BGP</strong></th>
<th><strong>방안 B: Keepalived + VRRP</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>프로토콜</strong></td>
<td>BGP (Border Gateway Protocol)</td>
<td>VRRP (Virtual Router Redundancy Protocol)</td>
</tr>
<tr>
<td><strong>GCP VPN</strong></td>
<td>HA VPN (이중 터널)</td>
<td>Classic VPN (단일 터널)</td>
</tr>
<tr>
<td><strong>HA 컴포넌트</strong></td>
<td>Cloud Router + BIRD</td>
<td>Keepalived + strongSwan HA Plugin</td>
</tr>
<tr>
<td><strong>구조</strong></td>
<td>Active-Active</td>
<td>Active-Standby</td>
</tr>
<tr>
<td><strong>라우팅 방식</strong></td>
<td>동적 (BGP Advertisement)</td>
<td>정적 (수동 설정)</td>
</tr>
<tr>
<td><strong>VIP 필요</strong></td>
<td>❌ 불필요</td>
<td>✅ 필요 (내부망 1개 + 외부망 1개)</td>
</tr>
<tr>
<td><strong>위치 제약</strong></td>
<td>❌ 없음</td>
<td>✅ 같은 랙/L2 세그먼트 필요</td>
</tr>
</tbody></table>
<p>언뜻 보면 방안 A가 모든 면에서 우월해 보입니다. 하지만 &quot;기술적으로 우월한 것&quot;과 &quot;운영에 적합한 것&quot;은 다른 문제예요.</p>
<h3 id="32-운영-복잡도-비교">3.2 운영 복잡도 비교</h3>
<p><strong>방안 A (BIRD)의 복잡도</strong></p>
<p>방안 A는 관리해야 할 컴포넌트가 많아요.</p>
<ol>
<li><strong>GCP Cloud Router</strong>: BGP 세션 관리, prefix advertisement</li>
<li><strong>GCP HA VPN</strong>: 이중 터널 상태 모니터링</li>
<li><strong>BIRD 데몬</strong>: BGP 설정, 라우팅 정책</li>
<li><strong>xfrm 인터페이스</strong>: 재부팅 시 자동 복구 스크립트</li>
<li><strong>워커 노드 라우팅</strong>: ECMP 기반 경로 관리</li>
</ol>
<p>장애가 발생하면 디버깅도 쉽지 않아요.</p>
<pre><code>Cloud Router 장애 → BGP 세션 단절 → 라우팅 재수렴
BIRD 프로세스 다운 → 로컬 BGP 세션 끊김
xfrm 인터페이스 누락 → 터널 통신 불가</code></pre><p>무엇보다 <strong>BGP 프로토콜에 대한 이해, BIRD 설정 문법, 라우팅 테이블 디버깅 능력</strong>이 모두 필요해요. 러닝 커브가 상당했어요.</p>
<p><strong>방안 B (Keepalived)의 단순성</strong></p>
<p>반면 방안 B는 관리 포인트가 세 가지로 줄어듭니다.</p>
<ol>
<li><strong>Keepalived</strong>: VRRP hello 패킷 (1초 간격)</li>
<li><strong>strongSwan + HA Plugin</strong>: SA 복제</li>
<li><strong>Health Check 스크립트</strong>: VPN 상태 감지</li>
</ol>
<p>장애 시 동작 흐름도 단순해요.</p>
<pre><code>MASTER 노드 다운 → VRRP 감지 → VIP 이동 → Failover 완료</code></pre><p>가장 큰 장점은 <strong>우리 팀이 이미 프록시 서버에서 Keepalived를 운영 중</strong>이라는 거예요. Health Check 스크립트만 커스터마이징하면 됐어요.</p>
<h3 id="33-성능-및-안정성-비교">3.3 성능 및 안정성 비교</h3>
<table>
<thead>
<tr>
<th><strong>측정 항목</strong></th>
<th><strong>BIRD + BGP</strong></th>
<th><strong>Keepalived + VRRP</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Failover 시간</strong></td>
<td>15초</td>
<td><strong>5초</strong></td>
</tr>
<tr>
<td><strong>평균 대역폭</strong></td>
<td>800 Mbps</td>
<td>790 Mbps</td>
</tr>
<tr>
<td><strong>장애 감지 방법</strong></td>
<td>BGP keepalive + VPN DPD</td>
<td>VRRP hello (1초) + Health Check</td>
</tr>
<tr>
<td><strong>무중단 Failover</strong></td>
<td>✅ 가능 (BGP 재수렴)</td>
<td>⚠️ VIP 이동 중 순간 단절</td>
</tr>
<tr>
<td><strong>대역폭 확장</strong></td>
<td>❌ 불가 (인프라 제약)</td>
<td>❌ 불가 (Active-Standby)</td>
</tr>
</tbody></table>
<p>Active-Active 구조인 방안 A가 무중단 Failover에서는 유리하지만, Failover 시간 자체는 오히려 방안 B가 <strong>3배 빠른 5초</strong>를 기록했어요. BGP 재수렴에 시간이 걸리기 때문이에요.</p>
<p>대역폭은 두 방안 모두 약 800Mbps로 거의 동일했어요. Active-Active의 대역폭 이점은 우리 인프라 환경에서는 제한적이었어요.</p>
<h3 id="34-비용-비교">3.4 비용 비교</h3>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>HA VPN (방안 A)</strong></th>
<th><strong>Classic VPN (방안 B)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>VPN 게이트웨이</strong></td>
<td>$0.075/시간 × 2터널 = <strong>$108/월</strong></td>
<td>$0.075/시간 = <strong>$54/월</strong></td>
</tr>
<tr>
<td><strong>Cloud Router</strong></td>
<td>필요 (약 $10/월)</td>
<td>❌ 불필요</td>
</tr>
<tr>
<td><strong>데이터 전송</strong></td>
<td>$0.19/GB</td>
<td>$0.19/GB</td>
</tr>
<tr>
<td><strong>SLA</strong></td>
<td>99.99% (연간 52분 down)</td>
<td>99.9% (연간 9시간 down)</td>
</tr>
</tbody></table>
<p>방안 B는 방안 A 대비 <strong>월 $64, 연간 $768을 절감</strong>할 수 있었습니다.</p>
<h3 id="35-cloud-bursting-환경-특성-고려">3.5 Cloud Bursting 환경 특성 고려</h3>
<p>여기서 한 가지 더 고려한 점이 있습니다. 우리의 하이브리드 클러스터는 <strong>&quot;필요할 때만 Cloud GPU를 사용&quot;</strong>하는 Cloud Bursting 구조라는 점이에요.</p>
<table>
<thead>
<tr>
<th><strong>특성</strong></th>
<th><strong>영향</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>간헐적 트래픽</strong></td>
<td>24시간 고트래픽 환경이 아님</td>
</tr>
<tr>
<td><strong>워크로드 패턴</strong></td>
<td>GPU 부족 시에만 Cloud 확장</td>
</tr>
<tr>
<td><strong>네트워크 요구</strong></td>
<td>복잡한 멀티패스 라우팅 불필요</td>
</tr>
<tr>
<td><strong>운영 우선순위</strong></td>
<td>고가용성 &gt; 대역폭 확장성</td>
</tr>
</tbody></table>
<p>Active-Active의 핵심 장점인 <strong>병렬 트래픽 처리</strong>와 <strong>대역폭 확장</strong>은 우리 환경에서는 실효성이 낮았어요. 상시 대량 트래픽이 흐르는 환경이 아니기 때문이에요. 오히려 <strong>라우팅 복잡도만 증가</strong>시키는 결과가 될 수 있었어요.</p>
<hr>
<h2 id="4-최종-선택-keepalived--vrrp">4. 최종 선택: Keepalived + VRRP</h2>
<h3 id="41-선택-근거">4.1 선택 근거</h3>
<p>종합적으로 판단했을 때, 방안 B(Keepalived + VRRP)가 우리 환경에 더 적합했습니다. 선택 근거를 정리하면 다음과 같습니다.</p>
<p><strong>✅ 단순성과 신뢰성</strong></p>
<blockquote>
<p>복잡도 감소 → 장애 지점 감소 → 운영 안정성 향상</p>
</blockquote>
<table>
<thead>
<tr>
<th><strong>요소</strong></th>
<th><strong>개선 효과</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>관리 컴포넌트 수</strong></td>
<td>5개 → 3개</td>
</tr>
<tr>
<td><strong>설정 복잡도</strong></td>
<td>높음 → 낮음</td>
</tr>
<tr>
<td><strong>디버깅 난이도</strong></td>
<td>BGP 라우팅 분석 → VIP 상태 확인</td>
</tr>
<tr>
<td><strong>운영 경험</strong></td>
<td>신규 학습 필요 → 기존 활용 중</td>
</tr>
</tbody></table>
<p>운영에서 가장 위험한 것은 &quot;복잡해서 아무도 손댈 수 없는 시스템&quot;입니다. 단순할수록 장애 지점이 줄고, 장애가 발생해도 빠르게 원인을 파악할 수 있습니다.</p>
<p><strong>✅ 기존 인프라 활용</strong></p>
<p>우리는 이미 <strong>프록시 서버에서 Keepalived를 운영</strong> 중이었어요. 이 점이 결정적이었어요.</p>
<pre><code>현재 프록시 구조:
  - proxy-active:  VIP 소유
  - proxy-standby: VIP 대기

VPN 게이트웨이 통합:
  - proxy-standby → VPN MASTER (rc13-13)
  - proxy-active  → VPN BACKUP (rc15-01)</code></pre><p>두 노드가 이미 <strong>같은 랙에 위치</strong>하고 있어 VIP 공유가 가능했고, Keepalived 운영 경험이 있으니 <strong>러닝 커브가 사실상 제로</strong>였어요. 기존 모니터링/알림 체계도 그대로 재사용할 수 있었어요.</p>
<p><strong>✅ 충분히 빠른 Failover</strong></p>
<table>
<thead>
<tr>
<th><strong>요구사항</strong></th>
<th><strong>목표</strong></th>
<th><strong>실제 성능</strong></th>
<th><strong>판정</strong></th>
</tr>
</thead>
<tbody><tr>
<td>자동 복구</td>
<td>수동 개입 없음</td>
<td>✅ VRRP 자동 전환</td>
<td><strong>충족</strong></td>
</tr>
<tr>
<td>복구 시간</td>
<td>10초 이내</td>
<td>✅ 4~5초</td>
<td><strong>초과 달성</strong></td>
</tr>
<tr>
<td>투명성</td>
<td>애플리케이션 인지 불가</td>
<td>✅ TCP Keep-Alive 재시도 내</td>
<td><strong>충족</strong></td>
</tr>
</tbody></table>
<p>여기서 <strong>strongSwan HA Plugin</strong>의 역할이 컸어요. VPN 세션(SA)을 실시간으로 복제하기 때문에, Failover 시 IKE/IPsec 세션을 처음부터 재수립할 필요가 없어요. 이 덕분에 <strong>4~5초라는 빠른 복구 시간</strong>을 달성할 수 있었습니다.</p>
<p><strong>✅ 비용 효율성</strong></p>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>연간 절감</strong></th>
</tr>
</thead>
<tbody><tr>
<td>VPN 게이트웨이</td>
<td>$648</td>
</tr>
<tr>
<td>Cloud Router</td>
<td>$120</td>
</tr>
<tr>
<td><strong>총 절감</strong></td>
<td><strong>$768/년</strong></td>
</tr>
</tbody></table>
<p>99.9% SLA도 우리 환경에 충분했어요. 연간 최대 다운타임이 9시간이지만, Cloud GPU는 간헐적으로만 사용하고, 온프레미스 우선 정책이기 때문에 실질적인 영향은 미미했어요.</p>
<hr>
<h2 id="5-ha-failover-테스트-결과">5. HA Failover 테스트 결과</h2>
<p>마지막으로 실제 Failover 테스트를 수행한 결과를 공유할게요. 세 가지 장애 시나리오를 시뮬레이션했습니다.</p>
<table>
<thead>
<tr>
<th><strong>시나리오</strong></th>
<th><strong>패킷 손실 기간</strong></th>
<th><strong>복구 방식</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>MASTER 노드 완전 다운</strong></td>
<td>약 5초</td>
<td>VRRP가 VIP 이동 → BACKUP이 MASTER로 승격</td>
</tr>
<tr>
<td><strong>strongSwan 프로세스 장애</strong></td>
<td>약 5초</td>
<td>Health Check 실패 → Keepalived가 VIP 반납</td>
</tr>
<tr>
<td><strong>네트워크 인터페이스 다운</strong></td>
<td>약 3초</td>
<td>VRRP hello 패킷 미수신 → 즉시 Failover</td>
</tr>
</tbody></table>
<p>세 가지 시나리오 모두 <strong>10초 이내에 자동 복구</strong>가 이루어졌으며, 특히 네트워크 인터페이스 다운의 경우 VRRP가 hello 패킷을 즉시 감지하지 못하기 때문에 <strong>3초 만에 Failover가 완료</strong>되었습니다.</p>
<blockquote>
<p>모든 시나리오에서 수동 개입 없이 자동 복구가 이루어졌고, 목표했던 10초 이내 복구 시간을 달성했습니다.</p>
</blockquote>
<hr>
<h2 id="6-마무리">6. 마무리</h2>
<p>이번 글에서는 하이브리드 클러스터 운영을 위한 <strong>VPN HA 구성</strong>을 다뤘습니다. 두 가지 방안을 비교 검토한 끝에, 우리 환경에 맞는 <strong>Keepalived + VRRP 기반 Active-Standby 구조</strong>를 선택했어요.</p>
<p>결국 HA 구성에서 가장 중요한 것은 <strong>&quot;가장 좋은 기술&quot;이 아니라 &quot;우리 환경에 가장 적합한 기술&quot;</strong>을 선택하는 것이라고 생각합니다. BGP + Active-Active가 기술적으로는 우월할 수 있지만, 운영 복잡도와 비용, 그리고 팀의 기존 역량까지 고려하면 Keepalived가 더 합리적인 선택이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[on-prem + GCP 하이브리드 클러스터 구축기 - 3]]></title>
            <link>https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-3</link>
            <guid>https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-3</guid>
            <pubDate>Sat, 10 Jan 2026 09:13:20 GMT</pubDate>
            <description><![CDATA[<h1 id="0-이전-글-요약">0. 이전 글 요약</h1>
<p><a href="https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-2">on-prem + GCP 하이브리드 클러스터 구축기-2 </a>에서의 Hybrid-Cluster 노드 오토스케일링 도입은 다음 질문에서 출발했습니다.  </p>
<blockquote>
<p>&quot;On-prem GPU가 부족해지는 순간, Cloud GPU 노드를 자동으로 끌어쓸 수 있을까?&quot;</p>
</blockquote>
<p>이를 검증하기 위해 우리는 <strong>&quot;온프레미스 k8s 클러스터&quot;</strong>와 <strong>&quot;GCP Cloud 노드&quot;</strong>를 하나의 <strong>&quot;하이브리드 클러스터&quot;</strong>로 묶고, Cluster Autoscaler 기반 자동 확장 기능이 실제로 운영 가능한 수준인지 테스트 했어요.</p>
<p>다음 내용은 그 과정에서 실제로 부딪힌 문제들과 이를 해결해 운영 수준으로 끌어올린 방식을 정리한 것입니다.</p>
<h2 id="1-테스트-과정에서-마주친-문제들">1. 테스트 과정에서 마주친 문제들</h2>
<p>하이브리드 환경에서 Autoscaling을 구현하는 일은 생각보다 손쉽게 흘러가지 않았어요.</p>
<p>실제 운영에서 사용되려면 아래와 같은 문제들이 반드시 해결되어야 했습니다.</p>
<p>이 섹션에서는 그중 핵심이었던 3가지 난제를 소개합니다.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>원인</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>Cluster Autoscaler 전용 노드 필요</td>
<td>GCP metadata 접근 필요 / CA 중단 시 스케일 로직 마비</td>
<td>전용 CPU 노드에 CA 고정 배치</td>
</tr>
<tr>
<td>GPU 노드 중복 스케일업</td>
<td>드라이버 초기화 중 Pod 스케줄 불가 → CA가 추가 스케일업</td>
<td>startup taint + GPU validator 기반 taint remover</td>
</tr>
<tr>
<td>MIG kube-env와 실제 노드 mismatch</td>
<td>CA는 kube-env 기반으로 시뮬레이션 수행</td>
<td>kube-env와 init-script 동기화</td>
</tr>
</tbody></table>
<h3 id="11-cluster-autoscaler는-결국-전용-노드가-필요하다">1.1 Cluster Autoscaler는 결국 &#39;전용 노드&#39;가 필요하다</h3>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/21ac7809-9cd8-4a81-82b2-07ac0da06bc6/image.png" alt=""></p>
<p>초기 설계에서는 On-prem 노드에 Cluster Autoscaler(CA)를 띄우면 되겠다고 판단했어요.</p>
<p>하지만 아래와 같은 문제가 드러났습니다.</p>
<p><strong>문제 1. Cluster Autoscaler는 GCP Metadata 서버와 통신해야 합니다</strong></p>
<p>Cluster Autoscaler(GCE Provider)는 아래 API들을 계속 호출합니다.</p>
<ul>
<li>MIG 상태 조회 (인스턴스 개수 / 준비 상태)</li>
<li>인스턴스 그룹 resize 요청</li>
<li>health check API</li>
<li>node template 추출을 위한 metadata 조회</li>
</ul>
<p>하지만 GCP Metadata 서버는 VPC 내부에서만 접근 가능하며, On-prem에서는 접근할 수 없습니다.</p>
<p>즉, Cluster Autoscaler가 On-prem에 있으면 Cloud Provider 기능이 정상 동작할 수 없어요.</p>
<p><strong>해결: Cluster Autoscaler는 반드시 &#39;전용 클라우드 노드&#39;가 필요합니다</strong></p>
<p>아래 기준으로 ClusterAutoscaler를 배치했습니다.</p>
<p>✔ 클라우드에 생성된 하나의 노드<br>✔ GPU 노드가 아닌 순수 CPU 노드<br>✔ eviction 방지를 위한 taint/toleration 구성<br>✔ 안정적 네트워크 환경(Cloud Metadata 접근 가능한 환경)</p>
<p>Cluster Autoscaler는 안정성을 최우선으로 하는 핵심 시스템이므로 반드시 전용 노드에서 운영해야 합니다.</p>
<h2 id="12-gpu-노드의-중복-스케일업-문제-→-startup-taint--taint-remover-도입">1.2 GPU 노드의 중복 스케일업 문제 → Startup Taint + Taint Remover 도입</h2>
<p>테스트 과정 중 가장 골칫거리는 다음 문제였어요.</p>
<p><strong>문제: GPU 노드는 1개만 필요하지만, 2개 이상 생성되는 현상</strong></p>
<p><strong>원인: GPU Driver 초기화가 오래 걸립니다</strong></p>
<p>Cloud GPU 노드가 생성되면 GPU Operator의 init 과정이 다음 순서로 진행됩니다.</p>
<ol>
<li>GPU 드라이버 설치</li>
<li>gpu-operator-validator 컨테이너 실행</li>
<li>GPU 장치 인식 확인</li>
<li>GPU를 kubernetes에서 사용할 수 있게 됨</li>
</ol>
<p>이 과정은 수십 초 ~ 수 분까지 걸릴 수 있으며, 초기화 중에는 Pod가 이 노드에 할당될 수 없어요.</p>
<p>그런데 Cluster Autoscaler는 이 상태를 보고 이렇게 판단합니다:</p>
<p>&quot;아직 스케줄 가능한 GPU 노드가 없구나 → 하나 더 늘리자&quot;</p>
<p>그 결과 중복 스케일업이 발생합니다.</p>
<p><strong>해결책: Cluster Autoscaler의 Startup Taint 기능 활용</strong></p>
<p>ClusterAutoscaler는 다음 prefix의 taint가 붙은 노드를 <strong>&quot;아직 준비 중이지만 존재하는 노드&quot;</strong>로 인식합니다.</p>
<pre><code>startup-taint.cluster-autoscaler.kubernetes.io/*</code></pre><p>따라서 다음 전략을 사용했어요.</p>
<ul>
<li>노드가 join되는 시점에 startup taint를 자동 부여</li>
<li>GPU 드라이버 설치 완료 후 → taint 제거</li>
<li>CA는 이 taint가 붙은 노드를 &quot;이미 스케일업된 노드&quot;로 인식</li>
<li>결과적으로 중복 스케일업 방지</li>
</ul>
<p><strong>문제: taint 제거를 누가, 언제, 어떻게 수행할 것인가?</strong></p>
<p>포인트는 GPU가 정상 인식되기 전에는 절대 taint를 제거하면 안 된다는 점입니다.</p>
<p>이 로직을 어디에 넣을지 결정하는 데 많은 시간이 걸렸어요.</p>
<p><strong>최종 해결: Taint Remover DaemonSet 생성</strong></p>
<p>gpu-operator-validator는 원래 init containers의 결과를 판정하고 성공하면 무한 sleep 상태에 들어갑니다.</p>
<p>저는 gpu-operator-validator의 이미지를 이용하여 startup taint를 제거하도록 직접 개발했습니다.</p>
<ul>
<li>원래 gpu-validator 동작을 그대로 수행합니다</li>
<li>GPU 장치 목록을 조회합니다</li>
<li>정상 인식 시 startup taint 제거합니다</li>
<li>클라우드 GPU 노드에만 스케줄링됩니다</li>
</ul>
<p><strong>장점</strong></p>
<p>✔ GPU driver 설치 라이프사이클과 완벽하게 동기화됩니다<br>✔ 노드 초기화 → 드라이버 설치 → gpu-operator-validator → taint 제거 이 전체 흐름이 하나로 연결돼요  </p>
<p>최종적으로 GPU 노드 스케일링부터 Taint 제거는 다음과 같은 시나리오로 동작합니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/82f18694-1e74-40c1-a9dd-b5192a77b42a/image.png" alt=""></p>
<ol>
<li><strong>GPU 노드 스케일업 요청</strong>: Cluster Autoscaler가 GCP MIG에 GPU 노드 생성을 요청합니다.</li>
<li><strong>새 인스턴스 생성</strong>: GCP MIG가 startup-taint가 부여된 상태로 새 인스턴스를 생성합니다.</li>
<li><strong>GPU Operator &amp; Validator Pod 스케줄링</strong>: 새 노드에 GPU Operator와 Validator Pod이 스케줄됩니다.</li>
<li><strong>드라이버 설치 및 GPU 장치 검증</strong>: Taint-Remover가 드라이버 설치 및 GPU 장치 검증을 수행합니다.</li>
<li><strong>초기화 진행 중</strong>: 이 시점에는 Pod 스케줄이 불가능합니다(<code>startup-taint.cluster-autoscaler.k8s.io/gpu-not-ready</code>).</li>
<li><strong>클러스터 스냅샷 확인</strong>: Cluster Autoscaler가 클러스터 상태를 확인하고 &quot;notStarted 노드가 이미 존재&quot;한다고 인식하여 추가 스케일업을 막습니다.</li>
<li><strong>Startup Taint 제거</strong>: validator 성공 시점에 Taint-Remover가 startup taint를 제거합니다.</li>
<li><strong>노드 Ready</strong>: 노드가 Ready 상태가 됩니다.</li>
<li><strong>GPU Pod 스케줄링 시작</strong>: Kube-Scheduler가 GPU Pod 스케줄링을 시작합니다.</li>
</ol>
<h2 id="13-gcp-mig의-kube-env와-실제-노드-설정이-일치해야-한다">1.3 GCP MIG의 kube-env와 실제 노드 설정이 일치해야 한다</h2>
<p>이 이슈의 구조를 정리하면 다음과 같습니다.</p>
<p><strong>설정 관리 흐름</strong>: Instance 초기화 script가 &quot;taint, label 적용해서 join&quot;을 통해 Cloud GPU Node에 설정을 적용합니다.</p>
<p><strong>ClusterAutoscaler가 보는 세계</strong>:</p>
<ul>
<li>MIG Instance Template (kube-env)는 &quot;scale-from-zero 시 템플릿 노드 생성&quot;합니다.</li>
<li>Cloud GPU Node는 &quot;이미 존재하는 노드 기준 템플릿 노드 생성&quot;합니다.</li>
<li>이 둘이 Cluster Autoscaler와 상호작용합니다.</li>
</ul>
<p>ClusterAutoscaler가 MIG를 스케일아웃할지 말지 판단할 때는 &quot;템플릿 노드&quot;를 생성해 시뮬레이션을 합니다.</p>
<p>이때 template node는 다음 우선 순위로부터 생성됩니다.</p>
<ol>
<li>이미 존재하는 healthy cloud node 기반</li>
<li>ClusterAutoscaler 내부 캐시 기반</li>
<li>MIG instance template의 kube-env 기반 ← scale-from-zero 상황에서 필수</li>
</ol>
<p><strong>문제: MIG kube-env와 실제 노드 설정이 다를 수 있습니다</strong></p>
<p>예시 상황: taint mismatch</p>
<ul>
<li>MIG template의 kube-env에는 <code>newen.ai/gcp=true</code></li>
<li>실제 노드에는 label 적용 안함</li>
<li>→ ClusterAutoscaler는 &quot;이 nodegroup은 taint 때문에 unschedulable&quot;로 판단</li>
<li>→ scale-out 실패</li>
</ul>
<p><strong>해결 전략: kube-env 싱크 관리</strong></p>
<p>아래 조치를 적용했습니다.</p>
<p>✔ MIG kube-env를 init-script와 100% 동일하게 유지<br>✔ label/taint/env 설정을 하나의 source에서만 관리</p>
<p>즉, MIG template의 kube-env는 CA 입장에서 절대적 진실(single source of truth)이므로 이를 맞추는 것이 스케일 업/다운의 핵심 안정성 기준이었습니다.</p>
<h2 id="2-vpn-gateway-고가용성ha-구성">2. VPN Gateway 고가용성(HA) 구성</h2>
<p>Cluster Autoscaler를 통한 노드 자동 확장 기능이 안정적으로 동작하게 되면서 새로운 과제가 대두되었어요.</p>
<p>바로 <strong>On-prem과 Cloud 간의 네트워크 연결을 담당하는 VPN Gateway의 장애 대비</strong>입니다.</p>
<p>테스트 단계에서는 strongSwan 기반 S2S VPN으로 단일 VPN 게이트웨이를 운영했지만, 실제 프로덕션 환경에서는 VPN Gateway 자체가 단일 장애점(Single Point of Failure)이 될 수 밖에 없습니다.</p>
<p>온프렘에 있는 Control-plane과 통신을 못하면 node가 NotReady 상태가 되기때문이죠.</p>
<p>다음 게시물에서는 VPN Gateway의 고가용성을 확보하기 위해 검토한 여러 HA 기법과, 최종적으로 선택하게 된 솔루션, 그리고 이를 통해 하이브리드 클러스터의 안정성을 한 단계 끌어올린 방식을 다뤄볼 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[on-prem + GCP 하이브리드 클러스터 구축기 - 2]]></title>
            <link>https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-2</link>
            <guid>https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-2</guid>
            <pubDate>Sat, 10 Jan 2026 08:49:32 GMT</pubDate>
            <description><![CDATA[<h1 id="1-hybrid-cluster-노드-오토스케일링-cloud-gpu를-자동으로-끌어쓰는-방법">1. Hybrid-Cluster 노드 오토스케일링: Cloud GPU를 자동으로 끌어쓰는 방법</h1>
<p><a href="https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-1">on-prem + GCP 하이브리드 클러스터 구축기-1</a> 에서 왜 Hybrid Cloud가 필요한지, 어떻게 On-prem ↔ Cloud 네트워크를 연결할지, 어떤 VPN 기술을 선택했는지 정리했어요.</p>
<p>이제 다음 과제로 넘어갑니다.
▶ Cloud GPU를 언제, 어떻게 자동으로 끌어 쓸 것인가?
▶ On-prem 기반 K8s에서 Cloud 쪽 Worker Node를 자동 확장할 수 있는가?</p>
<p>즉, Hybrid-Cluster의 핵심 능력인 Node Autoscaling에 대한 테스트를 시작해요.</p>
<h2 id="11-gpu-노드-오토스케일링의-필요성">1.1 GPU 노드 오토스케일링의 필요성</h2>
<p>AI workload는 대부분 다음과 같은 패턴을 갖습니다.</p>
<ul>
<li>순간적으로 폭증하는 RPS(Requests Per Second)</li>
<li>모델 특성 상 높은 GPU 자원 요구량</li>
<li>특정 고객사 업무 시간대 부하 쏠림</li>
<li>긴급 분석/파일 처리로 인한 단기적 GPU 필요</li>
</ul>
<p>실제 운영에서 발생한 주요 문제예요.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>On-prem GPU 부족</td>
<td>HPA가 스케일아웃하려 해도 GPU 노드가 없어 Pending Pod 발생</td>
</tr>
<tr>
<td>복구 지연</td>
<td>장애/업데이트 시 GPU 노드가 가용해질 때까지 시간 소요</td>
</tr>
<tr>
<td>고객 요구사항 미충족</td>
<td>&quot;갑자기 RPS 2배 이상 증가&quot; 같은 요구 대응 곤란</td>
</tr>
<tr>
<td>비용 이슈</td>
<td>온프렘 GPU 증설은 수개월 리드타임 + 초기 투자 ↑</td>
</tr>
</tbody></table>
<p>결국, 다음 조건을 모두 만족하는 오토스케일링 전략이 필요했습니다.</p>
<pre><code>✔ GPU 부족 시 Cloud에서 자동으로 노드 추가 
✔ Pod 생성 요청(HPA 등)에 따라 적시에 동작  
✔ 안전한 스케일 인
✔ On-prem 노드에 영향 없어야 함
✔ 네트워크는 이미 구성된 VPN 경로를 그대로 사용
✔ Pod-to-Pod 통신/서비스 Mesh 구조 유지</code></pre><h2 id="12-노드-오토스케일링-연구-어떤-방식이-있는가">1.2 노드 오토스케일링 연구: 어떤 방식이 있는가?</h2>
<p>Hybrid 환경에서 가능한 노드 오토스케일링 기법을 모두 조사했어요.</p>
<p>총 5가지 접근 방식이 검토되었습니다.</p>
<h3 id="📌-오토스케일링-방법-5가지-요약">📌 오토스케일링 방법 5가지 요약</h3>
<table>
<thead>
<tr>
<th>방법</th>
<th>장점</th>
<th>단점</th>
<th>결론</th>
</tr>
</thead>
<tbody><tr>
<td>1. Virtual Kubelet + Kip</td>
<td>빠른 확장 / 가상노드 기반</td>
<td>Kip 유지보수 중단, 안정성 ↓</td>
<td>❌ 탈락</td>
</tr>
<tr>
<td>2. Cluster Autoscaler + Cluster API</td>
<td>완전한 K8s native infra-as-code</td>
<td>관리 클러스터 필요, 오버엔지니어링</td>
<td>❌ 탈락</td>
</tr>
<tr>
<td>3. Cluster Autoscaler + 초기화 Script</td>
<td>안정적 / Cloud-agnostic / K8s 공식</td>
<td>구현 복잡도는 있지만 현실적인 대안</td>
<td>✅ 후보 채택</td>
</tr>
<tr>
<td>4. GCP Instance Group Metric Autoscaler</td>
<td>구성 쉽다</td>
<td>Cloud 측 metric만 사용 / 제로스케일 불가</td>
<td>❌ 탈락</td>
</tr>
<tr>
<td>5. Event-based Scaling (Grafana alert → Pub/Sub → instance scale)</td>
<td>최대 커스터마이즈</td>
<td>관리 복잡, 운영 난이도 ↑</td>
<td>△ 테스트 가능</td>
</tr>
</tbody></table>
<h2 id="13-각-방법에-대한-정리">1.3 각 방법에 대한 정리</h2>
<h3 id="1-virtual-kubelet--kip">1) Virtual Kubelet &amp; Kip</h3>
<p><strong>결론: ❌ Kip 유지보수 종료로 채택 불가</strong><br>가상 노드에 Pod 스케줄 → Kip가 Cloud에서 인스턴스를 생성하는 방식입니다. 하지만 Kip가 더 이상 활성 유지되지 않아 안정성 확보가 불가해 배제했어요.</p>
<h3 id="2-cluster-api-capi">2) Cluster API (CAPI)</h3>
<p><strong>결론: ❌ 운영 난이도 &amp; 새 클러스터 생성 필수 → 오버엔지니어링</strong><br>Cluster API는 K8s를 K8s로 관리하는 확장된 방식이지만, 이번 프로젝트 규모에는 과하다고 판단되었습니다.</p>
<h3 id="3-cluster-autoscaler-ca--초기화-스크립트">3) Cluster Autoscaler (CA) + 초기화 스크립트</h3>
<p><strong>결론: 현실적 + 안정적 + 클라우드 친화적 → 1순위 후보</strong><br><strong>Cluster Autoscaler</strong>는 K8s SIG에서 개발하는 공식 Autoscaler입니다.<br>Pod Pending 상태를 감지 → Cloud Provider API 호출하며, MIG / ASG / VM 생성이 가능해요.<br>GCP에서는 MIG(Instance Group)와 자연스럽게 연동되며, 초기화 스크립트로 자동 kubeadm join + label 등록 + providerID 설정까지 처리 가능합니다.<br>이 방식은 하이브리드 클러스터에서도 완전한 운영이 가능했으며 가장 좋은 결과를 보여 <strong>최종 검증 대상</strong>이 되었습니다.</p>
<h3 id="4-gcp-인스턴스-그룹-메트릭-기반-autoscaler">4) GCP 인스턴스 그룹 메트릭 기반 Autoscaler</h3>
<p><strong>결론: ❌ 간단하지만 Cloud 측 metric에 종속 → 하이브리드 부적합</strong><br>온프렘 기준 metric(HPA, Prometheus)을 사용할 수 없어서 제외했습니다.</p>
<h3 id="5-이벤트-기반-스케일링">5) 이벤트 기반 스케일링</h3>
<p><strong>결론: 매우 유연하지만 관리 난이도 매우 높음 → 부가적 전략으로 활용 가능</strong><br>Grafana alert → webhook → GCP pub/sub → MIG scale-out 흐름으로 조건 조합이 가능하지만, 운영 복잡도가 높아 보조적 활용이 적합해요.</p>
<h2 id="14-cluster-autoscaler로-승부-보기">1.4 Cluster Autoscaler로 승부 보기</h2>
<p>이번 프로젝트는 다음 목표를 기준으로 테스트를 설계하였습니다.</p>
<h3 id="✔-검증-목표">✔ 검증 목표</h3>
<table>
<thead>
<tr>
<th>검증 항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>노드 스케일 아웃</td>
<td>Pod pending 시 MIG가 정상적으로 VM 생성하는가</td>
</tr>
<tr>
<td>노드 스케일 인</td>
<td>VM 제거 시 Pod 안전 이동이 가능한가</td>
</tr>
<tr>
<td>GPU Pod 운용</td>
<td>GPU workload에도 Autoscaling 동작하는가</td>
</tr>
<tr>
<td>On-prem 영향</td>
<td>온프레미스 노드를 절대 변경하지 않는가</td>
</tr>
<tr>
<td>HPA 연동</td>
<td>HPA / KEDA 등 상위 autoscaler와 함께 동작하는가</td>
</tr>
<tr>
<td>네트워크</td>
<td>Cloud Pod ↔ On-prem Pod ↔ 외부 통신 정상 여부</td>
</tr>
<tr>
<td>비용</td>
<td>스케일인/아웃 기준 최적화 가능성</td>
</tr>
</tbody></table>
<h2 id="15-환경-구성">1.5 환경 구성</h2>
<p><strong>구성 요소</strong>  </p>
<ul>
<li>On-prem K8s Cluster (Control Plane + worker)  </li>
<li>Cloud MIG + GPU 인스턴스 or CPU 인스턴스  </li>
<li>S2S VPN (strongSwan ↔ Cloud VPN)  </li>
<li>Calico CNI / Istio Service Mesh  </li>
<li>Cluster Autoscaler (v1.33)  </li>
<li>Prometheus / Grafana / HPA / KEDA  </li>
</ul>
<p><strong>구성도</strong>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/59b8527e-0396-42b7-a652-9c8ff700d55a/image.png" alt=""></p>
<p><strong>노드 확장 시나리오</strong>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/dea080f2-724c-4193-a6bb-8895371f8600/image.png" alt=""></p>
<h2 id="16-주요-테스트-결과-요약">1.6 주요 테스트 결과 요약</h2>
<p>✔ <strong>1) 노드 스케일 아웃: 성공</strong><br>Pending Pod 발생 시 MIG가 자동으로 VM 생성 + kubeadm join 성공했습니다.  </p>
<p>✔ <strong>2) 노드 스케일 인: 성공</strong><br>PDB(pod disruption budget) 설정 시 다운타임 없이 안전하게 스케일 인 성공했어요.  </p>
<p>✔ <strong>3) GPU Pod Autoscaling: 성공</strong><br>CA의 scale-down-gpu-utilization-threshold로 GPU 기준 스케일 인이 정상 작동합니다.  </p>
<p>✔ <strong>4) On-prem 영향 없음</strong><br>Cluster Autoscaler가 MIG에 속하지 않은 온프레미스 노드는 절대 제거하지 않습니다.  </p>
<p>✔ <strong>5) HPA / KEDA 연동: 정상</strong><br>HPA로 Pod 확장 → Pod pending → CA 노드 확장 → 부하 감소 → HPA 축소 → CA 지연 후 노드 스케일 인 순서로 동작해요.  </p>
<p>✔ <strong>6) 네트워크: 정상</strong><br>Cloud Pod ↔ On-prem Pod ↔ 사무실 클라이언트 통신 성공하며, Istio mTLS, proxy-init 정상 작동합니다.</p>
<h2 id="17-테스트-결론">1.7 테스트 결론</h2>
<p>Hybrid-Cluster 환경에서도 K8s Cluster Autoscaler를 기반으로 Cloud Node 자동 확장이 충분히 실현 가능합니다.</p>
<p><strong>장점</strong>  </p>
<ul>
<li>K8s native 방식 → 기존 운영이 크게 변하지 않습니다  </li>
<li>HPA/KEDA와 자연스럽게 연동돼요  </li>
<li>GPU workload에도 정상 작동합니다  </li>
<li>온프레미스 노드 영향 없음  </li>
<li>안정적이고 예측 가능한 스케일링  </li>
</ul>
<p><strong>단점 / 개선 포인트</strong>  </p>
<ul>
<li>신규 노드 초기화 시간 최적화 필요 </li>
<li>Cloud 모니터링 체계 필요  </li>
</ul>
<h1 id="2-추후">2. 추후..</h1>
<p>다음 게시글 <a href="https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-3">on-prem + GCP 하이브리드 클러스터 구축기 - 3</a> 에서는 이번 검증을 거치면서 실제로 겪었던 이슈와 해결 방법 그리고 실제 Hybrid-Cluster 운영 전략을 정리합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[on-prem + GCP 하이브리드 클러스터 구축기 - 1]]></title>
            <link>https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-1</link>
            <guid>https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-1</guid>
            <pubDate>Sat, 10 Jan 2026 08:40:01 GMT</pubDate>
            <description><![CDATA[<h1 id="1-왜-cloud가-필요한가">1. 왜 Cloud가 필요한가?</h1>
<p>현재 저희는 자체 인프라(On-prem Cluster)를 기반으로 고성능 AI 서비스를 제공하고 있습니다.</p>
<p>이 전략은 많은 장점을 가져다주었어요.</p>
<ul>
<li>GPU 관련 <strong>장기 운영 비용 절감</strong></li>
<li>내부망 기반의 <strong>데이터 보안 강화</strong></li>
<li>연산 시 <strong>로컬 지연 최소화</strong></li>
<li>인프라 운영 경험 및 <strong>기술 내재화</strong></li>
</ul>
<p>하지만 운영을 지속할수록 다음과 같은 현실을 마주하게 됐습니다...</p>
<h3 id="11-gpu-공급은-항상-수요를-따라가지-못한다">1.1 GPU 공급은 항상 수요를 따라가지 못한다</h3>
<p>현재 AI 기반 기능 확장, 파일 처리량 증가, 신규 고객 PoC 등으로 GPU 수요는 날이갈수록 늘어나는 추세예요.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>필요 GPU</th>
<th>우리 현실</th>
</tr>
</thead>
<tbody><tr>
<td>신규 기능 확장</td>
<td>+2 ~ +4 GPU</td>
<td>예산 승인/납기 대기 📦</td>
</tr>
<tr>
<td>피크 트래픽</td>
<td>+2 ~ +4 GPU</td>
<td>이미 꽉 찬 장비 😥</td>
</tr>
<tr>
<td>긴급 PoC 요청</td>
<td>&quot;지금 필요합니다&quot;</td>
<td>&quot;3개월 뒤에 가능…&quot;</td>
</tr>
</tbody></table>
<blockquote>
<p>GPU는 남는 법이 없습니다.
늘 부족할 때가 찾아와요.</p>
</blockquote>
<h3 id="12-장애는-예고-없이-찾아오고">1.2 장애는 예고 없이 찾아오고</h3>
<h3 id="온프렘만으로는-유연한-복구가-어렵다">온프렘만으로는 &quot;유연한 복구&quot;가 어렵다</h3>
<p>우리가 통제할 수 없는 문제들:</p>
<ul>
<li>장비 장애(전원, 디스크, PCIe 문제)</li>
<li>네트워크 노드 다운</li>
<li>냉각 문제 → 랙 전체 비상 Shutdown</li>
<li>운영 중 실수로 인한 오작동</li>
</ul>
<p>온프렘은 <strong>내부 자원으로만 복구해야 하기 때문에</strong> 장애 시 <strong>즉시 대응력이 크게 떨어질 수 있습니다</strong>.</p>
<blockquote>
<p>&quot;우린 장애 복구 계획이 필요했고, 그것이 단순 백업 이상이어야 했어요.&quot;</p>
</blockquote>
<h3 id="13-그래서-선택한-해법-hybrid-cloud">1.3 그래서 선택한 해법: Hybrid Cloud</h3>
<p><strong>필요할 때 Cloud GPU를 &#39;당겨 쓰는&#39; 방식</strong></p>
<p>우리가 정의한 목표는 명확합니다</p>
<table>
<thead>
<tr>
<th>목표</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>GPU 탄력 확장(Bursting)</td>
<td>수요 급증 시 Cloud에서 GPU 임대</td>
</tr>
<tr>
<td>장애 복구</td>
<td>On-prem 장애 시 서비스 지속</td>
</tr>
<tr>
<td>Private Network 확장</td>
<td>Cloud도 내부망처럼 연결</td>
</tr>
<tr>
<td>운영 자동화</td>
<td>k8s 기반 배포/스케일링 유지</td>
</tr>
</tbody></table>
<p>&#39;All-in Cloud&#39;가 아니라</p>
<p><strong>On-prem 중심 + Cloud로 확장되는 구조</strong>를 만드는 거예요.</p>
<p>여기엔 필수적으로 on-prem과 cloud간 통신, 그리고 워크로드의 클라우드 자동 확장이 필요했습니다. </p>
<p>이 게시글에선 on-prem과 cloud 통신을 어떻게 성공적으로 이을 수 있었는지 설명할게요.</p>
<h1 id="2-on-prem-과-cloud-간의-네트워크-연결-방법">2. On-prem 과 Cloud 간의 네트워크 연결 방법</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/035083af-da1a-49ce-9d97-12d24730b571/image.png" alt=""></p>
<h3 id="21-네트워크-연결-방법-선정">2.1 네트워크 연결 방법 선정</h3>
<p>저희 AI 서비스는 On-prem 쿠버네티스 클러스터를 기반으로 서비스하고 있으며, Cloud GPU 자원을 <strong>내부망 연장</strong> 형태로 <strong>Pod 간의 네트워크 연결</strong>을 활용하는 것이 핵심 요구사항이었습니다.</p>
<p>이를 충족하기 위해 3가지 방안을 검토했어요.</p>
<p><strong>방안 1. Public 망 이용</strong></p>
<p>Cloud에서 공인 IP를 통해 직접 통신하는 방식</p>
<table>
<thead>
<tr>
<th>장점</th>
<th>한계</th>
</tr>
</thead>
<tbody><tr>
<td>구성 간단</td>
<td>Private 통신 요구사항 미충족</td>
</tr>
<tr>
<td>초기 비용 없음</td>
<td>CNI Multiple NIC 구성 불가</td>
</tr>
</tbody></table>
<p>❌ 결론: 요구사항을 충족하지 못해 제외</p>
<hr>
<p><strong>방안 2. 전용선(전용 회선) 임대</strong></p>
<p>Cloud ↔ IDC 간 물리 전용 회선 구성</p>
<table>
<thead>
<tr>
<th>장점</th>
<th>한계</th>
</tr>
</thead>
<tbody><tr>
<td>최고 수준 네트워크 성능 &amp; 안정성</td>
<td>초기/월 비용 과다</td>
</tr>
<tr>
<td>SLA 보장</td>
<td>Lead-time 길고 운영 제약</td>
</tr>
</tbody></table>
<p>❌ 결론: 예산 대비 효율성 미흡</p>
<hr>
<p><strong>방안 3. VPN 이용</strong></p>
<p>On-prem 과 Cloud 간 Site-to-Site VPN 구성</p>
<table>
<thead>
<tr>
<th>장점</th>
<th>효과</th>
</tr>
</thead>
<tbody><tr>
<td>사설 IP 기반 통신 보장</td>
<td>내부망처럼 Pod/Node 간 직접 연결</td>
</tr>
<tr>
<td>네트워크 제어/확장 용이</td>
<td>Calico 기반 Overlay 유지</td>
</tr>
<tr>
<td>운영 유연성</td>
<td>Cloud GPU 탄력 활용 가능</td>
</tr>
</tbody></table>
<p><strong>요구사항에 가장 부합하여 채택</strong></p>
<hr>
<h1 id="3-그러면-어떤-vpn을-사용해야-할까">3. 그러면 어떤 VPN을 사용해야 할까?</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/5ea0d9f6-806b-4e81-ad07-d288ed035cd4/image.png" alt=""></p>
<p>네트워크 연결 방식은 <strong>VPN</strong>으로 결정되었지만, 어떤 VPN 기술 스택을 선택할지에 대한 검토가 필요했습니다.</p>
<p>우리가 고려한 핵심 평가 기준은 다음과 같아요 👇</p>
<table>
<thead>
<tr>
<th>평가 항목</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>성능</td>
<td>Throughput / Latency / CPU 효율</td>
</tr>
<tr>
<td>보안</td>
<td>암호화 수준 / 인증 방식</td>
</tr>
<tr>
<td>운영 난이도</td>
<td>장애 대응, 문서화, 구성 복잡도</td>
</tr>
<tr>
<td>비용</td>
<td>Cloud 측 비용 구조</td>
</tr>
<tr>
<td>쿠버네티스 친화성</td>
<td>Pod-to-Pod 통신 및 Overlay 유지</td>
</tr>
</tbody></table>
<h3 id="31-후보군">3.1 후보군</h3>
<p><strong>1️⃣ Netmaker</strong></p>
<p><strong>제약사항으로 도입 X</strong></p>
<ul>
<li>WireGuard 기반 자동화 Mesh Networking</li>
<li><strong>운영 안정성 이슈</strong> 및 <strong>온프렘 IP 구조 변경 필수</strong></li>
</ul>
<p><strong>2️⃣ WireGuard</strong></p>
<ul>
<li>최신 커널 기반의 고성능 VPN</li>
<li>구성 간결 &amp; 낮은 오버헤드</li>
<li>커뮤니티 중심 문서화 → 내부 인지/학습 필요</li>
</ul>
<p>** 3️⃣ strongSwan (IPsec 기반)**</p>
<ul>
<li>다양한 암호화 옵션 &amp; GCP <strong>Cloud VPN 네이티브 연동</strong></li>
<li>성능 측면에서도 WireGuard와 유사하거나 우세</li>
<li>구성은 다소 복잡하지만 <strong>문서화 및 운영 경험 풍부</strong></li>
</ul>
<h3 id="32-성능-비교-결과">3.2 성능 비교 결과</h3>
<p>테스트 및 레퍼런스 결과를 기반으로 한 비교예요.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>WireGuard</th>
<th>strongSwan</th>
</tr>
</thead>
<tbody><tr>
<td>TCP Throughput</td>
<td>⭕ (901 Mbit/s)</td>
<td>⭕ (906 Mbit/s)</td>
</tr>
<tr>
<td>UDP Throughput</td>
<td>⭕</td>
<td>⭕</td>
</tr>
<tr>
<td>Latency</td>
<td>△</td>
<td>⭕ (낮음)</td>
</tr>
<tr>
<td>CPU 효율</td>
<td>△</td>
<td>⭕</td>
</tr>
<tr>
<td>연결 초기 속도</td>
<td>⭕</td>
<td>△</td>
</tr>
</tbody></table>
<blockquote>
<p>핵심 결론:
<strong>성능 자체는 사실상 동급</strong></p>
</blockquote>
<h3 id="33-운영-및-관리-비교">3.3 운영 및 관리 비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>WireGuard</th>
<th>strongSwan</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>GCP 통합</td>
<td>△ 별도 구성 필요</td>
<td>⭕ GCP Cloud VPN 완전 지원</td>
</tr>
<tr>
<td>보안 정책 유연성</td>
<td>△ 제한적</td>
<td>⭕ 고급 정책 운영 가능</td>
</tr>
</tbody></table>
<blockquote>
<p>핵심 결론:
특히 GCP를 활용하는 Hybrid 구조에서 <strong>Cloud VPN + strongSwan 궁합이 매우 좋음</strong></p>
</blockquote>
<h3 id="34-비용-비교-2tb-기준">3.4 비용 비교 (2TB 기준)</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>WireGuard Gateway VM</th>
<th>Cloud VPN(strongSwan)</th>
</tr>
</thead>
<tbody><tr>
<td>고정비</td>
<td>$80</td>
<td>$36</td>
</tr>
<tr>
<td>데이터 비용</td>
<td>$245.76</td>
<td>$225.28</td>
</tr>
<tr>
<td><strong>총합</strong></td>
<td><strong>$325.76</strong></td>
<td><strong>$261.28</strong></td>
</tr>
</tbody></table>
<blockquote>
<p>핵심 결론:
<strong>Cloud 측 비용은 strongSwan이 더 경제적</strong></p>
</blockquote>
<h3 id="35-종합-결론">3.5 종합 결론</h3>
<blockquote>
<p>성능 / 보안 / 운영 / 비용
모든 측면에서 균형이 가장 좋은 선택은
<strong>strongSwan 기반 S2S VPN 구성</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>후보</th>
<th>최종 판단</th>
<th>사유</th>
</tr>
</thead>
<tbody><tr>
<td>Netmaker</td>
<td>❌ 제외</td>
<td>인프라 변경 필요 + 안정성 리스크</td>
</tr>
<tr>
<td>WireGuard 단독</td>
<td>△ 후보</td>
<td>관리 자동화 부족, 비용 증가</td>
</tr>
<tr>
<td><strong>strongSwan</strong></td>
<td>🏆 <strong>최종 선정</strong></td>
<td>성능·운영·비용 최적 균형</td>
</tr>
</tbody></table>
<p><strong>네트워크 구성도</strong></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/efd4173c-1071-471d-8648-1ba06779c235/image.png" alt=""></p>
<p><strong>On-prem</strong>에서는 <strong>strongSwan을 VPN 게이트웨이</strong>로 구성하고 <strong>Cloud(GCP)</strong> 측은 Managed Cloud VPN(<strong>Cloud VPN</strong>)을 그대로 활용합니다.</p>
<h1 id="4-추후">4. 추후..</h1>
<p>이제 네트워크 연결을 완성했으니 <a href="https://velog.io/@hyeongjun-hub/ClusterAutoscaler%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%98%A8%ED%94%84%EB%A0%88-GCP-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B5%AC%EC%B6%95%EA%B8%B0-2"><strong>on-prem + GCP 하이브리드 클러스터 구축기 [2]</strong></a> 에서는 어떻게 워크로드를 Cloud로 자동으로 확장할지 다뤄보겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DevOps 세미나 후기]]></title>
            <link>https://velog.io/@hyeongjun-hub/DevOps-%EC%84%B8%EB%AF%B8%EB%82%98-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@hyeongjun-hub/DevOps-%EC%84%B8%EB%AF%B8%EB%82%98-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 19 Feb 2025 14:34:15 GMT</pubDate>
            <description><![CDATA[<h1 id="배경">배경</h1>
<p>입사 후 지금까지 1년 반이라는 긴 시간동안 사내 DevOps 플랫폼을 셋이서 열심히 구축하고 고도화 하였습니다. 하지만 최근 여럿 질문들과 피드백을 받으면서 우리가 너무 경주마처럼 앞만 보고 달렸다는 생각이 강하게 들었어요. DevOps의 본질인 소통을 강조하던 저는 개발자 대상으로 DevOps 세미나를 해야한다는 것을 강하게 어필하였습니다. </p>
<p>설날 긴 연휴동안 혼자 편히 쉬지 못했습니다. 그 다음주 금요일이 세미나 발표날이었거든요. 준비 기간동안 열심히 하였으나 기존 DevOps 플랫폼 사용자 뿐만 아니라 DevOps에 관심있는 아래층 개발자, 팀장들도 참석한다는 말에 긴장이 많이 됐습니다. (후덜덜)</p>
<h1 id="발표-주제">발표 주제</h1>
<p>세미나 주제는 크게 두 가지 였습니다.</p>
<p>*<em>1. DevOps란?
*</em>우리가 DevOps Engineer라는 직무를 맡으면서 우리가 뭘 하는지 궁금하신 분들도 있고 DevOps를 회사에 도입하면서 어떤 점이 바뀌었는지 대조해보았습니다.</p>
<p>*<em>2. DevOps 플랫폼 사용방법
*</em>이건 이번 주제에 핵심인데 저희가 구축한 CI/CD 파이프라인이 어떻게 돌아가는지, argocd와 grafana를 이용해 어떻게 애플리케이션을 운영하는지 설명하였습니다.</p>
<h1 id="발표-내용">발표 내용</h1>
<p>내용을 요약해보자면 아래와 같습니다.</p>
<p><strong>1. DevOps란?</strong></p>
<p>아마존, 넷플릭스, 페이스북 같은 글로벌 IT 대기업들이 기업의 공통원칙인 Move Fast를 실천하기 위해 모두 DevOps를 도입하였습니다. </p>
<p>DevOps란 개발과 운영의 벽을 허물고 개발 라이프사이클을 빠르게 돌려 조직의 전체적인 속도를 높이는 것입니다.
이러한 DevOps를 적용하려면 아래와 같은 조건이 있습니다.</p>
<ol>
<li>개발자는 자신이 개발한 것을 빌드, 배포, 운영까지 도맡아해야 하고 </li>
<li>1.을 실천할 수 있도록 충분한 자동화가 이뤄져야 합니다.</li>
</ol>
<p>우리 회사에서 이러한 DevOps를 실천하기 위해 아래 세 가지를 적용하였습니다.</p>
<ol>
<li>CI/CD 파이프라인 구축</li>
<li>쿠버네티스를 통한 운영 자동화</li>
<li>모니터링, 알림 시스템 구축</li>
</ol>
<p>그 결과 제품의 품질이 높아지고 개발 생산성이 향상되며 인프라 비용이 줄어드는 효과를 얻을 수 있었습니다.</p>
<p><strong>2. DevOps 플랫폼 사용방법</strong></p>
<p>CI/CD 파이프라인에서 commit id를 통해 배포 버전을 관리하는 방법과 쿠버네티스의 HealthCheck Probe들을 이론적으로 설명하였고 그 후엔 아래 순서와 같이 시연을 진행하였습니다.</p>
<ol>
<li>Grafana 대시보드를 이용해 애플리케이션 메트릭, 로그를 확인하는 방법</li>
<li>애플리케이션 장애가 발생하였을 때의 다섯 가지 행동 매뉴얼</li>
</ol>
<h1 id="결과">결과</h1>
<p>세미나 결과는 성공적이었습니다. 참석자들의 반응도 좋았고 QnA에서 질문이 굉장히 많이 나왔어요. 배포 횟수의 변화 그래프를 보여줄 때 놀라는 반응도 많았습니다. 그리고 세미나 끝나고 자리에 돌아오자마자 아래층 사람들의 협업 요청이 들어왔습니다. </p>
<p>저희 DevOps의 가치를 사내에 알린 것 같아 기분이 좋았습니다.</p>
<h1 id="발표-사진">발표 사진</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/f5fa4b86-50e3-445e-bc47-6044f3d5b277/image.jpeg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/f2369ea4-b6f6-4706-a27d-67ff66192049/image.jpeg" alt=""></p>
<h1 id="후기">후기</h1>
<p>세미나를 처음 시작하려는데 가장 큰 대회의실에 사람이 앉을 자리가 없을 정도로 참석자가 많았습니다. (결국 몇몇 분들은 zoom을 통해 화상으로 발표를 들으셨어요). 이 정도로 DevOps에 관심 있으신 분들이 많으실 줄 몰랐고 긴장이 배로 됐던 것 같습니다. 그리고 회의실이 너무 더운데 목관리를 위해 따뜻한 물을 준비해놔서 너무! 더웠습니다. (땀이 정말 많이 났어요). 처음엔 제일 앞에 앉아 계셨던 이사님 얼굴만 보였는데 2부 시작하면서 긴장이 좀 풀렸던 것 같아요. 같이 점심먹는 동료 얼굴이 보이기 시작하더라구요. 준비한 재밌는 멘트들도 반응이 좋았습니다.</p>
<p>이번 세미나는 저에겐 정말 좋은 경험이었습니다. 이렇게 많은 사람들 앞에서 발표하는 것이 정말 오랜만이었는데 반복적인 업무 속에서 색다른 자극이었어요. 많은 사람들이 저의 가치를 알아주니 일할 맛도 생기는 것 같습니다. 앞으로 더 열심히 노력해서 회사와 함께 발전하는 DevOps 엔지니어가 되겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파드 접속자 IP 확인하기 (프록시 프로토콜)]]></title>
            <link>https://velog.io/@hyeongjun-hub/%ED%8C%8C%EB%93%9C-%EC%A0%91%EC%86%8D%EC%9E%90-IP-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0-proxy-protocol</link>
            <guid>https://velog.io/@hyeongjun-hub/%ED%8C%8C%EB%93%9C-%EC%A0%91%EC%86%8D%EC%9E%90-IP-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0-proxy-protocol</guid>
            <pubDate>Mon, 16 Dec 2024 13:45:35 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<p>쿠버네티스의 파드로 통하는 트래픽의 클라이언트 IP를 확인하고 싶은 개발자 파트로부터의 요구사항이 발생했습니다. 현재 Istio access log에는 요청의 X-Forwarded-For 헤더가 로깅되도록 설정되어있는데 실제로 확인해보면 172로 시작하는 알 수 없는 IP가 담겨져 있습니다. 
<a href="https://istio.io/latest/docs/tasks/observability/logs/access-log/#default-access-log-format">istio access log 확인 방법
</a><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/2acfaca6-ab47-4fdd-a7eb-127ee5e53384/image.png" alt=""></p>
<p>이것은 pod가 띄워져있는 노드의 calico ip로 원하는 값이 아닙니다.</p>
<p>클라이언트의 ip를 얻는 방법은 무엇일까요?</p>
<h1 id="해결-방법">해결 방법</h1>
<h3 id="쿠버네티스의-externaltrafficpolicy">쿠버네티스의 externalTrafficPolicy</h3>
<p>쿠버네티스는 <a href="https://kubernetes.io/ko/docs/tutorials/services/source-ip/">externalTrafficPolicy</a>를 통해 client IP를 확인하는 방법을 지원합니다. externalTrafficPolicy가 기존값은 Cluster인데 Local로 수정하는 것이죠. 하지만 이 방법에는 큰 문제점이 있습니다.</p>
<p>externalTrafficPolicy를 Local로 설정한다면 클러스터 내에서 다른 노드로 트래픽이 이동할 수 없습니다. 예를 들어, 원래 NodePort 타입으로 서비스를 배포한다면 기존에는 클러스터의 아무 노드의 지정 포트번호로 접속하면 정상적으로 접근할 수 있었습니다. 하지만 externalTrafficPolicy를 Local로 설정하면 해당 pod가 떠있는 노드의 IP로만 서비스로 접근 가능합니다.</p>
<p>만약 Cloud의 로드밸런서를 사용한다면 알아서 노드를 찾아가므로 문제가 없겠지만 저처럼 온프레미스의 MetalLB L2 모드를 사용하는 경우에는 문제가 됩니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/1210b755-b9ea-4bbd-81b9-ba67558570e4/image.png" alt=""></p>
<p>(출처: <a href="https://velog.io/@youwins/MetalLB">https://velog.io/@youwins/MetalLB</a>)</p>
<p>위의 그림처럼 MetalLB는 여러 speaker pod 중 하나의 leader pod를 선정해 leader pod가 존재하는 노드로 external IP의 모든 트래픽이 들어오게 되므로 다른 노드로 트래픽이 이동하지 못하면 서비스가 작동하지 않는 참사가 발생합니다.</p>
<p>따라서 다른 방법을 찾아야 했습니다.</p>
<h3 id="프록시-프로토콜">프록시 프로토콜?</h3>
<p>저는 클러스터에 들어오는 요청이 모두 HAProxy를 통과하므로 HAProxy를 활용할 수 있는 방안을 생각했습니다. 그러다가 <a href="https://kubito.dev/posts/preserve-istio-source-ip-haproxy-reverse-proxy/">흥미로운 글</a>을 발견했습니다. HAProxy와 Istio ingress gateway에서 프록시 프로토콜을 적용하는 글입니다.</p>
<p>프록시 프로토콜은 <strong>클라이언트의 TCP 연결이 프록시를 통과할 때 클라이언트의 IP 주소를 보존하기 위한 네트워크 프로토콜</strong>입니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/9b94bc69-3112-48bc-9630-589a86d307b6/image.png" alt=""></p>
<p>(출처: <a href="https://docs.hivemq.com/hivemq/latest/user-guide/proxy-protocol.html">https://docs.hivemq.com/hivemq/latest/user-guide/proxy-protocol.html</a>)</p>
<p>중요한 점은 프록시 프로토콜 헤더를 보내면 받는 컴포넌트가 프록시 프로토콜을 지원하는 서비스여야 한다는 것입니다.</p>
<p>프록시 프로토콜의 지원 서비스는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/45128b89-f5db-49bb-9383-7685ef492311/image.png" alt=""></p>
<p>적용 방법도 굉장히 간단합니다.</p>
<ul>
<li><p>HAProxy 
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/3bfcf111-0cf4-4901-9258-8497cdbe949e/image.png" alt=""></p>
</li>
<li><p>Envoy Proxy</p>
<pre><code class="language-yaml">apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: proxy-protocol
  namespace: testbed
spec:
    workloadSelector:
    labels:
      app: test-ingressgateway 
  configPatches:
  - applyTo: LISTENER
    patch:
      operation: MERGE
      value:
        listener_filters:
        - name: envoy.filters.listener.proxy_protocol
          typed_config: 
            &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol
        - name: envoy.filters.listener.tls_inspector
          typed_config:
            &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector</code></pre>
<h1 id="테스트">테스트</h1>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6512429d-64d4-42e0-bf69-3907101aaea2/image.png" alt=""></p>
<p>HAProxy와 Istio ingress gateway에 프록시 프로토콜을 적용하고 트래픽을 발생시켜 보았습니다.</p>
<ul>
<li><p>istio gateway access log
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/895339a4-18b9-446d-803a-af92633fd17d/image.png" alt=""></p>
<ul>
<li>pod istio-proxy access log
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/7d1598c1-d879-4569-a5e2-307674a4dd93/image.png" alt=""></li>
</ul>
</li>
</ul>
<p>정상적으로 접속한 IP를 로깅하는 것을 알 수 있습니다. </p>
<h1 id="느낀점">느낀점</h1>
<p>HAProxy를 활용한 해결책을 찾는 과정에서, 기존에 당연하게 여겼던 인프라 구성요소들을 새로운 시각으로 바라보게 되었습니다. 이를 통해 기존 인프라의 수정없이 요구사항을 충족할 수 있었습니다. 이는 다른 문제에 직면했을 때도 현재 가지고 있는 자원들을 창의적으로 활용할 순 없는지를 바라보는 시각을 얻었습니다.</p>
<p>프록시 프로토콜에 대해 학습하면서, 네트워크 통신의 복잡성과 각 계층에서 일어나는 일들에 대해 더 깊이 이해하게 되었습니다. 이는 앞으로 네트워크 관련 문제를 해결할 때 큰 자산이 될 것 같습니다.</p>
<p>개발자분들이 요구사항이 해결된 인프라를 신기해하고 이에 대해 커뮤니케이션을 하는 것이 즐거웠습니다. 예전부터 속을 썩이던 문제여서 해결한 것에 대해 뿌듯함을 많이 느꼈습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CPU 폭주! go루틴에서 범인찾기]]></title>
            <link>https://velog.io/@hyeongjun-hub/CPU-%ED%8F%AD%EC%A3%BC-go%EB%A3%A8%ED%8B%B4%EC%97%90%EC%84%9C-%EB%B2%94%EC%9D%B8%EC%B0%BE%EA%B8%B0</link>
            <guid>https://velog.io/@hyeongjun-hub/CPU-%ED%8F%AD%EC%A3%BC-go%EB%A3%A8%ED%8B%B4%EC%97%90%EC%84%9C-%EB%B2%94%EC%9D%B8%EC%B0%BE%EA%B8%B0</guid>
            <pubDate>Fri, 30 Aug 2024 01:48:38 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<p>사내 플랫폼의 API Server를 모니터링하던 도중에 이상한 점을 발견하였습니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6515328d-5ac2-4740-9b71-6dc2df18801a/image.png" alt=""></p>
<p>API Server의 CPU가 25코어 남짓을 잡아먹고 있던 것이었습니다. </p>
<p>실제로 확인해보니 API Server 기능 중 로그 모니터링을 사용하면 CPU 사용률이 비정상적으로 높아지는 것을 찾을 수 있었고, 로그 모니터링 기능 중 websocket을 사용한 부분에 문제가 있다고 판단했습니다.</p>
<p>특이점은 실행중인 pod의 로그를 가져올 때는 문제가 없는데 completed나 failed된 파드의 로그를 가져올 때 cpu가 올라간다는 점이었습니다. </p>
<h3 id="현재-로직">현재 로직</h3>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/579991c3-0f06-42ab-a84c-cc7dd6af82fb/image.png" alt=""></p>
<p>두 개의 반복문이 있는데 아래와 같습니다.</p>
<ul>
<li><strong>메인 go루틴</strong> : 로그 채널 생성 후 로그 채널에 들어온 로그를 클라이언트로 보내도록 반복</li>
<li><strong>go루틴</strong> : 컨테이너로 부터 로그 채널로 로그 전송하도록 반복</li>
</ul>
<h1 id="원인-파악">원인 파악</h1>
<p>문제가 발생하는 원인에 대해 가장 높은 가능성순으로 나열한 뒤 소거법으로 문제를 해결했습니다. </p>
<ol>
<li>go루틴 내 반복문 무한루프</li>
<li>메인 go루틴 내 반복문 무한루프</li>
<li>websocket 로직의 문제</li>
</ol>
<h1 id="테스트-및-문제-해결-과정">테스트 및 문제 해결 과정</h1>
<h3 id="1-go루틴-내-반복문의-무한루프">1. go루틴 내 반복문의 무한루프</h3>
<p>먼저 go루틴 반복문에 log를 찍어봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/1fc94505-68fd-47f9-93b0-d38b8c86d875/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/1292b91d-ed95-48a8-8520-56abbabf3f29/image.png" alt=""></p>
<p>예상대로 무한루프가 돌고 있었습니다. </p>
<p>그 원인은 <strong>&quot;break</strong>&quot; 에 있었습니다. </p>
<p>completed 되었거나 failed된 pod의 로그는 EOF를 만나게 됩니다. </p>
<p>따라서 EOF를 만나면 반복문을 탈출해야하고 break가 그 역할을 수행할 줄 알았습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/21474ce0-ad75-43c1-ad95-d3b246a93d7e/image.png" alt=""></p>
<p>하지만 원하는대로 동작하지 않았고 반복문이 아닌 select문의 default 블록을 탈출하였습니다</p>
<p>그래서 break가 아닌 return을 적용하여 해결하였습니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/a782a9e6-4cae-469b-9793-5d7a5ec1346f/image.png" alt=""></p>
<p>이대로 문제가 해결된줄 알았지만 CPU 사용률은 여전히 높았습니다.</p>
<h3 id="2-main-go루틴-내-반복문-무한루프">2. main go루틴 내 반복문 무한루프</h3>
<p>테스트 결과 main go루틴의 반복문에도 무한루프가 돌고 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/c9f60bcb-6f5f-4e6c-a1f9-56196213e57d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/15f94d24-2413-4850-b275-58ab8c921200/image.png" alt=""></p>
<p>그 결과 go루틴이 무한대로 생성되었고 CPU 점유율이 기하급수적으로 상승하였습니다.</p>
<p>그 원인은 websocket의 connection을 제때 끊어주지 않음에 있었습니다.</p>
<p>EOF를 만나 go루틴이 끝나면 채널에는 빈 msg가 들어오게 됩니다.</p>
<p>빈 msg가 들어오면 websocket connection을 종료하도록 로직을 수정하였습니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/28375ec3-f316-48d6-ae3f-8abeec27facf/image.png" alt=""></p>
<h1 id="최종-로직과-결과">최종 로직과 결과</h1>
<p>최종 로직은 아래와 같이 수정되었고 그 결과 CPU 사용률이 1코어 내로 줄어들었습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/3c401e60-0e49-4a71-aca5-8d50b58008db/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/20449b0a-d0e8-4d32-ae10-a5f5a7ed5a52/image.png" alt=""></p>
<h1 id="방지-대책">방지 대책</h1>
<p>테스트를 통해 해결은 했지만 다음에 이런상황이 오는 것을 방지하기 위한 대책을 세워야 겠다고 판단했습니다. 
Uber에 <a href="https://github.com/uber-go/goleak">goleak</a>이라는 leak을 detect해주는 패키지가 있습니다. </p>
<p>해당 패키지로 테스트 케이스를 짜면 leak을 사전에 테스트 해볼 수 있습니다. </p>
<h1 id="느낀점">느낀점</h1>
<p>이번 문제를 해결하면서 몇 가지 중요한 교훈을 얻을 수 있었습니다.</p>
<h3 id="1-비동기-처리의-복잡성">1. 비동기 처리의 복잡성</h3>
<p>go루틴이 경량 스레드라 가볍다고 생각해서 별다른 관리 없이 그냥 방치했는데, 특히 WebSocket처럼 지속적인 연결이 있는 상황에서는 애플리케이션 전체 성능을 저하시킬 수 있다는 사실을 깨달았습니다.</p>
<h3 id="2-select문에서-잘-탈출하기">2. select문에서 잘 탈출하기!</h3>
<p>break가 반복문만 탈출하는 게 아니라 select문에서도 탈출할 수 있기 때문에 주의가 필요합니다. 이 작은 차이가 이번에 애플리케이션 전체에 영향을 주는 결과를 초래했기 때문에 중요한 포인트였습니다.</p>
<h3 id="3-websocket과-채널-관리">3. WebSocket과 채널 관리</h3>
<p>WebSocket과 채널이 결합된 비동기 로직에서는 연결 상태를 지속적으로 확인하고, 로그 스트림이 종료된 후 WebSocket 연결을 어떻게 종료할지도 관리가 필요합니다. 그렇지 않으면 자원 누수가 발생하기 쉽고, 이는 성능 저하로 이어질 수 있기 때문에 설계와 구현에 신중해야겠다고 느꼈습니다.</p>
<h3 id="4-지속적인-모니터링의-중요성">4. 지속적인 모니터링의 중요성</h3>
<p>마지막으로, 시스템 모니터링의 중요성도 크게 느꼈습니다. 이번 문제가 발생한 배경도 그라파나를 통해 CPU 사용량이 비정상적으로 높은 것을 발견하면서 시작되었거든요. 만약 모니터링이 없었다면 문제를 인지하지 못했을 수도 있고, 성능 이슈로 사내 개발자들의 관심을 끌지 못했을 수도 있었을 겁니다! 결국 시스템을 안정적으로 운영하려면 실시간으로 시스템 상태를 모니터링하고, 이상 증상을 빠르게 파악하는 것이 필수적이라는 교훈을 얻었습니다.</p>
<h3 id="결론">결론</h3>
<p>이번 이슈가 종료된 pod의 로그에서만 발생했기 때문에, 해당 pod의 로그만 WebSocket을 사용하지 않는 방법도 고려할 수 있었습니다. 그러나 이런 <strong>회피책</strong>은 <strong>실력 향상에 도움이 되지 않을 뿐만 아니라 좋은 해결책이라고 생각하지 않습니다.</strong> 시간적 여유가 없는 상황에서 최후의 수단으로 삼아야 할 방법입니다. </p>
<p>문제의 근본 원인을 해결하는 과정에서 보람을 느꼈으며, 앞으로도 이러한 도전을 통해 제 개발 역량을 발전시킬 것입니다.</p>
<p>긴 글 읽어주셔서 감사합니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스 네트워크 문제 트러블슈팅]]></title>
            <link>https://velog.io/@hyeongjun-hub/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%AC%B8%EC%A0%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@hyeongjun-hub/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%AC%B8%EC%A0%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Thu, 22 Aug 2024 08:02:40 GMT</pubDate>
            <description><![CDATA[<p>Production 클러스터에서 p99 지연 4초 반복 발생으로 2주간 총력 대응한 사례입니다. DevOps 팀 3명이 모든 업무를 멈추고 집중한 &quot;지옥의 트러블슈팅&quot; 기록입니다.</p>
<h1 id="문제-발생-production-성능-이슈">문제 발생: Production 성능 이슈</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e9271cf8-f683-44ce-a71a-5500c6eb6179/image.png" alt=""></p>
<p>24년 7월 10일 경에 production 클러스터 앱의 성능 이슈가 발생한다는 문의가 들어왔습니다. 이날부터 2주동안, 저를 포함한 DevOps Engineer 3명은 모든 업무를 멈추고 네트워크 이슈 원인 찾기에 돌입하였습니다.</p>
<h1 id="1-문제-재현을-위한-부하테스트">1. 문제 재현을 위한 부하테스트</h1>
<p>문제 재현을 위해 k6, Grafana를 이용해 부하테스트를 진행하였습니다.</p>
<p>Grafana로 확인해보니 정말로 특정 노드로 통하는 외부망에서 request duration이 1분 간격으로 4초간 지연이 발생하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/2db8f9a4-8937-44c1-96b6-275f57f4fb5b/image.png" alt=""></p>
<blockquote>
<p>조사 결과: 외부 → 클러스터 특정 노드 경로에서만 발생</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e1bf515a-0e98-4d8e-ae4c-0ee1f47105fd/image.png" alt=""></p>
<h1 id="2-원인-분석-metallb-layer2-leader-election-문제">2. 원인 분석: MetalLB Layer2 Leader Election 문제</h1>
<p>현재 클러스터는 MetalLB Layer2 모드 운영 중:</p>
<pre><code class="language-text">- Leader Pod가 있는 노드(Leader Node)가 모든 LB 트래픽 수신
- Leader 선출/전환 시 네트워크 불안정 발생</code></pre>
<p>MetalLB Leader Node 확인 과정에서 충격적인 현상을 발견했습니다.
leader node를 확인하기 위해 LB의 ip로 arping을 날리면 하나의 MAC address가 아닌 *<em>다수의 MAC address가 응답을 하였습니다. *</em></p>
<pre><code class="language-bash">$ arping -I eth0 &lt;LB_VIP&gt;
예상: 단일 Leader Node MAC
실제: 5개 노드의 MAC 주소 동시 응답 😱</code></pre>
<p>tcpdump로 패킷 캡처 결과:</p>
<pre><code class="language-text">GARP 스톰 + Leader 전환 중 ARP 테이블 혼란
src MAC이 LB_VIP에 대해 계속 바뀜</code></pre>
<p>Reddit에서 동일 증상 확인</p>
<p><a href="https://www.reddit.com/r/vmware/comments/y68emx/metallb_vmware_multiple_mac_address_associated/">https://www.reddit.com/r/vmware/comments/y68emx/metallb_vmware_multiple_mac_address_associated/</a></p>
<h1 id="3-근본-원인-bitnami-metallb-helm-chart-버그">3. 근본 원인: Bitnami MetalLB Helm Chart 버그</h1>
<p>이것은 저희가 클러스터에 배포한 bitnami의 metalLB 차트 이상으로 확인되었습니다. </p>
<blockquote>
<p>참고: MetalLB Layer2 mode 동작 원리</p>
</blockquote>
<pre><code class="language-text">1. Leader Speaker가 VIP를 ARP로 광고 (단일 MAC)
2. Memberlist Gossip(TCP/UDP 7946)으로 Speaker들끼리 Leader 선출 조율
3. 비-Leader Speaker는 VIP 광고 중단</code></pre>
<p>Bitnami 차트에서 발견된 문제: 여러 Speaker가 동시에 VIP를 광고 → Split-Brain</p>
<p><strong>근본 원인: NetworkPolicy가 Memberlist 차단</strong></p>
<p>문제점은 Speaker 간 Gossip 포트(7946) 미허용하고 있었습니다.</p>
<pre><code class="language-yaml"># Bitnami 차트 기존 networkpolicy.yaml
spec:
  networkPolicy:
    enabled: true
  egress:
    - ports:  # DNS, API Server만 허용
        - port: 53  # ❌ 7946/TCP+UDP 누락!</code></pre>
<p>Memberlist Gossip 차단으로 인한 결과:</p>
<pre><code class="language-text">- 각 Speaker가 &quot;내가 Leader&quot;로 오인
- 모든 노드에서 VIP ARP 광고 지속
- arping → 다중 MAC 응답 </code></pre>
<h1 id="4-문제-해결">4. 문제 해결</h1>
<p>metallb 공식차트는 7946 포트에서 memberlist를 잘 쓰도록 설계되어 있습니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/7a12db64-bdc0-409d-9247-8f09cde37018/image.png" alt=""></p>
<p>metallb의 차트를 bitnami 에서 배포한 차트에서 공식 차트로 수정하였고 정상적으로 하나의 MAC address가 찍히는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/bf016200-1fe0-4485-ae54-a1ad9e11c73a/image.png" alt=""></p>
<p>이후 네트워크 이슈가 해결된 것을 부하테스트 결과로 확인할 수 있었습니다. (req_duration이 1s 미만) </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/c56224f4-0591-4a97-ae36-451724f5f2d2/image.png" alt=""></p>
<h1 id="5-추가-최적화-모델-다운로드-대역폭-제한">5. 추가 최적화: 모델 다운로드 대역폭 제한</h1>
<p>추가로 ai 개발자분들이 모델을 다운로드 받는 시간동안 네트워크 성능이 많이 떨어지는 현상을 확인해 해당 deployment(object storage)에 calico 대역폭 제한을 설정하였습니다. </p>
<pre><code class="language-yaml">metadata:
  annotations:
    kubernetes.io/ingress-bandwidth: 512M
    kubernetes.io/egress-bandwidth: 512M</code></pre>
<p>이후 네트워크 대역폭이 안정적으로 유지되었고 성능 이슈가 발생하지 않았습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/449d43d5-b2bf-41d6-ab87-1016604611d9/image.png" alt=""></p>
<h1 id="배운점">배운점</h1>
<h3 id="1-리눅스의-네트워크-툴과-명령어">1. 리눅스의 네트워크 툴과 명령어</h3>
<p><code>$ arping</code></p>
<ul>
<li>arping을 날려 해당 ip가 가진 MAC address 확인</li>
</ul>
<p><code>$ tcp_dump</code></p>
<ul>
<li>네트워크 인터페이스로 들어오는 패킷들을 캡쳐</li>
</ul>
<p><code>$ mtr (my traceroute)</code></p>
<ul>
<li>ping + traceroute</li>
<li>ip로 통신하기 위해 어떤 경로를 사용하는 지 확인할 수 있었음</li>
</ul>
<p><code>$ ip neigh show</code></p>
<ul>
<li>해당 노드가 주변 노드들에 대해 가지는 neighbor ip + MAC address 확인</li>
</ul>
<p><code>$ ip route show</code></p>
<ul>
<li>해당 노드가 가진 라우팅 테이블을 확인, 목적지 ip가 어떤 인터페이스를 통해 전달되는지<h3 id="2-grafana-k6-사용법-숙지">2. grafana k6 사용법 숙지</h3>
테스트는 grafana + influxdb 로 모니터링을 진행하였고 
<a href="https://grafana.com/docs/k6/latest/">grafana k6 공식 문서</a>를 살펴보면서 여러가지 테스트 가이드가 있는 걸 확인하였습니다.<br>그 중에서 최소한의 부하에서도 적절하게 작동하는지 확인하기 위해 <a href="https://grafana.com/docs/k6/latest/testing-guides/test-types/smoke-testing/">smoke test</a>를 진행하였습니다.</li>
</ul>
<h3 id="3-bitnami-helm-차트는-웬만해선-쓰지-말고-공식-helm-차트를-사용하는-것이-좋다">3. &quot;bitnami helm 차트는 웬만해선 쓰지 말고 공식 helm 차트를 사용하는 것이 좋다&quot;</h3>
<p>처음엔 bitnami의 helm chart가 공식 helm chart를 가져온 것인줄로만 알았는데 이번 기회를 통해 전혀 아님을 확인했습니다. 
다행히 현재 클러스터엔 metallb만 bitnami를 사용하는 중이었습니다. </p>
<h3 id="4-calico의-대역폭-제한">4. calico의 대역폭 제한</h3>
<p><a href="https://kubernetes.io/ko/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#%ED%8A%B8%EB%9E%98%ED%94%BD-%EC%85%B0%EC%9D%B4%ED%95%91-shaping-%EC%A7%80%EC%9B%90">쿠버네티스 트래픽 셰이핑 지원</a>을 통해 CNI 플러그인에서 파드 수신 및 송신 트래픽 셰이핑을 지원하는 것을 알게 되었고 이를 클러스터에 적용하였습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes의 여러 Probe들]]></title>
            <link>https://velog.io/@hyeongjun-hub/Kubernetes%EC%9D%98-%EC%97%AC%EB%9F%AC-Probe%EB%93%A4</link>
            <guid>https://velog.io/@hyeongjun-hub/Kubernetes%EC%9D%98-%EC%97%AC%EB%9F%AC-Probe%EB%93%A4</guid>
            <pubDate>Sun, 14 Apr 2024 08:52:05 GMT</pubDate>
            <description><![CDATA[<h1 id="이-글을-쓰게-된-계기">이 글을 쓰게 된 계기</h1>
<p>저희 DevOps팀에 하나의 문의사항이 왔습니다. </p>
<p>&quot;원래 새 버전 파드가 <strong>정상이어야</strong> 전 버전의 파드가 삭제되는 것 아닌가요? <strong>새로운 버전의 파드가 비정상인데 이전 버전의 파드가 지워져서</strong> 서버가 내려갔습니다.&quot;</p>
<p>그래서 저는 probe를 둬야 할 것 같다고 답변했으나 정확히 어떤 probe를 둬야할지 바로 떠오르지 않았습니다. 그래서 이번 기회에 probe에 대해 공부하고 확실히 내 것으로 만들자는 생각이 들었습니다. </p>
<h1 id="pod의-상태">Pod의 상태</h1>
<p>Ready와 Running은 <code>kubectl get</code> 명령어를 치면 쉽게 볼 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/4a470fdc-7387-4d44-a71c-7180ca5342dd/image.png" alt=""></p>
<p>혹시 Pod의 Ready상태와 Running상태의 차이점을 알고계신가요? Ready 다음이 Running이라고 생각하시는 분은 없으시겠죠? </p>
<p>일단 Ready와 Running은 같은 범주에 있는 상태값은 아닙니다.</p>
<h3 id="ready">Ready</h3>
<p>Ready에 해당하는 범주는 <strong>파드의 컨디션</strong>(상태)입니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/ebc13ec8-a273-4885-b62e-11f3cb080373/image.png" alt=""></p>
<p>각 컨디션의 자세한 정보는 <a href="https://kubernetes.io/ko/docs/concepts/workloads/pods/pod-lifecycle/?source=post_page-----7872cec9e568--------------------------------#%ED%8C%8C%EB%93%9C%EC%9D%98-%EC%BB%A8%EB%94%94%EC%85%98">파드의 컨디션</a>에서 확인 가능합니다. </p>
<p>컨디션을 결정하는 방법은 kubelet이 직접 검사해야 합니다. 이는 아래의 container probe에서 자세히 다루겠습니다. </p>
<h3 id="running">Running</h3>
<p>Running은 <strong>파드의 단계</strong>를 나타내는 값입니다. 파드의 라이프사이클 중에서 어디 쯤에 있느냐를 나타내는 값이죠</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/a23cde44-91a6-4fcd-b652-66c45166cc43/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/5f0bf907-b574-4d17-a4bc-e3996eb93df7/image.png" alt=""></p>
<p>각 단계의 정확한 정보는 <a href="https://kubernetes.io/ko/docs/concepts/workloads/pods/pod-lifecycle/?source=post_page-----7872cec9e568--------------------------------#%ED%8C%8C%EB%93%9C%EC%9D%98-%EB%8B%A8%EA%B3%84">파드의 단계(phase)</a>에서 확인 가능합니다.</p>
<h3 id="ready-와-running">Ready 와 Running</h3>
<p><code>Ready</code> 상태가 나타내는 정보는 조건을 검사하여 <strong>파드가 사용할 수 있는지</strong>입니다.</p>
<p><code>Running</code> 단계가 나타내는 정보는 <strong>컨테이너를 실행했다</strong>는 것만 담고 있습니다. </p>
<p>위의 정보로 미루어보아 <code>Running</code> 단계이지만 <code>Ready</code> 상태가 아니면 파드(정확히는 컨테이너)가 정상이 아니라는 말이겠죠. </p>
<h1 id="container-probe">Container Probe</h1>
<h3 id="ready가-되려면-어떤-검사를-해야할까">Ready가 되려면 어떤 검사를 해야할까?</h3>
<p>자 이제 Probe 얘기를 해봅시다. 위에서 Ready 상태가 되려면 필요한게 kubelet에 의한 검사라했는데 여기서 말하는 검사가 Container Probe 입니다. Container Probe에 설정된 값에 따라 kubelet은 주기적으로 진단을 수행합니다. </p>
<h3 id="probe의-종류">Probe의 종류</h3>
<p>kubelet은 실행 중인 컨테이너들에 대해 선택적으로 세 가지 종류의 프로브를 수행하고 그에 반응할 수 있습니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/98edaa95-4930-4738-a5a2-97e9adaf1ce4/image.png" alt=""></p>
<ul>
<li><strong>LivenessProbe(활성 프로브)</strong><ul>
<li>컨테이너가 살아있는지를 나타냅니다. </li>
<li>만약 활성 프로브가 실패하면 kubelet은 해당 컨테이너를 재시작정책에 맞게 재시작합니다.</li>
</ul>
</li>
<li><strong>ReadinessProbe(준비성 프로브)</strong><ul>
<li>컨테이너가 요청을 처리할 준비가 됐는지를 나타냅니다.</li>
<li>만약 준비성 프로브가 실패하면 파드에 연관된 모든 서비스들의 엔드포인트에서 파드를 제외합니다. </li>
</ul>
</li>
<li><strong>StartupProbe(스타트업 프로브)</strong><ul>
<li>컨테이너 내의 애플리케이션이 시작되었는지를 나타냅니다.</li>
<li>스타트업 프로브가 주어진 경우엔 성공할 때 까지 다른 프로브들은 활성화되지 않습니다.</li>
<li>이외에는 활성 프로브와 동일합니다.</li>
</ul>
</li>
</ul>
<h3 id="probe의-옵션">Probe의 옵션</h3>
<p>예시와 주석으로 살펴봅시다</p>
<pre><code class="language-yaml">    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 30 # 첫 번째 프로브를 실행하기 전에 주는 Delay (default 0)
      periodSeconds: 10 # kubelet이 얼마나 자주 진단을 할 지 나타냄 (default 10)
      failureThreshold: 3 # 연속으로 몇 번 실패 시 실패인지 (default 3)
      successThreshold: 1 # 연속으로 몇 번 성공 시 성공인지 (default 1)</code></pre>
<p>자세한 내용은 <a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes">Configure Probe</a>에서 확인 가능합니다. </p>
<h1 id="결론">결론</h1>
<p>세 가지 Probe들의 기본값 (설정하지 않았을 때의 값)은 Success입니다. 즉 <strong>Probe를 설정하지 않으면</strong> 검사하지않고 Ready 상태를 나타내며 <strong>정상적이지 않은 파드로 교체될 수 있다</strong>는 뜻입니다. 결국 처음 문의받은 정상적이지 않은 파드로 교체된 이유는 <strong>Probe를 설정하지 않아서</strong>라고 할 수 있고 설정해야하는 Probe는 <code>ReadinessProbe</code>입니다. </p>
<p>그 이유는 업데이트한 새 버전의 파드에 <code>LivenessProbe</code>나 <code>StartupProbe</code>가 설정되어 있었다면 이전 버전의 파드는 교체되고 새 파드의 Probe가 실패한 이후 <strong>계속 파드가 재시작</strong>되었을 것이기 때문입니다. <code>ReadinessProbe</code>를 줌으로써 Deployment의 ReaplicaSet이 파드가 Ready 상태가 되지 않은 것을 확인하고 새 파드로 교체하지 않을 것입니다. </p>
<blockquote>
<p>livenessProbe만 설정한 파드다
파드는 교체되지만 probe는 실패하고 이후에 계속 restart를 반복한다
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/0c9d03e3-4507-4766-a512-39e95e7a06a0/image.png" alt=""></p>
</blockquote>
<p>오늘은 Pod의 상태와 Container Probe에 대해 알아봤습니다. 많은 도움이 되었기 바랍니다. 감사합니다!</p>
<hr>
<h3 id="출처">출처</h3>
<p><a href="https://kubernetes.io/ko/docs/concepts/workloads/pods/pod-lifecycle">https://kubernetes.io/ko/docs/concepts/workloads/pods/pod-lifecycle</a>
<a href="https://malwareanalysis.tistory.com/637">https://malwareanalysis.tistory.com/637</a>
<a href="https://cwal.tistory.com/30">https://cwal.tistory.com/30</a>
<a href="https://www.openmaru.io/kubernetes-%EC%97%90%EC%84%9C-pod-%EC%97%90-%EB%8C%80%ED%95%9C-%ED%97%AC%EC%8A%A4%EC%B2%B4%ED%81%AC-probe/">https://www.openmaru.io/kubernetes-%EC%97%90%EC%84%9C-pod-%EC%97%90-%EB%8C%80%ED%95%9C-%ED%97%AC%EC%8A%A4%EC%B2%B4%ED%81%AC-probe/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테라폼 상태와 격리]]></title>
            <link>https://velog.io/@hyeongjun-hub/%ED%85%8C%EB%9D%BC%ED%8F%BC-%EC%83%81%ED%83%9C%EC%99%80-%EA%B2%A9%EB%A6%AC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%AA%A8%EB%93%88%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@hyeongjun-hub/%ED%85%8C%EB%9D%BC%ED%8F%BC-%EC%83%81%ED%83%9C%EC%99%80-%EA%B2%A9%EB%A6%AC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%AA%A8%EB%93%88%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Fri, 26 May 2023 10:35:51 GMT</pubDate>
            <description><![CDATA[<p>이번 포스트는 요즘 제가 공부하고 있는 테라폼에 대해 다뤄보려 합니다. 테라폼은 현시점 가장 강력한 IaC 도구로 자리매김 하였죠. 오늘은 현업에서 테라폼을 사용할 때 환경을 어떻게 격리하는지, 모듈화는 어떻게 하는지 알아보겠습니다.</p>
<h1 id="목차">목차</h1>
<ul>
<li>테라폼 상태란?</li>
<li>상태 파일을 공유하기<ul>
<li>원격 백엔드</li>
<li>S3를 원격 백엔드로 사용</li>
<li>테라폼 백엔드의 단점</li>
</ul>
</li>
<li>격리<ul>
<li>테라폼 작업 공간을 통한 격리<ul>
<li>테라폼 작업 공간의 단점</li>
</ul>
</li>
<li>파일 레이아웃을 이용한 격리<ul>
<li>파일 레이아웃을 이용한 격리의 단점</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="테라폼-상태란">테라폼 상태란?</h1>
<p>테라폼을 실행할 때마다 테라폼은 생성한 인프라에 대한 정보를 <strong>테라폼 상태 파일</strong>에 기록합니다.
이 파일에는 구성파일(.tf)의 테라폼 리소스가 실제 리소스의 표현으로 매핑되는 내용을 기록하는 사용자 정의 JSON 형식이 포함되어 있습니다.</p>
<p>terraform.tfstate의 예시</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/925219af-3f1c-4003-8133-a40040c8823c/image.png" alt=""></p>
<p>테라폼을 실행할 때마다 상태 파일의 ID를 통해 발견된 컴퓨터의 코드와 실제 세계에 배포된 인프라 간의 차이를 확인합니다. (<code>terraform plan</code>)</p>
<blockquote>
<p>상태파일은 배포할 때마다 변경되는 프라이빗 API로 오직 테라폼 내부에서 사용하기 위한 것.
테라폼 상태 파일을 직접 편집하거나 직접 읽는 코드를 작성해서는 안됨</p>
</blockquote>
<h1 id="상태-파일을-공유하기">상태 파일을 공유하기</h1>
<p>가장 대표적인 파일 공유방법은 Git과 같은 버전 관리 시스템에 두는 것인데 terraform.tfstate를 버전 관리 시스템에 두면 안되는 이유는 다음과 같습니다.</p>
<ol>
<li><p>휴먼 에러
최신 변경사항을 가져오고 push하는 것을 잊기 쉽습니다.</p>
</li>
<li><p>잠금
여러명의 팀원이 동시에 하나의 상태 파일에 terraform apply 명령을 실행하지 못하게 하는 잠금 기능을 제공하지 않습니다.</p>
</li>
<li><p>시크릿
특정 테라폼 리소스에 중요한 데이터를 저장해야 할 때 문제가 발생합니다. </p>
</li>
</ol>
<p>버전 관리 시스템대신 테라폼에 내장된 원격 백엔드 기능을 사용할 수 있습니다.</p>
<h3 id="원격-백엔드">원격 백엔드</h3>
<p>테라폼의 기본 백엔드는 로컬 백엔드로써 로컬 디스크에 상태파일을 저장합니다. 
<strong>원격 백엔드를 사용하면 상태파일을 원격 공유 저장소에 저장할 수 있습니다.</strong> 아마존 S3와 애저 스토리지, 구글 클라우드 스토리지, 해시코프의 테라폼 클라우드, 테라폼 프로, 테라폼 엔터프라이즈 등 다양한 원격 백엔드가 지원됩니다.
아마존 S3를 사용해서 실습을 진행하겠습니다.</p>
<h3 id="s3를-원격-백엔드로-사용">S3를 원격 백엔드로 사용</h3>
<p>먼저 S3를 생성합니다.</p>
<pre><code class="language-json">provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
}

resource &quot;aws_s3_bucket&quot; &quot;terraform_state&quot; {
  bucket = &quot;terraform-up-and-running-state-hyeongjun&quot;

  # 중요한 리소스를 실수로 destroy 하지 않게 destroy 중 오류 발생
  lifecycle {
    prevent_destroy = true
  }
}

resource &quot;aws_s3_bucket_server_side_encryption_configuration&quot; &quot;example&quot; {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = &quot;AES256&quot;
    }
  }
}

resource &quot;aws_s3_bucket_versioning&quot; &quot;versioning_example&quot; {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = &quot;Enabled&quot;
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/8b33ce3b-5492-45af-8952-c5f51e2e3e3d/image.png" alt=""></p>
<p>S3가 생성된 것을 확인 가능합니다.
이제 이 S3를 terraform backend로 사용하기 위해 terraform에서 backend 구성을 s3로 작성합니다.</p>
<pre><code class="language-json">
terraform {
  backend &quot;s3&quot; {
    bucket = &quot;terraform-up-and-running-state-hyeongjun&quot;
    key = &quot;stage/services/webserver-cluster/terraform.tfstate&quot;
    region = &quot;ap-northeast-2&quot;

    dynamodb_table = &quot;terraform-up-and-running-locks&quot;
    encrypt = true
  }
}</code></pre>
<p>그리고 다시 <code>terraform init</code> 명령을 실행하면 테라폼은 로컬 디스크에 이미 상태 파일이 있음을 자동으로 감지해 새로운 S3 백엔드에 복사한다는 메시지를 표시합니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/74f90739-23b8-45fc-a327-09f0f1452c12/image.png" alt=""></p>
<p>yes를 입력하면 S3 버킷에 상태파일이 생성된 것을 확인 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e4a9e9ac-3927-48d2-a4f6-e6ea27061e71/image.png" alt=""></p>
<p>백엔드가 활성화되면 테라폼은 명령(terraform apply)을 <strong>실행하기 전에 이 S3 버킷에서 최신 상태를 자동으로 가져옵니다.</strong> 그리고 명령을 실행한 후에는 최신 상태를 <strong>s3 버킷에 자동으로 푸시</strong>합니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b1340015-386f-4b5a-8034-ee7473382807/image.png" alt=""></p>
<h2 id="테라폼-백엔드의-단점">테라폼 백엔드의 단점</h2>
<p>테라폼 백엔드에는 몇 가지 단점이 있는데</p>
<ol>
<li>S3 버킷을 먼저 생성하고 backend 구성을 추가해야 합니다.</li>
<li>테라폼의 backend 블록에는 변수나 참조를 사용할 수 없습니다.
backend 블록을 다시 보면<pre><code class="language-json"></code></pre>
</li>
</ol>
<p>terraform {
  backend &quot;s3&quot; {
    bucket = &quot;terraform-up-and-running-state-hyeongjun&quot;
    key = &quot;stage/services/webserver-cluster/terraform.tfstate&quot;
    region = &quot;ap-northeast-2&quot;</p>
<pre><code>dynamodb_table = &quot;terraform-up-and-running-locks&quot;
encrypt = true
}</code></pre><p>}</p>
<pre><code>수동으로 입력해야하는 &quot;&quot; 값들이 굉장히 많습니다.
key 값은 테라폼 모듈마다 고유해야 하고 실수로 다른 모듈을 덮어 쓰지 않도록 해야 합니다.

---
# 격리
모든 인프라를 단 하나의 테라폼 파일로 정의하면 실수로 전체를 날려버릴 수 있습니다.
보통 개발환경은 Prod, Dev, Stage, Test 등등 여러가지로 나뉘는데 테스트 중 실수로 Prod 환경의 인프라까지 사라진다면 큰 낭패입니다.
따라서 현업에서 상태 파일의 격리는 필수입니다.
상태 파일을 격리하는 방법은 두 가지 방법이 있는데
1. 테라폼 작업 공간(work space)을 통한 격리
2. 파일 레이아웃을 이용한 격리
가 있습니다.
## 테라폼 작업 공간을 통한 격리
테라폼은 테라폼 작업 공간(Terraform workspace)을 통해 테라폼 상태를 여러 개의 작업 공간에 저장할 수 있습니다.
원래는 default 라는 기본 작업 공간에서 사용합니다.
terraform workspace 명령으로 테라폼 작업공간을 관리할 수 있습니다.
![](https://velog.velcdn.com/images/hyeongjun-hub/post/fbe03696-372a-4582-a256-5960c9bc9510/image.png)
### 테라폼 작업 공간의 단점
1. 모든 작업 공간의 상태파일이 동일한 백엔드(s3)에 저장되므로 같은 인증 메커니즘을 사용합니다.
2. terraform workspace 명령어를 통해서만 작업 공간의 정보를 알 수 있으므로 인프라를 제대로 파악 못하고 작업을 할 가능성이 있습니다.
## 파일 레이아웃을 이용한 격리
테라폼 작업 공간의 단점을 극복하고 환경을 완전히 격리하려면 각 테라폼 구성파일을 분리된 폴더에 넣는 방법이 있습니다.
![](https://velog.velcdn.com/images/hyeongjun-hub/post/f9eb4ca7-f4ca-49e0-840e-f50102b8add4/image.png)

**일반적인 환경의 예시**

- stage

    테스트 환경과 같은 사전 프로덕션 워크로드 환경

- prod

    사용자용 맵 같은 프로덕션 워크로드 환경

- mgmt

    베스천 호스트나 젠킨스와 같은 데브옵스 도구 환경

- global

    S3, IAM과 같이 모든 환경에서 사용되는 리소스를 배치하는 장소


**각 환경에서의 일반적인 구성 요소**

- vpc

    해당 환경을 위한 네트워크 토폴로지

- services

    해당 환경에서 서비스되는 애플리케이션 또는 마이크로서비스

- data-storage

    해당 환경에서 실행할 데이터 저장소


**각 구성 요소에서의 일반적인 테라폼 구성 파일**

- variables.tf

    입력변수

- outputs.tf

    출력변수

- main.tf

    리소스
### 파일 레이아웃을 이용한 격리의 단점
- 한 번의 명령으로 전체 인프라를 만들지 못하고 각 폴더에서 `terraform apply`를 진행해야 합니다.
    - 테라그런트를 사용하면 apply-all 명령 사용 가능합니다.
- **리소스 종속성**을 사용하기 어렵습니다.
    - 다른 폴더에 있으면 서로 접근하기 어렵습니다.
    - 현재 예제에서는 웹 서비스가 다른 폴더에 있는 DB의 정보를 직접 가져올 수 없습니다.
        - 즉 web을 담당하는 terraform과 db를 담당하는 terraform이 격리되어 액세스가 불가능합니다.
    - 이것을 해결하는 방법은 **`terraform_remote_state`**를 이용하는 것입니다.
### terraform_remote_state
다른 테라폼 구성 세트에 완전한 읽기 전용 방식으로 (s3에) 저장된 테라폼 상태 파일을 가져올 수 있습니다.
물론 output으로 변수를 terraform.tfstate에 저장해야 합니다.

`data.terraform_remote_state.&lt;NAME&gt;.outputs.&lt;ATTRIBUTE&gt;`
형식으로 사용할 수 있습니다.

### 실습
https://hyeongjun-hub.notion.site/Chap03-a5a80889b1da423cafdaba2acf2b9499?pvs=4

---
### Reference
https://github.com/brikis98/terraform-up-and-running-code</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Github Actions 부터 CI/CD 실습까지]]></title>
            <link>https://velog.io/@hyeongjun-hub/Github-Actions%EB%B6%80%ED%84%B0-CICD-%EC%8B%A4%EC%8A%B5%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@hyeongjun-hub/Github-Actions%EB%B6%80%ED%84%B0-CICD-%EC%8B%A4%EC%8A%B5%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Fri, 19 May 2023 08:59:02 GMT</pubDate>
            <description><![CDATA[<h1 id="목차">목차</h1>
<ul>
<li>Gihub Actions?<ul>
<li>Github Actions의 구성요소</li>
<li>Github Actions의 Workflow 문법</li>
</ul>
</li>
<li>실습<ul>
<li>Github Actions을 이용해 docker hub에 이미지 자동 push</li>
<li>Server instance 생성</li>
<li>CD</li>
</ul>
</li>
<li>Github Actions의 장점</li>
</ul>
<h1 id="github-actions">Github Actions?</h1>
<p>GitHub Actions는 코드 저장소(repository)로 유명한 GitHub에서 제공하는 CI(Continuous Integration, 지속 통합)와 CD(Continuous Deployment, 지속 배포)를 위한 비교적 최근에 추가된 서비스입니다. 당연히 GitHub에서 코드를 관리하고 있는 소프트웨어 프로젝트에서 사용할 수 있으며 개인은 누구나 GitHub에서 코드 저장소를 무료로 만들 수 있기 때문에 다른 CI/CD 서비스 대비 진입장벽이 낮은 편입니다.</p>
<p>GitHub Actions를 사용하면 자동으로 코드 저장소에서 어떤 이벤트(event)가 발생했을 때 특정 작업이 일어나게 하거나 주기적으로 어떤 작업들을 반복해서 실행시킬 수도 있습니다. 예를 들어, 누군가가 코드 저장소에 Pull Request를 생성하게 되면 GitHub Actions를 통해 해당 코드 변경분에 문제가 없는지 각종 검사를 진행할 수 있고요. 어떤 새로운 코드가 메인(main) 브랜치에 유입(push)되면 GitHub Actions를 통해 소프트웨어를 빌드(build)하고 상용 서버에 배포(deploy)할 수도 있습니다. 뿐만 아니라 매일 밤 특정 시각에 그날 하루에 대한 통계 데이터를 수집시킬 수도 있습니다.</p>
<p>이렇게 소프트웨어 프로젝트에서 지속적으로 수행해야하는 반복 작업들을 업계에서는 소위 CI/CD라고 많이 줄여서 부르는데요. 사람이 매번 직접 하기에는 비효율적인데다가 실수할 위험도 있기 때문에 GitHub Actions와 같은 자동화시키는 것이 유리합니다.</p>
<h3 id="github-actions의-구성요소">Github Actions의 구성요소</h3>
<ul>
<li>Workflow<ul>
<li>최상위 개념</li>
<li>여러 Job으로 구성되고, Event에 의해 트리거될 수 있는 자동화된 프로세스입니다.</li>
<li>Workflow 파일은 YAML로 작성되고, .github/workflows 폴더 아래에 저장됩니다.</li>
</ul>
</li>
<li>Event<ul>
<li>Workflow를 트리거하는 특정 활동이나 규칙입니다.<ul>
<li>특정 브랜치로 Push하거나</li>
<li>특정 브랜치로 Pull Request하거나</li>
<li>특정 시간대에 반복(Cron)</li>
<li>Webhook을 사용해 외부 이벤트를 통해 실행</li>
</ul>
</li>
</ul>
</li>
<li>Job<ul>
<li>Job은 여러 Step으로 구성되고 가상 환경의 인스턴스에서 실행됩니다.</li>
<li>여러 Job 간 의존 관계를 가질 수 있고 기본적으로 병렬 실행됩니다.</li>
</ul>
</li>
<li>Step<ul>
<li>Task들의 집합으로 커맨드를 실행하거나 action을 실행할 수 있습니다.</li>
</ul>
</li>
<li>Action<ul>
<li>Workflow의 가장 작은 블럭</li>
<li>Job을 만들기 위해 Step 들을 연결할 수 있습니다. </li>
<li>재사용이 가능한 컴포넌트입니다.</li>
<li>개인적으로 만든 Action을 사용할 수도 있고 Marketplace에 있는 공용 Action을 사용할 수도 있습니다.</li>
</ul>
</li>
<li>Runner<ul>
<li>Github Action Runner 어플리케이션이 설치된 머신으로 Workflow가 실행될 인스턴스입니다.</li>
<li>Github에서 호스팅해주는 Github-hosted runner와 직접 호스팅하는 Self-hosted runner로 나뉩니다.</li>
<li>Github-hosted runner는 Azure의 Standard_DS2_v2로 vCPU 2, 메모리 7GB, 임시 스토리지 14GB의 사양입니다.</li>
</ul>
</li>
</ul>
<h3 id="github-actions의-workflow-문법">Github Actions의 Workflow 문법</h3>
<p>아래는 예제에 쓰인 docker image를 push하는 yaml 파일입니다.</p>
<pre><code class="language-yaml">
name: Docker Image CI

on:
  push:
    branches: [ &quot;main&quot; ]
  pull_request:
    branches: [ &quot;main&quot; ]

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v1

    - name: Login to DockerHub
      uses: docker/login-action@v1
      with:
        username: chogudwns
        password: ${{ secrets.DOCKER_HUB_PW }}

    - name: Build and push
      id: docker_build
      uses: docker/build-push-action@v2
      with:
        push: true
        tags: chogudwns/saramin</code></pre>
<p>가장 처음의 <strong>name</strong>은 workflow의 이름입니다.</p>
<p>그 다음의 <strong>on</strong> 은 해당 workflow가 수행될 trigger인 Event를 의미합니다. push - branches - main 은 main branch 에 push 가 일어나면 수행된다는 의미입니다. 그렇기 때문에 요청된 PR 을 merge 하거나 main branch 에 직접 push 하면 해당 workflow가 trigger 됩니다.</p>
<p><strong>jobs</strong> 는 pipeline을 구성하는 부분입니다. steps 처음을 보면 &quot;name: Checkout&quot; 부분에 uses: actions/checkout@v3 로 되어 있는데 미리 정의된 action입니다. 실체 소스는 <a href="https://github.com/actions/checkout">https://github.com/actions/checkout</a> 으로 접속하면 볼 수 있으며 해당 기능은 디폴트로 workflow가 수행되는 github repository 의 소스 (현재 project 의 소스)를 내려받는 기능입니다. 내려 받을 repository 를 바꿀려면 변경 가능합니다.</p>
<p><strong>stebs</strong>는 여러개로 이루어져 job을 구성합니다. &quot;name: Set up Docker Buildx&quot; 의 steb은 builder driver 나 platform 등을 세팅하는 부분입니다. 역시 자세한 부분은 <a href="https://github.com/docker/setup-buildx-action">https://github.com/docker/setup-buildx-action</a> 에서 확인 가능합니다.</p>
<p>&quot;name: Login to DockerHub&quot; 는 docker hub 에 접근하기 위해서 id 와 password 를 지정하고 docker hub에 로그인하는 부분입니다. 이를 위해서는 github repository 의 settings 로 들어가서 좌측 하단의 secrets &gt;&gt; Actions 에서 Action 용 secret 을 만들어줘야 합니다.</p>
<h1 id="실습">실습</h1>
<p>실습에 사용한 github repository는 제가 이전에 생성했던  검색어를 입력하면 사람인 채용공고 사이트에서 채용공고를 크롤링하는 코드가 있는 repository를 사용했습니다.</p>
<p><a href="https://github.com/hyeongjun-hub/learngo">github repository 링크</a></p>
<p>스크린샷 </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/315469e8-95d5-47f0-a167-77962f07bf4c/image.png" alt=""></p>
<p>csv 파일 예시
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/80bb964e-5044-4457-a517-7c8bfbbae396/image.png" alt=""></p>
<h3 id="github-actions을-이용해-docker-hub에-이미지-자동-push">Github Actions을 이용해 docker hub에 이미지 자동 push</h3>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/52489c9b-d196-4f69-b937-b849f930a35e/image.png" alt=""></p>
<pre><code class="language-yaml">name: Docker Image CI

on:
  push:
    branches: [ &quot;main&quot; ]
  pull_request:
    branches: [ &quot;main&quot; ]

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v1

    - name: Login to DockerHub
      uses: docker/login-action@v1
      with:
        username: chogudwns
        password: ${{ secrets.DOCKER_HUB_PW }}

    - name: Build and push
      id: docker_build
      uses: docker/build-push-action@v2
      with:
        push: true
        tags: chogudwns/saramin

</code></pre>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6177b215-2750-4eea-b4c7-cda9c1208002/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/64b66bd2-8dd2-451f-ab77-db45740962d2/image.png" alt=""></p>
<p>자동 image push 성공</p>
<h3 id="server-instace-생성">Server instace 생성</h3>
<p>aws의 EC2 인스턴스를 server instance로 이용했습니다.
인스턴스가 docker hub에 접근해 이미지를 pull 하고 해당 이미지를 run 할 수 있게 하였습니다.</p>
<p>docker를 다운받습니다.</p>
<pre><code class="language-bash">$ sudo su
$ yum install -y docker
$ systemctl start docker
$ systemctl enable docker
</code></pre>
<p>외부에서도 docker를 접근 가능하게 권한을 변경합니다.
<code>chmod 666 /var/run/docker.sock</code></p>
<h3 id="cd">CD</h3>
<p>이제 파이프라인에 방금 만든 인스턴스에 접근하여 image를 pull하고 실행하는 코드를 작성해야 합니다.</p>
<p>먼저 github에 secret으로 EC2의 호스트명과 EC2 key를 저장합니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6c190a82-0f68-4ff8-8780-12b14afa0409/image.png" alt=""></p>
<p>yaml 파일에 코드를 추가합니다. </p>
<pre><code class="language-yaml"> - name: EC2 Docker Run
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_KEY }}
          script: |
            sudo su
            docker rm -f $(docker ps -qa)
            docker rmi chogudwns/saramin
            docker pull chogudwns/saramin 
            docker run -p -d 80:1323 -t saramin chogudwns/saramin
</code></pre>
<blockquote>
<p>container stop 과 remove를 한번에 하기 위해 -f 옵션을 줬습니다.
workflow가 docker run에서 멈추지 않기 위해 -d 옵션으로 백단에서 실행했습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/c23fc79a-a569-4f95-bd90-a7b2edf1f040/image.png" alt=""></p>
<p>workflow가 성공했음을 확인했습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/40feed17-0ac4-4409-89ad-51ac06ff569d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/07493b6d-daca-4878-8abe-06ba134205da/image.png" alt=""></p>
<p>이제는 소스코드를 변경하고 push를 해보겠습니다.
전체적인 흐름은 </p>
<ol>
<li>source code 변경</li>
<li>github action workflow 실행</li>
<li>docker image 생성</li>
<li>docker image push</li>
<li>ec2 instance에서 docker image pull</li>
<li>ec2 instance에서 docker container 실행
순서 입니다.</li>
</ol>
<p>페이지의 header를 변경해보겠습니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/56da9821-6194-4e70-8b0a-6e72aacd2b03/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b228ed05-feb2-413d-a450-32419f0abdf0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/85fad0d5-ebe1-43e2-937e-182d00a08d95/image.png" alt=""></p>
<p>성공적으로 변경된 것을 확인가능합니다.</p>
<h1 id="github-actions의-장점">Github Actions의 장점</h1>
<p>저는 Jenkins와 Travis CI, 그리고 이번에 github actions를 사용해 보았는데 github actions의 장점은 public repository에 대해 2,000분까지 무료이고 github 레포지토리와의 연동이 매우 쉽다는 것입니다.
또한 github actions는 안전합니다. GitHub Actions는 GitHub의 보안 인프라를 기반으로 구축되므로 코드와 리소스가 안전하게 유지됩니다.
뿐만 아니라 따로 설치할 종속성이 없다는 것이 장점입니다. Jenkins는 Java로 만들어졌기 때문에 JDK가 필요하고 만약 Container로 실행하면 노드에 Docker도 포함되어야 합니다. </p>
<hr>
<h3 id="reference">Reference</h3>
<ul>
<li>github actions 공식문서 : <a href="https://docs.github.com/en/actions">https://docs.github.com/en/actions</a></li>
<li>github actions yaml file(docker image push) : <a href="https://devocean.sk.com/blog/techBoardDetail.do?ID=163350">https://devocean.sk.com/blog/techBoardDetail.do?ID=163350</a></li>
<li>github actions 소개 : <a href="https://www.daleseo.com/github-actions-basics/">https://www.daleseo.com/github-actions-basics/</a></li>
<li>docker hub repository : <a href="https://hub.docker.com/r/chogudwns/saramin">https://hub.docker.com/r/chogudwns/saramin</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS SAA 합격 후기]]></title>
            <link>https://velog.io/@hyeongjun-hub/AWS-SAA-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@hyeongjun-hub/AWS-SAA-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 18 May 2023 02:05:48 GMT</pubDate>
            <description><![CDATA[<p>지난 23년 5월 9일 AWS SAA-C03 시험을 보았고 합격하였습니다. 어떻게 자격증을 준비했는지와 자격증을 준비하면서 느낀점을 간략하게 정리해보는 글을 써보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/9bc3c485-533b-4602-8e04-4c1cdd9b5d19/image.png" alt=""></p>
<h1 id="saa란">SAA란?</h1>
<p>AWS Certified Solutions Architect - Associate 약자로
공식에서는 &quot;이 자격 증명은 클라우드 이니셔티브 구현에 중요한 기술을 갖춘 인재를 식별하고 개발하는 데 도움이 됩니다. AWS Certified Solutions Architect - Associate를 취득하면 AWS에서 배포 시스템을 설계하고 구현하는 능력을 입증할 수 있습니다.&quot;
라고 설명합니다.
제가 생각하기엔 클라우드 시스템 서비스에 대한 기본 지식, 구성요소, 동작방식들과 AWS에서 어떤 서비스로 제공해 주고 있는지를 아는지, 또
이를 기반으로 주어진 상황에서는 어떤 AWS 서비스를 선택해야하는 지를 물어보는 시험입니다.</p>
<p>시험 응시료가 150 USD로 비싼편에 속하기 때문에 한 번에 붙기 위해서 두 달간 열심히 노력했습니다.</p>
<h1 id="공부-방법">공부 방법</h1>
<p>3월 부터 공부를 시작하였는데 가장 먼저 스터디를 만들었습니다. 
AWS 서비스들을 공부 할 수 있는 Udemy <a href="https://www.udemy.com/course/best-aws-certified-solutions-architect-associate/">강의</a>를 선정하여 매주마다 각자 공부하고 만나서 발표하는 시간을 가졌고 4주에 한 번 스터디원과 만나서는 발표 뿐만 아니라 SAA-C03의 Dump 문제들을 풀고 풀이하는 시간을 가졌습니다.
스스로 공부하기 위해 Udemy 강의를 듣고 정리하는 <a href="https://hyeongjun-hub.notion.site/AWS-SAA-8ded051ed0fc43f7a5ae4248a5bc4534">노션 페이지</a>를 생성하였고 매일 오전시간을 활용해 3시간 동안 강의를 듣고 강의 내용을 노션에 날짜별로 기록하고 정리하였습니다.
그리고 시험 이틀 전부터는 Dump 문제들을 200문제 정도 풀고 시험을 치뤘습니다.</p>
<h1 id="saa-시험">SAA 시험</h1>
<p>시험은 오프라인으로 직접 강남에 가서 치뤘고 시간은 두 시간 딱 맞춰 끝냈습니다. 시간이 널널하기 때문에 차근차근 문제를 풀었습니다. 적으면서 문제를 풀 수 있게 시험 모니터 앞에는 지워지는 펜과 노트가 있었습니다. 굳이 사용은 안해도 되는데 저는 사용해서 풀었습니다.</p>
<p>시험을 마치면 결과가 바로 나오지는 않고 6시간 정도 있다가 메일로 결과를 받았습니다. 
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/f142fd12-8459-4734-bd7e-bac8633969ad/image.png" alt=""></p>
<h1 id="느낀점">느낀점</h1>
<p>원래 AWS 자격증을 준비하기 전에도 AWS 서비스들을 공부했었습니다. 하지만 자격증이라는 목적을 가지고나니 강의를 들을 때도 정리할 때도 기억에 오래 남기기 위해 노력하고 어떤 식으로 시험에 나올지 상상하면서 공부했기 때문에 효율이 올라갔습니다. SAA 합격 후기들을 보면 그냥 Dump 문제들을 달달 외워서 시험에 합격한 후기들이 많은데 그러기 보다는 자격증을 따기위해서가 아니라 본인의 역량을 키우기 위해 자격증을 공부한다고 생각하시고 시험준비에 임하길 추천드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker compose의 네트워크부터 실습까지]]></title>
            <link>https://velog.io/@hyeongjun-hub/Docker-compose%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EB%B6%80%ED%84%B0-%EC%8B%A4%EC%8A%B5%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@hyeongjun-hub/Docker-compose%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EB%B6%80%ED%84%B0-%EC%8B%A4%EC%8A%B5%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 04 May 2023 04:54:10 GMT</pubDate>
            <description><![CDATA[<h1 id="목표">목표</h1>
<ul>
<li>docker compose의 use case</li>
<li>docker network의 복습</li>
<li>docker compose가 사용하는 docker network를 알아보고</li>
<li>실제 여러 컨테이너가 통신할 수 있는 환경을 구축해본다.</li>
</ul>
<h1 id="docker-compose">Docker compose</h1>
<p>이번 포스트는 docker compose입니다. docker compose는 무엇이고 어떨 때 쓰일까요?
<strong>Docker Compose</strong> 는 다중 컨테이너 애플리케이션을 정의, 공유할 수 있도록 개발된 도구로 단일 명령을 사용하여 모두 실행 또는 종료할 수 있도록 개발된 도구입니다.
도커 컴포즈는 이럴 때 쓰입니다.</p>
<h3 id="개발-환경의-표준화">개발 환경의 표준화</h3>
<p>팀 구성원이 규칙대로 개발 환경을 수동으로 정비하면 시간이 걸릴뿐만 아니라 수작업 실수를 유발할 가능성이 있습니다. Docker Compose를 사용하여 설정 파일을 전달하면, 개발 환경의 세부 사항을 걱정하지 않고 하나의 명령으로 환경을 정비 할 수 있습니다. 이로 개발 환경 정비 작업을 간소화 할 수 있습니다.</p>
<h3 id="테스트-환경의-자동화">테스트 환경의 자동화</h3>
<p>CI(Continuous Integration)와 CD(Continuous Deployment)를 할 경우, end to end 테스트(모든 구성 요소를 결합하여 테스트)를 자동화하려면 이상적으로는 테스트마다 독립적인 테스트 환경을 제공 해야 합니다. Docker Compose를 사용하면 필요한 테스트 환경을 명령어 1개로 시작할 수 있으며, 검사가 완료되면 쉽게 파기할 수도 있습니다.</p>
<h3 id="단일-호스트-배포">단일 호스트 배포</h3>
<p>운영 환경이 단일 서버의 경우 Docker Compose를 사용하여 배포 할 수 있습니다.
그러나 응용 프로그램을 확장하기 위해 다중 노드에 배포하려면 일반적으로 Docker Engine의 Swarm mode 라는 기능 또는 K8S, AWS ECS 와 Google Container Engine과 같은 클라우드 클러스터 매니저가 더 적합합니다.</p>
<h1 id="docker-network-복습">Docker network 복습</h1>
<p>이전 포스트에 docker의 네트워크를 공부해보았습니다. 기억이 안나시는 분은 <a href="https://velog.io/@hyeongjun-hub/%EB%8F%84%EC%BB%A4%EC%99%80-%EB%8F%84%EC%BB%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC">여기</a>를 참고해주시기 바랍니다.
짧게 복습하자면 Default 네트워크인 Bridge 네트워크를 사용하게 되면 그림과 같이 네트워크가 구성되게 됩니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e846e7e3-70d5-4072-8dcf-ace5b14479c2/image.png" alt="">
그림은 두 개의 컨테이너가 네트워크에 소속되어 있는 모습이고 임의로 컨테이너를 네트워크에 연결하기 위해서는 
<code>docker network connect NETWORK CONTAINER</code> 명령을 사용할 수 있습니다.
그러면 해당 네트워크가 가진 private한 IP 대역의 IP중 하나를 container가 할당받습니다.</p>
<h3 id="컨테이너-간-통신">컨테이너 간 통신</h3>
<p>이번에는 같은 도커 네트워크에 속한 컨테이너들의 통신을 살펴보겠습니다.</p>
<p><a href="https://hub.docker.com/_/busybox">busybox</a> 이미지로 A 컨테이너와 B 컨테이너를 생성합니다.</p>
<blockquote>
<p>busybox는 Linux 환경에서 경량화된 유틸리티 도구 모음인 BusyBox 프로젝트에서 파생된 이미지로, 최소한의 도구만을 갖추고 있습니다. busybox 이미지는 디버깅이나 테스트, 간단한 기능의 실행을 위해 사용될 수 있습니다. busybox는 다른 이미지의 기반이 되는 미니멀한 운영체제 이미지를 만들 때도 자주 사용됩니다.</p>
</blockquote>
<p>기본적으로 default network를 사용하므로 connect 명령어를 사용할 필요는 없습니다.
<code>docker network ls</code>로 네트워크 이름을 확인하고
<code>docker network inspect bridge</code>로 상세정보를 확인가능합니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/c11d9bb9-c248-4ee5-9466-4dc910d024bc/image.png" alt=""></p>
<p>같은 네트워크에서는 컨테이너간 통신에서 ip로 접근이 가능합니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/d6a71080-d5e8-48a6-9723-cdb7837119fa/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/650443f4-6a04-4fd9-85cd-6c1f72fa57d2/image.png" alt=""></p>
<h1 id="docker-compose의-network">Docker compose의 network</h1>
<blockquote>
<p>docker compose의 link기능은 version 2부터 deprecated 되었습니다. </p>
</blockquote>
<h3 id="default-네트워크">default 네트워크</h3>
<p>docker compose는 기본적으로 default 네트워크를 생성하고 모든 컨테이너를 연결하는데 그 이름은 <code>docker-compose.yaml이 존재하는 폴더명</code> + <code>_default</code>입니다.</p>
<p>이외에도 사용자지정 네트워크를 지정할 수 있습니다</p>
<p><strong>docker-compose.yaml</strong></p>
<pre><code class="language-yaml">version: &#39;3&#39;
services:
  web:
    image: nginx:latest
    ports:
      - &quot;8000:80&quot;
    networks:
      - default
      - our_net

  db:
    image: postgres
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

networks: # 네트워크 지정
  our_net:
   # driver: bridge                   </code></pre>
<p><code>docker compose up</code></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/49d38892-0ff1-4979-806a-1213f449e85c/image.png" alt=""></p>
<p>docker network들을 생성합니다</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/aa8c01ae-22af-4d7f-a88f-23488f6122a3/image.png" alt=""></p>
<p>db 컨테이너는 default 네트워크에만 자동으로 연결되지만 web 컨테이너는 default와 our_net 네트워크 둘 다 연결됩니다.</p>
<p><strong>default network</strong>
172.22.0.0/16 대역</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/67fb1ecd-4a71-4e96-85f5-28b1602ae31a/image.png" alt=""></p>
<p><strong>our-net</strong>
172.23.0.0/16 대역
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/3eda8737-b52b-4162-8c7c-88362df57675/image.png" alt=""></p>
<p>default 네트워크를 docker-compose 외부에서 설정한 네트워크로 설정할 수도 있습니다.</p>
<pre><code class="language-yaml">version: &#39;3&#39;
services:
  web:
    image: nginx:latest
    ports:
      - &quot;8000:80&quot;

  db:
    image: postgres
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

networks:
  default:
    external:
      out_net # 외부 네트워크 설정</code></pre>
<p>이렇게 진행하면 docker compose down 시에도 외부 네트워크는 삭제되지 않습니다.</p>
<h1 id="docker-compose-실습">Docker compose 실습</h1>
<p>이번에는 docker compose로 여러 컨테이너들을 띄우고 네트워크를 이용해 서로 통신을 하는 실습을 진행하겠습니다.</p>
<p>여러 컨테이너로 실습하기 위해 <a href="https://github.com/dockersamples/example-voting-app">voting app</a>을 선택하였습니다.</p>
<p>해당 깃허브 레포지토리에 자세히 나와있지만 그림과 함께 간략히 설명하자면
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/16cb0719-3e72-417b-80a1-116468ff31d3/image.png" alt=""></p>
<ul>
<li>voting-app    <ul>
<li>사용자에게 투표를 촉구하는 Web 페이지를 표시하는 응용 프로그램.    </li>
<li>데이터는 redis에 저장한다. </li>
<li>Python과 Flask에 따르는 Web 응용 프로그램</li>
</ul>
</li>
<li>redis<ul>
<li>투표 결과를 임시로 저장 캐시.</li>
<li>Redis</li>
</ul>
</li>
<li>worker<ul>
<li>투표 결과를 가져오고 Postgres 데이터베이스에 저장하는 작업자.</li>
<li>.NET</li>
</ul>
</li>
<li>db<ul>
<li>투표 결과를 저장하는 데이터베이스. 데이터는 Docker volume 보관한다.</li>
<li>Postgres</li>
</ul>
</li>
<li>result-app<ul>
<li>실시간 투표 결과를 표시하는 Web 응용 프로그램.</li>
<li>Node.js</li>
</ul>
</li>
</ul>
<p><strong>docker-compose.yml</strong></p>
<pre><code class="language-yaml">version: &quot;3&quot;

services:
  vote:
    build: ./vote
    command: python app.py
    volumes:
     - ./vote:/app
    ports:
      - &quot;5000:80&quot;
    networks:
      - front-tier
      - back-tier

  result:
    build: ./result
    command: nodemon --debug server.js
    volumes:
      - ./result:/app
    ports:
      - &quot;5001:80&quot;
      - &quot;5858:5858&quot;
    networks:
      - front-tier
      - back-tier

  worker:
    build:
      context: ./worker
    networks:
      - back-tier

  redis:
    image: redis:alpine
    container_name: redis
    ports: [&quot;6379&quot;]
    networks:
      - back-tier

  db:
    image: postgres:9.4
    container_name: db
    volumes:
      - &quot;db-data:/var/lib/postgresql/data&quot;
    networks:
      - back-tier

volumes:
  db-data:

networks:
  front-tier:
  back-tier:
</code></pre>
<p>해당 docker-compose.yaml은 front-tier와 back-tier 두 개의 네트워크를 사용합니다.</p>
<p>열려있는 포트번호 확인
<code>sudo lsof -PiTCP -sTCP:LISTEN</code></p>
<p><code>docker compse up</code>으로 docker-compose.yaml에 작성된 이미지들을 띄워 봅시다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/2d2c4e6e-08fd-46d8-b283-0809d6f61c47/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/1067d17b-7515-4a0c-8a0e-d92f42745f24/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e0fae36a-5077-4a17-bbcc-fe1c2f85efa0/image.png" alt=""></p>
<p>애플리케이션이 잘 작동하는 것을 확인할 수 있습니다. </p>
<p><code>docker compose down</code>을 이용해 하나의 명령어로 모든 컨테이너를 중지시킬 수 있습니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/9f6d36d9-0415-449b-be6b-36d293abb68c/image.png" alt=""></p>
<h1 id="plus">Plus</h1>
<h3 id="docker-compose의-health-check">Docker Compose의 Health check</h3>
<p>docker의 기능 중에는 Health check 기능도 있는데 이는 Docker compose에도 또한 지원됩니다.</p>
<p>docker-compose의 version 2부터는 depends on과 health check 기능이 추가되었는데 그 내용을 간략히 살펴보겠습니다. </p>
<pre><code class="language-yaml">healthcheck: 
      test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost&quot;]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 10s</code></pre>
<p>test - 컨테이너를 테스트하는 데 사용됩니다. 이를 위해 <code>curl</code> 명령을 사용하여 호스트로부터 응답이나 신호를 받았습니다.</p>
<blockquote>
<p>curl 명령의 -f 옵션이 궁금해서 찾아보았습니다. <code>man curl</code>
(HTTP) 서버 오류가 전혀 출력되지 않고 빠르게 실패합니다. 이것은 스크립트와 사용자가 실패한 시도를 더 잘 처리할 수 있도록 하는 데 유용합니다. HTTP 서버가 문서 전달에 실패하는 일반적인 경우에는 이를 알리는 HTML 문서를 반환합니다(종종 이유 등을 설명하기도 함). 이 플래그는 컬이 출력하는 것을 방지하고 오류 22를 반환합니다. 이 방법은 절대 안전하지 않으며 특히 인증이 관련된 경우(응답 코드 401 및 407) 성공하지 못한 응답 코드가 빠져나가는 경우가 있습니다.</p>
</blockquote>
<p><code>interval</code> - healthcheck 프로세스가 실행되는 기간 또는 간격을 지정합니다.
<code>timeout</code> - 상태 확인을 기다리는 시간을 정의합니다. 오류 또는 비정상적인 조건의 경우 지정된 시간이 지나면 종료 코드를 반환합니다.
<code>retries</code> - 실패 후 상태 확인을 구현하기 위한 시도 횟수를 정의하는 데 사용됩니다.
<code>start_period</code> - 시작 기간은 부트스트랩 시간이 필요한 컨테이너의 초기화 시간을 제공합니다. 해당 기간 동안의 프로브 실패는 최대 재시도 횟수에 포함되지 않습니다. 그러나 시작 기간 동안 상태 확인이 성공하면 컨테이너가 시작된 것으로 간주되며 모든 연속 실패는 최대 재시도 횟수에 포함됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/31af5148-b1f1-4195-828c-f712c4b6c050/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/8323a35f-add9-4396-87f7-9c94f5f6f1a2/image.png" alt=""></p>
<p>health check를 설정했다면 <code>depends_on:</code>에 <code>condition: service_healthy</code>를 작성가능 합니다.</p>
<p>이는 종속된 service를 시작하고 health check를 마친 후 의존된 서비스를 실행한다는 얘기입니다.</p>
<p><strong>예시</strong></p>
<pre><code class="language-yaml">services:
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy
        restart: true
      redis:
        condition: service_started
  redis:
    image: redis
  db:
    image: postgres</code></pre>
<hr>
<p>참고</p>
<ul>
<li>네트워크 : <a href="https://www.daleseo.com/docker-networks/">https://www.daleseo.com/docker-networks/</a></li>
<li>실습 : <a href="https://www.devkuma.com/docs/docker/get-started/4-deploying-multiple-containers-using-docker-compose/">https://www.devkuma.com/docs/docker/get-started/4-deploying-multiple-containers-using-docker-compose/</a></li>
<li>실습 코드 : <a href="https://github.com/dockersamples/example-voting-app">https://github.com/dockersamples/example-voting-app</a></li>
<li>dockerfile -&gt; docker-compose.yaml 변환 사이트 : <a href="https://www.composerize.com/">https://www.composerize.com/</a></li>
<li><a href="https://meetup.nhncloud.com/posts/277">https://meetup.nhncloud.com/posts/277</a></li>
<li>docker-compose 공식 문서 사이트 : <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/">https://docs.docker.com/compose/compose-file/compose-file-v3/</a></li>
<li>health check : <a href="https://docs.docker.com/engine/reference/builder/#healthcheck">https://docs.docker.com/engine/reference/builder/#healthcheck</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Solution Architect Example]]></title>
            <link>https://velog.io/@hyeongjun-hub/AWS-Solution-Architect-Example</link>
            <guid>https://velog.io/@hyeongjun-hub/AWS-Solution-Architect-Example</guid>
            <pubDate>Wed, 05 Apr 2023 03:40:33 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가며">들어가며..</h1>
<p>지금까지 배운 AWS solution을 복습할 겸 여러 사용자 요구조건에 따라 어떤 AWS Service들을 사용해야 하는지를 정리해보았습니다. </p>
<h1 id="example-1">Example 1</h1>
<p>현재 시각을 알려주는 서버를 설계한다.</p>
<h3 id="요구사항">요구사항</h3>
<ul>
<li>데이터베이스 없음</li>
<li>무상태<h3 id="최초설계">최초설계</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/58cd949d-ba4e-4311-ba1e-301d51a122dc/image.png" alt=""></li>
<li>ec2 인스턴스는 t2.micro 타입을 가짐 </li>
<li>ec2 인스턴스는 elastic IP를 가짐</li>
<li>사용자는 ec2 인스턴스에게 현재 시각을 물어본다</li>
<li>ec2 인스턴스는 현재 시각을 response 해준다.<h3 id="수직적-확장">수직적 확장</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/a72ebcc0-2579-430b-bf45-828485539633/image.png" alt=""></li>
<li>t2 인스턴스가 부하가 많아져 느리다면</li>
<li>ec2 인스턴스를 m5 인스턴스 타입으로 수직적 확장<h3 id="수평적-확장">수평적 확장</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b337ca30-11a1-46ab-8578-2fd982793be6/image.png" alt=""></li>
<li>그럼에도 부하가 많아져 느리다면</li>
<li>ASG에 인스턴스들을 넣어 수평적확장하도록 설정</li>
<li>각 인스턴스들은 elasticIP를 가짐</li>
<li>하지만 elastic IP는 계정 당 5개 사용가능하므로 문제점 발생<h3 id="route53">Route53</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/c0fb2de8-572b-4bc1-8bff-b70314bfd059/image.png" alt=""></li>
<li>Route53을 이용해 elastic ip대신 dns를 사용</li>
<li>TTL 설정</li>
<li>하나의 인스턴스에 부하가 집중되는 장애가 발생 할 가능성이 있음<h3 id="elb">ELB</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b4d9cf29-86fa-4cd3-a7e5-911bb907e6d3/image.png" alt=""></li>
<li>ELB를 이용해 부하를 분산시킴<h3 id="more">More..</h3>
Multi-AZ를 이용해 재해에 대비할 수 있음
인스턴스의 용량을 예약해 on-demand보다 비용적으로 이점을 볼 수 있음</li>
</ul>
<h1 id="example-2">Example 2</h1>
<p>쇼핑몰을 설계한다</p>
<h3 id="요구사항-1">요구사항</h3>
<ul>
<li>회원, 쇼핑물품에 대한 데이터베이스를 가짐</li>
<li>회원들의 로그인 세션, 장바구니 상태를 유지해야 함<h3 id="sticky-session">sticky session</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/5fe713ca-6b08-498a-ab38-fc253b7c0638/image.png" alt=""></li>
<li>elb에 stickness를 설정해 같은 회원은 같은 인스턴스에 연결되도록 설정</li>
<li>부하가 집중될 가능성 높음<h3 id="cookie">cookie</h3>
</li>
<li>cookie에 사용자의 정보들을 전부 집어넣어 통신한다</li>
<li>cookie의 최대 크기는 4KB로 제한적</li>
<li>보안적으로 critical<h3 id="session-store-사용">session store 사용</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b505efab-b9e5-4b8e-9ae7-0961d571d4cd/image.png" alt=""></li>
<li>elastiCache를 session store로 사용한다</li>
<li>redis혹은 memcached를 선택하여 사용 가능</li>
<li>amazon dynamoDB를 사용할 수도 있다.<h3 id="데이터베이스">데이터베이스</h3>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/d6edab89-21e6-4b70-afc2-05d0dfe19e16/image.png" alt=""></li>
<li>Amazon RDS를 데이터베이스로 사용</li>
<li>RDS 읽기 전용 복제본을 이용해 성능을 높일 수도 있다.<h3 id="elasticache를-rds의-캐시서버로-사용">ElastiCache를 RDS의 캐시서버로 사용</h3>
</li>
<li>ElastiCache를 RDS의 캐시서버로 사용해서 RDS의 성능을 높일 수 있다.</li>
<li>Redis를 사용한다면 Multi-AZ 기능을 이용해 재난에 대비할 수 있다.<h1 id="마치며">마치며..</h1>
여태 배웠던 AWS Service들을 복습하면서 어떻게 적재적소에 사용해야하는 지를 알게되었습니다. AWS는 알면 알수록 정답이 뚜렷해지는 것 같습니다. </li>
</ul>
<hr>
<p><a href="https://www.udemy.com/course/best-aws-certified-solutions-architect-associate/">https://www.udemy.com/course/best-aws-certified-solutions-architect-associate/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 도커 네트워크]]></title>
            <link>https://velog.io/@hyeongjun-hub/%EB%8F%84%EC%BB%A4%EC%99%80-%EB%8F%84%EC%BB%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC</link>
            <guid>https://velog.io/@hyeongjun-hub/%EB%8F%84%EC%BB%A4%EC%99%80-%EB%8F%84%EC%BB%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC</guid>
            <pubDate>Tue, 14 Mar 2023 09:03:02 GMT</pubDate>
            <description><![CDATA[<h1 id="docker">Docker</h1>
<p>도커 이야기를 더 해볼까요? </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/48ca4275-774d-4d0b-ad0a-3afb3abb200f/image.png" alt=""></p>
<p>앞서 하이퍼바이저와 도커를 비교하면서 도커를 소개했습니다(<a href="https://velog.io/@hyeongjun-hub/%ED%95%98%EC%9D%B4%ED%8D%BC%EB%B0%94%EC%9D%B4%EC%A0%80%EB%B6%80%ED%84%B0-Docker-Desktop%EA%B9%8C%EC%A7%80">링크</a>). 가상머신에 비해 도커가 가볍고 빠르다는 것은 알았는데 만약 가상머신을 사용하지 않는 환경에서 도커를 도입하는 것은 무조건적으로 타당할까요? 도커를 사용하는 것의 장점은 무엇이 더 있을까요? </p>
<h3 id="컨테이너-기술의-장점">컨테이너 기술의 장점</h3>
<p>컨테이너가 VM에 비해 가볍고 빠르다는 경량화의 관한 장점은 이미 언급했습니다.</p>
<p>컨테이너는 컨테이너 런타임이 있는 환경에서는 어디서든 실행 가능하므로 애플리케이션의 <strong>이식성</strong>이 올라간다는 장점을 갖고있습니다. 따라서 애플리케이션 개발 환경/테스트 환경이나 데모 환경 등 높은 이식성이 요구되는 경우부터 검토해 갈 것을 권장드립니다.</p>
<blockquote>
</blockquote>
<p>애플리케이션의 이식성이란, 애플리케이션이 특정 운영체제나 하드웨어 환경에 종속되지 않고 다양한 환경에서 쉽게 이식될 수 있는 능력을 말한다. 이식성이 높은 애플리케이션은 새로운 환경에서 빠르게 배포할 수 있으며, 특정 플랫폼에 종속되지 않기 때문에 이후 플랫폼 변경 등의 작업에도 유연하게 대처할 수 있다.
그래서 클라우드 시스템과의 친화력도 높은 것이 특징이다.</p>
<p>그뿐 아니라 컨테이너는 확장성이 높습니다. 컨테이너는 필요에 따라 수평적으로(scale-out) 확장 가능하므로, 더 많은 트래픽을 처리하거나 병렬 처리를 수행할 수 있습니다.</p>
<p>마지막으로 컨테이너는 다른 컨테이너와 격리되어 있기 때문에 안정성이 높습니다. 또한 컨테이너 안에서 실행되는 애플리케이션은 호스트와 분리되어 있기 때문에 보안성도 높아집니다.</p>
<h3 id="도커의-장점">도커의 장점</h3>
<p>그러면 많은 컨테이너 기술 중에 왜 도커일까요? 일차원적으로 생각하면 <strong>많이 사용해서 입니다.</strong> 
<em>Docker Hub Hits 5 Billion Pulls(2016/08)</em>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/fb13754e-d512-4007-8e5b-8c98bf3d82cc/image.png" alt="">
<em>Docker Hub Hits 396 Billion Pulls(2021/02)</em> 
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/3e2401b7-5e15-4e48-b6fb-f094255090ed/image.png" alt="">
굉장히 많은 도커 기반의 오픈소스들이 있고 도커 개발자 커뮤니티가 활성화 되어 있습니다. 어마어마한 사용량으로 사실상 컨테이너 기술의 de facto가 되었습니다.</p>
<p>또 하나의 도커의 장점은 레이어에 있습니다.</p>
<p>도커 이미지는 컨테이너를 실행하기 위한 모든 정보를 가지고 있기 때문에 보통 용량이 수십메가바이트에서 많게는 수기가바이트에 이릅니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/1e46f903-d822-46e1-a67e-389d7e8408c8/image.png" alt="">
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/322da262-dc3c-426a-b4bf-dee2d989282b/image.png" alt=""></p>
<p>처음 이미지를 다운받을 땐 크게 부담이 안되지만 기존 이미지에 파일 하나 추가했다고 수 기가바이트를 다시 다운받는다면 끔찍합니다.</p>
<p>도커는 이런 문제를 해결하기 위해 레이어라는 개념을 사용하고 리눅스 커널의 유니온 파일 시스템(UnionFS)을 이용하여 여러개의 레이어를 하나의 파일시스템으로 사용할 수 있게 해줍니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/4afaf8f1-57d8-4d86-b12c-6c973595b09d/image.png" alt=""></p>
<p>이미지는 여러개의 읽기 전용 레이어로 구성되고 파일이 추가되거나 수정되면 새로운 레이어가 쌓입니다. ubuntu 이미지가 A + B + C의 집합이라면, ubuntu 이미지를 베이스로 만든 nginx 이미지는 A + B + C + nginx가 됩니다. webapp 이미지를 nginx 이미지 기반으로 만들었다면 A + B + C + nginx + source 레이어로 구성됩니다. 
이러한 읽기 전용 레이어는 다른 이미지와도 공유됩니다. 만약 공통 베이스 이미지를 바탕으로 여러 개의 이미지를 작성한 경우 베이스 이미지의 레이어가 공유됩니다. 
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/34545f44-ea23-48d8-83eb-482fe416164d/image.png" alt="">
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e52b5484-72f1-452d-b61a-549ca61bb1fa/image.png" alt=""></p>
<p>이로써 Docker에서는 디스크의 용량을 효율적으로 이용합니다.</p>
<p>이 외에도 public repository를 무료로 사용할 수 있는 레지스트리 docker hub, docker 만의 독자적인 언어(DSL)를 이용해서 이미지 생성 과정을 적을 수 있는 Dockerfile 등 다양한 장점이 있습니다.</p>
<h3 id="주의점">주의점</h3>
<p>Docker 컨테이너는 트래픽의 증감 등에 맞춰 필요할 때 실행하고 필요 없어지면 파기시키는 일회성 운용에 적합합니다. 파기된 컨테이너는 어떤 데이터도 가지고 있지 않습니다. 그 때문에 컨테이너 안은 중요한 영구 데이터를 저장하는 데는 적합하지 않아요. 
특히 컨테이너에서 민감한 영구 데이터를 다룰 때 주의해야 합니다. 이것을 해결할 Docker의 기능으로서는 데이터 전용 컨테이너에서 데이터를 관리하는 방법이나 로컬 호스트의 디렉토리를 마운트하여 영구 데이터를 저장해두는 방법 등이 있긴 하지만 어떤 방법으로든 데이터의 증가량이나 I/O량은 어느정도인지, 데이터를 어느 정도를 다중화할지, 어떤 운용으로 백업/복원을 해야 할지를 정해 두는 것이 필요합니다.
Docker는 애플리케이션 실행 환경의 측면에서 보면 트래픽의 변동이 큰 대규모 웹 시스템이나 여러 디바이스로부터 데이터를 처리할 필요가 있는 IoT, 머신 리소스를 유효하게 활용하여 대량의 처리를 단시간에 하는 과학기술계산 분야 등에서 위력을 발휘합니다. 이 때문에 비용 절감을 목적으로 기존 시스템을 이전하는 용도보다 지금까지 없었던 새로운 시스템을 검토하거나 도입하는 용도에 더 적합하다고 할 수 있습니다.</p>
<p>결론은 docker가 만능 열쇠처럼 꼭 도입해야 하는 기술이 아니라 상황에 따라 적재적소에 사용해야 한다는 것입니다.</p>
<h1 id="docker-network">Docker Network</h1>
<p>자! 이제 도커를 도입하겠다는 결론을 내려서 컨테이너를 만들게 되었습니다. 보통 컨테이너 하나에는 하나의 어플리케이션을 가동하는 것이 약속입니다. 그러므로 하나의 컴퓨터에서 여러 어플리케이션을 가동하던 이전의 시스템에서 벗어나 여러 컨테이너를 운용하는 환경을 세팅해야 합니다. 
저희가 이번에 다뤄 볼 환경은 네트워크 환경입니다.</p>
<p>Linux는 Docker를 설치하면 서버의 물리 NIC가 docker0이라는 가상 브릿지 네트워크로 연결됩니다. 이 docker0은 Docker가 실행되면 디폴트로 만들어집니다. 컨테이너가 생성이 되면 컨테이너마다 가상 NIC(veth) 또한 생성이 됩니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/34935da2-9df2-485e-8863-a42045e9c357/image.png" alt=""></p>
<p>가상 NIC은 docker0과 연결돼고 이는 L2 스위치 처럼 작동합니다. 가상 브릿지는 docker의 NAPT기능을 이용하여 호스트와 컨테이너의 통신이 가능하도록 합니다. 이것은 NAT 라우터와 비슷해 보입니다. </p>
<blockquote>
<p>NAPT란
하나의 IP 주소를 여러 컴퓨터가 공유하는 기술로 IP주소와 포트번호를 변환하는 기능이다. NAT는 IP주소를 변환하지만 NAPT는 Port 번호까지 변환해준다. 이는 IP 마스커레이드(가면무도회)라고도 불린다.
도커는 이것을 Linux의 iptables를 사용하여 제공한다.</p>
</blockquote>
<h1 id="docker-network의-종류">Docker Network의 종류</h1>
<p>위에서 설명한 네트워크는 도커의 네트워크 종류 중 브릿지 네트워크의 설명입니다. 이외에도 도커는 호스트 네트워크, Overlay 네트워크를 가지고 있습니다.
<code>docker network ls</code>를 통해 현재 기본적으로 가지고 있는 네트워크를 나열해 보겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b979113b-8340-4901-ae14-4e4c32f0d610/image.png" alt=""></p>
<ul>
<li>bridge 네트워크는 하나의 호스트 컴퓨터 내에서 여러 컨테이너들이 서로 소통할 수 있도록 해줍니다.</li>
<li>host 네트워크는 컨터이너를 호스트 컴퓨터와 동일한 네트워크에서 컨테이너를 가동하기 위해서 사용됩니다.</li>
<li>none 네트워크는 네트워크를 사용하지 않는 것입니다.</li>
</ul>
<p>여기에 사용자 임의의 네트워크를 생성하여 사용할 수도 있고 기본 네트워크를 사용할 수 있습니다. default는 bridge라는 이름의 bridge 네트워크입니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e89c3b6a-d41a-4f90-b1a3-d551118587f7/image.png" alt=""></p>
<p>이외에도 </p>
<ul>
<li>container 네트워크는 다른 컨테이너의 네트워크 환경을 공유하기 위해 사용됩니다.</li>
<li>overlay 네트워크는 여러 호스트에 분산되어 돌아가는 컨테이너들 간에 네트워킹을 위해서 사용됩니다.</li>
</ul>
<p>등이 있습니다. </p>
<h1 id="새로운-네트워크-만들어보기">새로운 네트워크 만들어보기</h1>
<p>기존에 사용하던 bridge 네트워크 말고 새로운 bridge 네트워크를 생성해 보겠습니다. </p>
<p><code>$ docker network create --driver bridge mybridge</code></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/f07f1dcc-a44c-413a-9182-c7d40649144e/image.png" alt=""></p>
<p>172.20.0.0/16 대역을 가지고 있습니다. </p>
<p><code>$ docker run -i -t --name mynetwork_container --net mybridge travelping/nettools</code>
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/3c38ef7e-ce43-4546-ba96-8b941ceab2f6/image.png" alt=""></p>
<p>새로 생성한 172.20.0.2 ip 주소를 할당 받았습니다.</p>
<p>기본 네트워크는 간단하게 사용할 수 있지만, 여러 컨테이너를 동시에 실행할 경우 IP 충돌이 발생하거나, 보안 문제 등 다양한 이슈가 발생할 수 있습니다.</p>
<p>또한 기본 네트워크를 사용하면 컨테이너 간의 통신이 호스트를 거쳐서 이루어지기 때문에 성능 저하가 발생할 수 있습니다. 이러한 이유로 기본 네트워크 대신 사용 목적에 맞게 새로운 네트워크를 생성하고, 각 컨테이너에 네트워크를 연결하는 것이 좋습니다.</p>
<p>오늘은 docker와 docker network에 대해 공부 해보았습니다. 피드백은 언제나 환영입니다! 읽어주셔서 감사합니다.</p>
<hr>
<p>참고 :</p>
<ul>
<li><a href="http://www.yes24.com/Product/Goods/64728692">http://www.yes24.com/Product/Goods/64728692</a></li>
<li><a href="https://subicura.com/2017/01/19/docker-guide-for-beginners-1.html">https://subicura.com/2017/01/19/docker-guide-for-beginners-1.html</a></li>
<li><a href="https://captcha.tistory.com/70">https://captcha.tistory.com/70</a></li>
</ul>
<p>이미지 출처: </p>
<ul>
<li>도커의 사용률 : <a href="https://containerjournal.com/features/docker-inc-sees-increased-pulls-from-docker-hub/">https://containerjournal.com/features/docker-inc-sees-increased-pulls-from-docker-hub/</a></li>
</ul>
<p>실습에 사용한 이미지 docker-hub:</p>
<ul>
<li><a href="https://hub.docker.com/r/travelping/nettools/tags">https://hub.docker.com/r/travelping/nettools/tags</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[빌런 호석]]></title>
            <link>https://velog.io/@hyeongjun-hub/%EB%B9%8C%EB%9F%B0-%ED%98%B8%EC%84%9D</link>
            <guid>https://velog.io/@hyeongjun-hub/%EB%B9%8C%EB%9F%B0-%ED%98%B8%EC%84%9D</guid>
            <pubDate>Mon, 13 Mar 2023 11:22:29 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/22251">https://www.acmicpc.net/problem/22251</a></p>
<h1 id="문제">문제</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/fb9e5c90-b115-4b6d-89dd-cf40133bedc1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/946dde27-d9c7-4297-a515-e44eac8f436e/image.png" alt=""></p>
<ul>
<li>N - 바꿀 숫자의 최대값</li>
<li>K - 자리수</li>
<li>P - 바꿀 수 있는 LED 최대 값</li>
<li>X - 현재 층</li>
</ul>
<p>K 자리의 LED 숫자 중 P 개의 LED를 반전시켜 바뀐 숫자가 1 ~ N이 되도록 바꿀 수 있는 숫자의 총 개수</p>
<h1 id="풀이">풀이</h1>
<p>0 ~ 9 까지의 디지털 숫자의 배열을 미리 지정하고
현재 층의 숫자(5층)와 틀린 개수가 P를 넘지 않은 것(3층, 6층, 8층, 9층)의 개수를 구해야 함.
근데 숫자는 N(9층)이 넘지 않아야 하므로
=&gt; 1 ~ N까지 반복하면서 반복하는 숫자와 입력으로 주어진 현재 층의 숫자에서 틀린 개수가 P를 넘지 않도록 하는 층의 개수를 세면 됨</p>
<h1 id="코드">코드</h1>
<p>0 ~ 9까지 디지털로 표현한 배열 저장</p>
<pre><code class="language-java">static int[][] digit = {
        {1, 1, 1, 0, 1, 1, 1}, // 0
        {0, 0, 1, 0, 0, 0, 1}, // 1
        {0, 1, 1, 1, 1, 1, 0}, // 2
        {0, 1, 1, 1, 0, 1, 1}, // 3
        {1, 0, 1, 1, 0, 0, 1}, // 4
        {1, 1, 0, 1, 0, 1, 1}, // 5
        {1, 1, 0, 1, 1, 1, 1}, // 6
        {0, 1, 1, 0, 0, 0, 1}, // 7
        {1, 1, 1, 1, 1, 1, 1}, // 8
        {1, 1, 1, 1, 0, 1, 1}, // 9
};
static int N, K, P, X, ans;</code></pre>
<p>숫자를 디지털 배열로 바꾸는 함수</p>
<pre><code class="language-java">public static int[][] numToDigit(int num) {

    int[][] makeFloor = new int[K][7];

    for (int i = 0; i &lt; K; i++) {
        int cur = num % 10;
        num /= 10;
        makeFloor[K-i-1] = digit[cur];
    }

    return makeFloor;
}</code></pre>
<p>입력으로 받은 현재 층을 디지털로 변환</p>
<pre><code class="language-java">// 현재 층을 디지털로 변환
int[][] currentFloor = numToDigit(X);</code></pre>
<p>1 ~ N 까지 순회</p>
<pre><code class="language-java">for (int i = 1; i &lt;= N; i++) {
    // 1부터 N까지 틀린 개수 P를 안넘는 것 몇 개?
    // rising
    if(i == X) continue;
    int[][] risingNum = numToDigit(i);

    int diffNum = 0;
    for (int j = 0; j &lt; K; j++) {
        for (int k = 0; k &lt; 7; k++) {
            if(currentFloor[j][k] != risingNum[j][k]) diffNum++;
        }
    }
    if(diffNum &lt;= P) ans++;
}
System.out.println(ans);</code></pre>
<p>1 ~ N까지 순회하는 숫자(<code>i</code>)의 디지털 배열이 현재 층 숫자(<code>X</code>)의 디지털 배열과 틀린 개수 몇갠지 세고 P보다 적으면 <code>ans++</code></p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">package baekjoon._22251;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    static int[][] digit = {
            {1, 1, 1, 0, 1, 1, 1}, // 0
            {0, 0, 1, 0, 0, 0, 1}, // 1
            {0, 1, 1, 1, 1, 1, 0}, // 2
            {0, 1, 1, 1, 0, 1, 1}, // 3
            {1, 0, 1, 1, 0, 0, 1}, // 4
            {1, 1, 0, 1, 0, 1, 1}, // 5
            {1, 1, 0, 1, 1, 1, 1}, // 6
            {0, 1, 1, 0, 0, 0, 1}, // 7
            {1, 1, 1, 1, 1, 1, 1}, // 8
            {1, 1, 1, 1, 0, 1, 1}, // 9
    };
    static int N, K, P, X, ans;

    public static void input() {
        FastReader fr = new FastReader();
        N = fr.nextInt();
        K = fr.nextInt();
        P = fr.nextInt();
        X = fr.nextInt();
    }

    public static void main(String[] args) {
        input();

        int[][] currentFloor = numToDigit(X);
        // 현재 층을 디지털로 변환


        for (int i = 1; i &lt;= N; i++) {
            // 1부터 N까지 틀린 개수 P를 안넘는 것 몇 개?
            // rising
            if(i == X) continue;
            int[][] risingNum = numToDigit(i);

            int diffNum = 0;
            for (int j = 0; j &lt; K; j++) {
                for (int k = 0; k &lt; 7; k++) {
                    if(currentFloor[j][k] != risingNum[j][k]) diffNum++;
                }
            }
            if(diffNum &lt;= P) ans++;
        }
        System.out.println(ans);
    }

    public static int[][] numToDigit(int num) {
        int[][] makeFloor = new int[K][7];

        for (int i = 0; i &lt; K; i++) {
            int cur = num % 10;
            num /= 10;
            makeFloor[K-i-1] = digit[cur];
        }

//        StringBuilder sb = new StringBuilder();
//        for (int i = 0; i &lt; K; i++) {
//            for (int j = 0; j &lt; 7; j++) {
//                sb.append(makeFloor[i][j]);
//                sb.append(&quot; &quot;);
//            }
//            sb.append(&quot;\n&quot;);
//        }
//
//        System.out.println(sb);
        return makeFloor;
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader(){ br = new BufferedReader(new InputStreamReader(System.in));}

        String next(){
            while(st == null || !st.hasMoreTokens()){
                try{
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() { return Integer.parseInt(next()); }

        long nextLong() { return Long.parseLong(next()); }

        Double nextDouble() { return Double.parseDouble(next()); }

        String nextLine(){
            String str = &quot;&quot;;
            try{
                str = br.readLine();
            } catch(IOException e){
                e.printStackTrace();
            }
            return str;
        }
    }
}
</code></pre>
<h1 id="느낀점">느낀점</h1>
<p>처음 부터 지레 겁먹고 못풀겠다고 생각하지말고 일단 도전하면서 생각해보는 게 답이 될 수도!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[불!]]></title>
            <link>https://velog.io/@hyeongjun-hub/%EB%B6%88</link>
            <guid>https://velog.io/@hyeongjun-hub/%EB%B6%88</guid>
            <pubDate>Tue, 07 Mar 2023 05:39:49 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/4179">https://www.acmicpc.net/problem/4179</a></p>
<h1 id="문제">문제</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/e9398030-1b8f-49fd-bb92-d2cf6446fba9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b475c6cd-74de-4069-867c-d5641c6b7324/image.png" alt=""></p>
<h1 id="풀이">풀이</h1>
<ol>
<li><p>fireMap이라는 새로운 맵을 만들어 최대값으로 초기화 한 후 Fire이 가는 시간(거리)을 채운다.
(예시 테스트케이스의 fireMap)
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/1068e2c1-dd37-44ef-b01a-caa422ee6708/image.png" alt=""></p>
</li>
<li><p>지훈이 탈출할 수 있는 시간을 구한다.
 a. 탈출구간에 fire이 먼저 왔으면(fireMap의 값이 더 적으면) 탈출 불가능</p>
</li>
<li><p>탈출 못하면 IMPOSSIBLE 출력</p>
</li>
</ol>
<h1 id="코드">코드</h1>
<pre><code class="language-java">for (int i = 0; i &lt; R; i++) {
    String s = fr.nextLine();
    for (int j = 0; j &lt; C; j++) {
        map[i][j] = s.charAt(j);
        fireMap[i][j] = R*C+1;
        if(map[i][j] == &#39;J&#39;){
            jr = i;
            jc = j;
        }
    }
}</code></pre>
<p>지훈이의 좌표는 따로 채워둔다. fire의 좌표는 여러개가 있을 수 있으므로 나중에 탐색하기로 한다. 
fireMap은 입력에서 나올 수 있는 최대값으로 채워놓는다. 나중에 bfs하면서 갱신한다.</p>
<pre><code class="language-java">for (int i = 0; i &lt; R; i++) {
    for (int j = 0; j &lt; C; j++) {
        if(map[i][j] == &#39;F&#39;) bfs(i, j, true);
    }
}
bfs(jr, jc, false);
System.out.println(answer);</code></pre>
<p>탐색하면서 Fire을 찾는다. 찾으면 bfs.
fireMap을 다 채우고 나면 지훈이의 좌표로 bfs해 정답 출력한다.</p>
<p>bfs 코드를 살펴보자</p>
<pre><code class="language-java">public static void bfs(int r, int c, boolean isFire) {
    boolean[][] visit = new boolean[R][C];
    Queue&lt;int[]&gt; queue = new LinkedList&lt;&gt;();
    queue.add(new int[]{r, c, 1});
    visit[r][c] = true;
    while (!queue.isEmpty()) {
        int[] poll = queue.poll();
        r = poll[0];
        c = poll[1];
        int dis = poll[2];
        if (isFire) fireMap[r][c] = Math.min(dis, fireMap[r][c]);
        for (int i = 0; i &lt; 4; i++) {
            int nr = r + dir[i][0];
            int nc = c + dir[i][1];
            if (nr &lt; 0 || nc &lt; 0 || nr &gt;= R || nc &gt;= C) {
                if (!isFire) {
                    if (dis &lt; fireMap[r][c]) {
                        answer = String.valueOf(dis);
                        return;
                    }
                }
                continue;
            }
            if (map[nr][nc] != &#39;.&#39;) continue;
            if (visit[nr][nc]) continue;
            visit[nr][nc] = true;
            queue.add(new int[]{nr, nc, dis + 1});
        }
    }
}</code></pre>
<p>queue에는 r값, c값, distance값이 들어간다.
평상시와 다르게 boolean 값을 포함하는데 fire이 bfs하는지 지훈이 bfs하는지를 판단하기 위한 boolean 값이다.
fire이면 fireMap을 채워야 하고
지훈이면 정답을 출력해야 하기 때문</p>
<ul>
<li>만약 fire이면 <ul>
<li>불이 갈 수 있는 위치에 시간(거리)을 기록한다. 
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/5b3dd1dc-6028-4245-bbae-570c37a7e673/image.png" alt=""></li>
</ul>
</li>
<li>만약 지훈이면(<code>!isFire</code>)<ul>
<li>가장자리를 만나면 fireMap의 값과 비교한다.<ul>
<li>fireMap이 더 적어서 먼저 fireMap이 도착하였으면 탈출할 수 없다.</li>
<li>지훈이의 dis값이 fireMap보다 적어서 빨리 도착했으면 탈출 할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">package baekjoon._4179;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
    static String answer = &quot;IMPOSSIBLE&quot;;
    static int R, C;
    static char[][] map;
    static int[][] fireMap;
    static int[][] dir = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

    public static void input() {
        FastReader fr = new FastReader();
        R = fr.nextInt();
        C = fr.nextInt();
        map = new char[R][C];
        fireMap = new int[R][C];
        int jr = 0;
        int jc = 0;

        for (int i = 0; i &lt; R; i++) {
            String s = fr.nextLine();
            for (int j = 0; j &lt; C; j++) {
                map[i][j] = s.charAt(j);
                fireMap[i][j] = R * C + 1;
                if (map[i][j] == &#39;J&#39;) {
                    jr = i;
                    jc = j;
                }
            }
        }

        for (int i = 0; i &lt; R; i++) {
            for (int j = 0; j &lt; C; j++) {
                if (map[i][j] == &#39;F&#39;) bfs(i, j, true);
            }
        }
        bfs(jr, jc, false);
        System.out.println(answer);
    }


    public static void bfs(int r, int c, boolean isFire) {
        boolean[][] visit = new boolean[R][C];
        Queue&lt;int[]&gt; queue = new LinkedList&lt;&gt;();
        queue.add(new int[]{r, c, 1});
        visit[r][c] = true;
        while (!queue.isEmpty()) {
            int[] poll = queue.poll();
            r = poll[0];
            c = poll[1];
            int dis = poll[2];
            if (isFire) fireMap[r][c] = Math.min(dis, fireMap[r][c]);
            for (int i = 0; i &lt; 4; i++) {
                int nr = r + dir[i][0];
                int nc = c + dir[i][1];
                if (nr &lt; 0 || nc &lt; 0 || nr &gt;= R || nc &gt;= C) {
                    if (!isFire) {
                        if (dis &lt; fireMap[r][c]) {
                            answer = String.valueOf(dis);
                            return;
                        }
                    }
                    continue;
                }
                if (map[nr][nc] != &#39;.&#39;) continue;
                if (visit[nr][nc]) continue;
                visit[nr][nc] = true;
                queue.add(new int[]{nr, nc, dis + 1});
            }
        }
    }

    public static void main(String[] args) {
        input();
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        String next() {
            while (st == null || !st.hasMoreTokens()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() {
            return Integer.parseInt(next());
        }

        String nextLine() {
            String str = &quot;&quot;;
            try {
                str = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return str;
        }
    }
}
</code></pre>
<h1 id="느낀점">느낀점</h1>
<p>98% 쯤에서 오답이 나왔는데 fireMap의 초기화 값을 R*C로 해서였다. 나올 수 있는 최대값보다 +1을 더해야했다. 
그리고 처음에는 fireBFS와 jBFS로 bfs의 함수를 두 개로 나눴지만 refactoring하여 하나의 함수로 합쳤다.
BFS 문제는 풀 때마다 재밌는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[틱택토]]></title>
            <link>https://velog.io/@hyeongjun-hub/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%8B%B1%ED%83%9D%ED%86%A0</link>
            <guid>https://velog.io/@hyeongjun-hub/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%8B%B1%ED%83%9D%ED%86%A0</guid>
            <pubDate>Fri, 03 Mar 2023 09:35:07 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/7682">https://www.acmicpc.net/problem/7682</a></p>
<h1 id="문제">문제</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/cfea6d4a-85c7-40df-a49d-842e665504d6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/b7e33deb-2f09-4c8b-b413-329f47d30048/image.png" alt=""></p>
<p>입력에 주어진 상태가 틱택토게임이 정상적으로 끝난 상태인지 묻는 문제였다.</p>
<p>틱택토게임판의 크기가 3*3의 작은 크기임을 이용해서 풀어야겠다고 생각했다.</p>
<p>테스트케이스를 그림으로 그리면서 invalid인 이유를 써내려갔다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/3b894681-a7a8-4eda-8b1a-6f6a8d3d1570/image.jpg" alt=""></p>
<p>그림을 보면서 규칙을 찾을 수 있었다.</p>
<h1 id="풀이">풀이</h1>
<p>X가 항상 먼저 공격하므로
무조건 X의 개수가 3<del>5개이고 Y의 개수가 2</del>4개여야 한다.
처음 이 조건이 만족되지 않으면 바로 invalid이다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/a065dbfe-1a1c-4026-961d-9078a101b569/image.jpg" alt=""></p>
<p>그 다음으로는 그 경기가 끝이 났는지 판별했다. X의 개수와 Y의 개수, 빈 공간의 개수를 세면서 빈공간이 하나도 없으면 게임이 끝날 수 있다.
또한 그 전에 누군가가 승리한다면 게임이 끝날 수 있다.</p>
<ol>
<li>모두 채워져서 게임이 끝났을 때
 a. X = 5개, O = 4개로 끝나야 하고, O가 승리하지 않아야 한다.</li>
<li>누군가가 승리했을 때
 a. X가 승리했을 때 -&gt; X가 O보다 하나 더 많아야 한다.
 b. Y가 승리했을 때 -&gt; X와 O의 개수가 같아야 한다.
 c. 동시에 승리하면 안된다.</li>
</ol>
<p>위의 조건을 만족하지 않으면 모두 invalid로 취급한다.</p>
<h1 id="코드">코드</h1>
<pre><code class="language-java">map = new char[3][3];
int x = 0;
int o = 0;
int dot = 0;

for (int i = 0; i &lt; 9; i++) {
    map[i / 3][i % 3] = s.charAt(i);
    if (s.charAt(i) == &#39;X&#39;) x++;
    if (s.charAt(i) == &#39;O&#39;) o++;
    if (s.charAt(i) == &#39;.&#39;) dot++;
}</code></pre>
<p>X, O, .의 개수를 센다</p>
<p><code>if (x &gt; 5 || x &lt; 3 || o &gt; 4 || o &lt; 2){ System.out.println(&quot;invalid&quot;); return;}</code>
만약 있을 수 없는 개수이면 바로 invalid</p>
<pre><code class="language-java">// 모두 채워졌을 경우
if (dot == 0 &amp;&amp; x == 5 &amp;&amp; o == 4) {
    // O 이 이기지 않아야 함
    if (judge(&#39;O&#39;)) {
        System.out.println(&quot;invalid&quot;);
        return;
    }

    System.out.println(&quot;valid&quot;);
    return;
}</code></pre>
<p>모두 채워졌을 경우이다.</p>
<pre><code class="language-java">// 누군가 이겼을 경우
// X가 이겼을 때
if (judge(&#39;X&#39;) &amp;&amp; !judge(&#39;O&#39;)) {
    if(x == o + 1) {
        System.out.println(&quot;valid&quot;);
        return;
    }
} else if (!judge(&#39;X&#39;) &amp;&amp; judge(&#39;O&#39;)) {
    if (x == o) {
        System.out.println(&quot;valid&quot;);
        return;
    }
}
</code></pre>
<p>누군가 승리했을 경우이다.</p>
<p>이 외의 나머지는 전부 invalid를 출력한다.</p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">package baekjoon._7682;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    static char[][] map;

    public static void input() {
        FastReader fr = new FastReader();
        while (true) {
            String s = fr.nextLine();
            if (s.equals(&quot;end&quot;)) return;
            pro(s);
        }
    }

    public static void pro(String s) {
        map = new char[3][3];
        int x = 0;
        int o = 0;
        int dot = 0;

        for (int i = 0; i &lt; 9; i++) {
            map[i / 3][i % 3] = s.charAt(i);
            if (s.charAt(i) == &#39;X&#39;) x++;
            if (s.charAt(i) == &#39;O&#39;) o++;
            if (s.charAt(i) == &#39;.&#39;) dot++;
        }

        if (x &gt; 5 || x &lt; 3 || o &gt; 4 || o &lt; 2){ System.out.println(&quot;invalid&quot;); return;}


        // 경기 끝났는지 확인
        // 모두 채워졌을 경우
        if (dot == 0 &amp;&amp; x == 5 &amp;&amp; o == 4) {
            // o 이 이기지 않아야 함
            if (judge(&#39;O&#39;)) {
                System.out.println(&quot;invalid&quot;);
                return;
            }

            System.out.println(&quot;valid&quot;);
            return;
        }

        // 누군가 이겼을 경우
        // X가 이겼을 때
        if (judge(&#39;X&#39;) &amp;&amp; !judge(&#39;O&#39;)) {
            if(x == o + 1) {
                System.out.println(&quot;valid&quot;);
                return;
            }
        } else if (!judge(&#39;X&#39;) &amp;&amp; judge(&#39;O&#39;)) {
            if (x == o) {
                System.out.println(&quot;valid&quot;);
                return;
            }
        }

        System.out.println(&quot;invalid&quot;);
    }

    public static boolean judge(char c) {
        for(int i=0; i&lt;3; i++) {
            if(map[i][0] == c &amp;&amp; map[i][1] == c &amp;&amp; map[i][2] == c) {
                return true;
            }
        }
        // 세로 
        for(int i=0; i&lt;3; i++) {
            if(map[0][i] == c &amp;&amp; map[1][i] == c &amp;&amp; map[2][i] == c) {
                return true;
            }
        }
        // 대각선 
        if(map[0][0] == c &amp;&amp; map[1][1] == c &amp;&amp; map[2][2] == c) {
            return true;
        }

        if(map[2][0] == c &amp;&amp; map[1][1] == c &amp;&amp; map[0][2] == c) {
            return true;
        }

        return false;
    }


    public static void main(String[] args) {
        input();
    }

    static class FastReader {
        BufferedReader br;
        StringTokenizer st;

        public FastReader() {
            br = new BufferedReader(new InputStreamReader(System.in));
        }

        String next() {
            while (st == null || !st.hasMoreTokens()) {
                try {
                    st = new StringTokenizer(br.readLine());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return st.nextToken();
        }

        int nextInt() {
            return Integer.parseInt(next());
        }

        long nextLong() {
            return Long.parseLong(next());
        }

        Double nextDouble() {
            return Double.parseDouble(next());
        }

        String nextLine() {
            String str = &quot;&quot;;
            try {
                str = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return str;
        }
    }
}
</code></pre>
<h1 id="느낀점">느낀점</h1>
<p>처음엔 BFS나 DFS로 승리했는지 판단해야하나 했는데 3*3인 맵을 적극반영해 손쉽게 O(3)으로 판단가능했다.
또한 BFS, DFS는 같은 방향으로 나가야 하는 데 코드짜기가 복잡
문제의 특성을 잘 파악하고 문제를 풀어나가자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EBS부터 Linux Mount까지]]></title>
            <link>https://velog.io/@hyeongjun-hub/EBS%EB%B6%80%ED%84%B0-Linux-Mount%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@hyeongjun-hub/EBS%EB%B6%80%ED%84%B0-Linux-Mount%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 28 Feb 2023 10:07:00 GMT</pubDate>
            <description><![CDATA[<h1 id="aws-ebs">AWS EBS?</h1>
<p>AWS를 공부하는 친구와 이것 저것 얘기하다가 <strong>EBS</strong>에 대한 얘기가 나왔습니다. 데이터를 저장소인데 무엇일까요?</p>
<h3 id="ebs">EBS</h3>
<p>EBS는 Elastic Block Storage의 약자로 AWS에서 제공하는 블록 수준 스토리지 서비스입니다.</p>
<p>EC2(Elastic Compute Cloud) 인스턴스를 만들고 중지하거나 삭제한다면 기존에 EC2가 가지고 있던 데이터가 <strong>삭제</strong>되는 문제점이 있습니다. 이 데이터들을 바깥에 따로 보관한다면 그런 걱정을 할 필요 없겠죠. </p>
<p>제가 직접 한 번 인스턴스를 생성하고 EBS를 연결해 보도록 하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/56eb9411-0136-4942-9df5-10a3b65aee49/image.png" alt=""></p>
<p>인스턴스를 구성할 때 네트워크 설정을 마친 뒤 스토리지 구성을 보면
기본적으로 루트 볼륨이 설정되어 있습니다. 여기서 새 볼륨을 추가하면</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/a6c5ea03-42c1-4e59-9b2b-b5bd93301f64/image.png" alt=""></p>
<p>그림과 같이 8GB의 EBS 볼륨을 추가하게 됩니다.
루트 볼륨은 뭐고 EBS 볼륨은 뭘까요? 자세히 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/ba6bfa1e-b221-4452-98d8-459fd1df7833/image.png" alt=""></p>
<p>볼륨 1은 (AMI루트)라고 나와있습니다. 이것은 컴퓨터를 실행하기 위해서 필요한 스토리지입니다. 쉽게 말해서 컴퓨터가 켜지고 실행되기 위해서는 OS가 필요한데 이 OS는 2차 스토리지에 설치되어 있어야 합니다. 이런 컴퓨터의 기본 프로그램들을 AWS AMI 스냅샷으로부터 바로 받아와서 컴퓨터 부팅에 사용하는 것입니다.</p>
<p>여기서 중요한 점은 볼륨 1인 루트 볼륨의 스토리지 유형도 EBS로 나타납니다. 결국 <strong>인스턴스가 기본적으로 사용하는 2차 메모리도 EBS입니다.</strong></p>
<p>제가 추가한 볼륨 2는 스토리지 유형이 EBS이고 디바이스 이름은 /dev/sdb입니다. 리눅스는 스토리지를 파일단위(특수파일)로 관리하죠? 그 디바이스 파일을 /deb/sdb를 사용하는 것입니다. 이 파일은 나중에 파일시스템을 포맷할 때 사용해야 합니다. 자세한 내용은 뒤에서 설명하겠습니다.
이 외에도 크기와 볼륨 유형, IOPS(Input/output Operations Per Second), 종료시 삭제, 암호화 여부를 묻습니다. 종료시 삭제를 아니요로 설정하면 인스턴스의 생명주기와 별도로 EBS 볼륨이 관리됩니다.</p>
<p>이렇게 인스턴스 설정을 한 뒤 생성을 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/d4027eb8-eb2e-40fc-a091-6e1e9042ff49/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/aa183143-5386-4bfb-94f8-49d98d1bb220/image.png" alt=""></p>
<p>인스턴스가 정상적으로 실행되었고 볼륨에 생성한 볼륨과 root볼륨이 있는 것을 확인했습니다. Name을 new와 root로 지정해서 구분하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/0ef64de1-904b-4ba4-8182-cfa9fbbbccae/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/beff57f5-6c70-4f61-9df6-a4b2ab3b7e56/image.png" alt=""></p>
<p>root 볼륨은 자동으로 마운트되어 사용할 수 있지만 (운영체제가 설치되고 컴퓨터가 부팅하기 위해서 자동으로 마운트) 새로 생성한 빈 ebs 볼륨은 인스턴스와 연결(attach)만 되어있고 사용할 수는 없는 상태입니다. 이것을 사용하려면 파일시스템을 포맷하고 마운트하는 작업이 필요합니다. 그럼 여기서 <strong>파일시스템</strong>은 뭐고 <strong>마운트</strong>는 뭘까요?</p>
<h1 id="파일시스템">파일시스템</h1>
<p>파일시스템(File System)은 운영체제에서 파일을 저장하고 관리하는 방식입니다. 파일 시스템은 파일이나 디렉토리를 생성하고 저장하며, 디스크 공간을 효율적으로 사용하기 위한 알고리즘을 제공합니다. 일반적으로 파일 시스템은 블록 단위로 데이터를 저장하며, 파일 시스템의 종류에 따라 데이터를 저장하는 방식이나 알고리즘 등이 다를 수 있습니다. 예를 들어, Linux 운영체제에서는 ext4, XFS 등의 파일 시스템이 주로 사용되고 있으며, Windows 운영체제에서는 NTFS, FAT32 등의 파일 시스템이 사용됩니다</p>
<h3 id="파티션">파티션</h3>
<p>하드디스크에 빈 공간이 있다고 막 저장하지 않는다는 것이죠. 운영체제가 파일을 관리하는 시스템이 스토리지에서 설정되어 있어야 하는 것입니다. 여기서 말하는 스토리지는 하나의 파티션을 의미합니다. 하드디스크는 여러개의 파티션을 나뉠 수 있습니다. 대표적으로 Windows에서 C드라이브와 D드라이브로 나누어 데이터를 저장하는 것이 파티션을 나눈 것입니다. </p>
<h3 id="블록">블록</h3>
<p>넘어가기 전에 설명하고 싶은 것이 있습니다. 이전에 제가 EBS가 어느 수준의 스토리지라고 했죠? <strong>블록 수준</strong>이라고 했습니다. 여기서 블록은 뭘까요? 블록은 섹터들의 집합으로 보통 4KB를 사용합니다. 파일시스템은 블록을 최소단위로 데이터들을 Input Output 합니다. EBS도 블록을 최소단위로 데이터를 저장하기 때문에 블록 수준의 스토리지라고 하는 것입니다. 이와 반대로 EFS는 파일 수준의 스토리지입니다.  </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/60f28353-a1b5-4d94-8ea3-9ac5927d60d2/image.png" alt=""></p>
<blockquote>
<p>섹터(Sector)란?
하드디스크가 원통형으로 생긴 것은 다들 알 것으로 생각한다. 하드디스크는 데이터를 저장할 때 원통으로 생긴 저장소 중 하나의 원판(플래터)을 트랙으로 나누고 트랙을 섹터로 나누는데 이 때 하나의 섹터를 데이터를 저장하는 최소 단위로 사용한다. 보통 512Byte를 사용하는데 이것은 운영체제가 데이터를 읽고 쓰는데 너무 작기 때문에 여러 섹터를 묶어 하나의 블록이라는 단위를 만들고 I/O의 최소단위로 쓰는 것이다. 블록은 윈도우 운영체제에서 클러스터라 부르기도 한다.</p>
</blockquote>
<p>하지만 EBS는 여러 파티션으로 나눌 수 있지만 하나의 파티션으로 사용하는 것이 일반적입니다. 이제는 마운트에대해 알아보겠습니다.</p>
<h1 id="마운트">마운트?</h1>
<p>마운트는 왜 할까요? 꼭 필요한 걸까요? 어느 블로거의 표현을 빌리자면 마운트는 배를 정박하는 것과 같은 개념입니다. 배는 디스크의 파티션이고, 파일시스템은 이 배의 선장, 디렉토리는 선착장입니다. 마운트를 하지않으면 해당 하드디스크의 공간을 운영체제가 사용할 수 없습니다. </p>
<p>빈 하드디스크 하나가 있습니다. 이것을 리눅스 운영체제 컴퓨터와 연결하면 자연스럽게 빈 공간이 뿅 생길 것 같지만 (윈도우는 자동으로 마운트가 되기 때문에 뿅 생기긴 한다) 리눅스 운영체제는 이 하드디스크가 추가되었는지 이 기기가 뭔지 모릅니다. 그래서 이 새로생긴 배(디스크파티션)에 선장(파일시스템)을 배치하고 배들이 정박되어 있는 선착장(디렉토리)에 정박해야 합니다.</p>
<p>마운트를 하는 과정을 알아봅시다.</p>
<ol>
<li><p>디스크 추가
당연히 사용하려면 디스크(선박)가 있어야죠?</p>
</li>
<li><p>디스크 파티션을 나눈 뒤 사용할 시스템에 맞춰 타입 정하기
(전투함인지, 수송함인지, 그냥 뗏목인지)
세상에 수 많은 배가 있듯이, 시스템도 수 많은 시스템(XFS, FAT32..)이 있습니다. 그리고 당연히 그 시스템에 맞는 디스크 파티션을 만들어야겠죠?
대표적으로 <code>fdisk</code> 명령어를 사용합니다.</p>
</li>
<li><p>용도에 맞게 파일시스템 포맷하기
(전투함이면 전투함을 사용할 줄 아는 선장을 배치해야하고, 수송함이면 수송함에 특화된 선장을 배치해야겠죠?)
당연한 거죠. 전투함에 일반 선박 선장을 배치하면 제대로 조종을 할 수 있겠습니까?
대표적으로 <code>mkfs</code> 명령어를 사용합니다.</p>
</li>
<li><p>디스크를 마운트 할 마운트 포인트(디렉토리)를 만들어 둔다
(선박이 선착할 수 있는 선착장을 마련한다)
선착장이 없으면 배가 선착할 수가 없겠죠? 그리고 일반 파일 공간을 선착장이라고
마련하면 배가 선착할 수 없겠죠? 배가 들어갈 크기의 공간 &quot;디렉토리&quot;정도가 필요할 것입니다.
<code>mkdir</code> 명령어를 이용해서 간단하게 디렉토리를 만듭시다.</p>
</li>
<li><p>마운트하기
이제 배가 선착하는 마운트 명령을 내립니다.
<code>mount</code> 명령어를 이용해 디렉토리와 디바이스 파일을 일시적으로 마운트하거나
/etc/fstab 파일을 수정해 영구적으로 마운트할 수 있습니다.</p>
</li>
</ol>
<p>여기서 더깊게 들어가면 글이 길어지므로 자세한 내용은 추후에 따로 블로깅 하겠습니다.</p>
<p>이제 EBS가 왜 마운트해야 사용할 수 있는지 알았으니 다시 AWS EBS로 넘어와서 인스턴스에 연결해 봅시다.</p>
<h1 id="ebs-volume의-mount">EBS volume의 Mount</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/00fc89f9-93f7-4b49-846b-4bb9da012ac1/image.png" alt=""></p>
<blockquote>
<p>여담으로 인스턴스에 처음 연결하면 습관처럼 하는 것이 <code>sudo yum update -y</code> 입니다. 왜 업데이트를 해야하는지 생각해보신적 있으신가요? 인스턴스가 AMI으로부터 운영체제, 소프트웨어 패키지들을 현재 인스턴스의 root 볼륨에 마운트했지만 최신 버전의 패키지가 아닐 수도 있기 때문에 최신 버전으로의 업데이트를 추천하는 것입니다. AMI가 최신 버전의 소프트웨어 패키지들을 가지고 있으면 최고겠지만 현실적으로 그럴 수가 없습니다.</p>
</blockquote>
<p><code>lsblk</code>(list block devices)을 통해 사용 가능한 디스크 디바이스 및 마운트 포인트(해당하는 경우)를 출력합니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/884ef746-a73c-40b5-bfc3-93b474ec7f0a/image.png" alt=""></p>
<p>루트 디바이스는 xvda1이라는 파티션이 하나 있는 /dev/xvda입니다. 연결된 볼륨은 파티션이 없고 아직 탑재되지 않은 /dev/xvdb입니다.</p>
<p>볼륨에 파일 시스템이 있는지 확인합니다. 새 볼륨은 원시 블록 디바이스이므로 볼륨을 탑재하고 사용하기 전에 해당 볼륨에서 파일 시스템을 생성해야 합니다. 스냅샷에서 생성된 볼륨에는 이미 파일 시스템이 있을 수 있습니다. 기존 파일 시스템 위에 새 파일 시스템을 생성하면 해당 작업으로 데이터가 덮어쓰기됩니다.</p>
<p><code>file -s</code> 명령을 사용하면 파일 시스템 유형 등의 특정 디바이스 정보를 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/d611b5ac-adec-4a9e-a1da-62d7d367b4dd/image.png" alt=""></p>
<p>data가 출력되면 파일시스템이 없는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6b32c0d6-eafd-4b01-8e7e-b535c9d168df/image.png" alt=""></p>
<p><code>lsblk -f</code> 명령으로도 파일시스템을 확인할 수 있습니다. /dev/xvdb에는 파일시스템이 없습니다.</p>
<h3 id="파일시스템-포맷">파일시스템 포맷</h3>
<p>EBS 볼륨은 파티셔닝을 보통 생략하기 때문에 바로 파일시스템을 포맷해보겠습니다.
<code>mkfs</code> 명령을 통해 linux에서 주로 사용되는 xfs 파일시스템을 포맷합니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/42f273de-0bda-463a-9c3d-89730d44ce9b/image.png" alt=""></p>
<h3 id="마운트포인트디렉토리-생성">마운트포인트(디렉토리) 생성</h3>
<p>/data라는 새로운 디렉토리를 만들고 볼륨을 마운트하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/74cacdc0-99d6-46db-b37f-8aa8a67c5de0/image.png" alt=""></p>
<h3 id="마운트-1">마운트</h3>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/96e62d7b-4f0a-46ed-bb17-a72646e3d55f/image.png" alt=""></p>
<p>마운트한 뒤 마운트 정보를 출력하는 <code>mount</code> 명령을 입력했습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6bc0d542-48a8-4f0e-b861-06229179fb34/image.png" alt=""></p>
<p>마운트가 성공적으로 이루어진 것을 볼 수 있습니다.</p>
<p>마운트를 해제할 때는 <code>unmount</code> 명령을 이용해 손쉽게 해제할 수 있습니다.</p>
<p><code>df</code> 명령을 이용해 시스템이 사용할 수 있는 디스크 공간을 확인해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/abc5cd95-26bb-4835-b999-1d4834887619/image.png" alt=""></p>
<p>EBS에서 만든 8GB의 공간을 사용할 수 있는 것을 확인할 수 있습니다! </p>
<p>하지만 앞서 말했지만 <code>mount</code> 명령은 일시적 마운트라서 인스턴스가 꺼졌다가 켜지면 인스턴스는 마운트했던 것을 까먹습니다.
따라서 /etc/fstab파일(file system table)을 수정해 영구적 마운트를 해야합니다. 진짜 인스턴스 재부팅후 언마운트 됐는지 확인해보기 위해 /data에 파일 abc를 만들고 재부팅해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/9b588d68-fa19-4fdc-a662-5ec916034491/image.png" alt=""></p>
<p>재부팅 후</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6f18f445-4837-4da5-acca-d4a3af7e7b76/image.png" alt=""></p>
<p>마운트포인트가 사라졌고 /data에 아무 파일이 없는 것을 확인할 수 있습니다.</p>
<h3 id="영구적-마운트">영구적 마운트</h3>
<p>이제 부팅때마다 귀찮게 마운트하는 것을 방지하기위해 얼른 /etc/fstab을 수정하도록 합시다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/8150c2b9-a763-470d-a8e2-9e50593312d8/image.png" alt=""></p>
<p>현재 /etc/fstab 파일의 모습입니다. xvdb의 UUID를 복사해서 붙여놓고 위의 형식과 같게 작성해봅시다.</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/12f3720e-a72d-4275-9401-5415f54361b5/image.png" alt=""></p>
<p>/etc/fstab을 잘못건들인다면 부팅이 안될 수도 있으니 수정에 조심해야합니다.
이제 재부팅해서 다시 확인해봅시다! 두구두구</p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/d996d05f-8e9e-4001-89db-9c8c739d935f/image.png" alt=""></p>
<p>오늘은 AWS의 EBS와 Linux에서 파일시스템의 mount에 대해 알아보았습니다. 피드백은 언제나 환영입니다.</p>
<hr>
<p>참고: </p>
<ul>
<li>AWS EBS
<a href="https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/AmazonEBS.html">https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/AmazonEBS.html</a></li>
<li>EBS 볼륨 유형 
<a href="https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ebs-volume-types.html">https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ebs-volume-types.html</a></li>
<li>FileSystem 
<a href="https://mamu2830.blogspot.com/2019/10/chs-lba.html">https://mamu2830.blogspot.com/2019/10/chs-lba.html</a></li>
<li>mount
<a href="https://mamu2830.blogspot.com/2019/11/fdisk-df-etcfstab-blkid.html">https://mamu2830.blogspot.com/2019/11/fdisk-df-etcfstab-blkid.html</a></li>
<li>mount 실습
<a href="https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ebs-using-volumes.html">https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/ebs-using-volumes.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[하이퍼바이저부터 Docker Desktop까지]]></title>
            <link>https://velog.io/@hyeongjun-hub/%ED%95%98%EC%9D%B4%ED%8D%BC%EB%B0%94%EC%9D%B4%EC%A0%80%EB%B6%80%ED%84%B0-Docker-Desktop%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@hyeongjun-hub/%ED%95%98%EC%9D%B4%ED%8D%BC%EB%B0%94%EC%9D%B4%EC%A0%80%EB%B6%80%ED%84%B0-Docker-Desktop%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 20 Feb 2023 10:11:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/0816b370-7d6d-487f-9d36-e5938b877b55/image.png" alt=""></p>
<h1 id="하이퍼바이저란">하이퍼바이저란?</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6681cbc4-01b5-49d3-8d30-c54135d135a6/image.png" alt=""></p>
<p>이 그림 많이 보신 적 있으시죠? 
Host OS위에 하이퍼바이저(Hypervisor)가 존재하고 있습니다. 
그렇다면 하이퍼바이저는 뭘까요?</p>
<p>하이퍼바이저는 <strong>가상 머신</strong>을 생성하고 구동하는 프로그램입니다.</p>
<h3 id="가상-머신은-뭘까요">가상 머신은 뭘까요?</h3>
<p>Virtual Machine(VM)이라 불리는 <strong>가상 머신</strong>은 물리적 컴퓨터의 디지털 버전입니다. 컴퓨터 공학에서 Virtual, 가상이라는 단어는 소프트웨어와 같은 개념입니다. Machine은 보통 CPU 자원을 뜻할 때 사용하지만 여기서는 컴퓨터 자체를 얘기합니다. 그래서 가상머신은 프로그램 및 운영체제를 실행하고, 데이터를 저장하고, 네트워크에 연결하고, 기타 컴퓨팅 기능을 수행합니다.</p>
<p>이제 아시겠죠? 가상 머신을 생성한다는 것은 기존의 컴퓨터 위에 <strong>새로운 컴퓨터</strong>를 만드는 개념입니다. 어떻게 이게 가능할까요? 그것은 하이퍼바이저가 기존 Host System으로부터 자원을 할당받기 때문입니다.</p>
<h3 id="하이퍼바이저의-자원-할당">하이퍼바이저의 자원 할당</h3>
<p>하이퍼바이저가 가상 머신을 생성하기 위해서는 Host System으로부터 적절한 자원 즉 CPU, RAM, HDD등을 할당받고 완전히 격리된 환경을 사용하게 됩니다. 그 곳에서 하이퍼바이저는 적절히 분배한 자원에 설치하고 싶은 OS를 이용하여 VM을 생성하고 구동합니다. 사용자는 하이퍼바이저를 이용해 설치한 OS에 접속하여 완전히 다른 컴퓨터처럼 이용할 수 있습니다.</p>
<p>예를 들어 철수는 Linux 환경에서만 돌아가는 애플리케이션(ex.Apache HTTP Server)을 실행하고 싶습니다. 하지만 철수의 Host OS는 아쉽게도  Windows이죠. 그래서 철수는 하이퍼바이저를 설치하여 손쉽게 Linux 가상 머신을 만들 수 있습니다. 기존의 컴퓨터에서 사용하는 자원을 VM에게 분배해주면 Linux가 잘 돌아가서 애플리케이션을 실행할 수 있는 것이죠. 하지만 용량이 좋지 않은 컴퓨터를 사용하는 철수는 이 VM 때문에 많은 시스템 자원을 잃었습니다.
그래서 철수는 Docker를 사용하기로 마음먹었습니다.</p>
<blockquote>
<p> Apache HTTP Server는 크로스 플랫폼을 지원하지만 리눅스에 최적화된 기능을 사용하기 때문에 리눅스에서 사용하는 것이 실용적이다.</p>
</blockquote>
<h1 id="docker란">Docker란?</h1>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/3413a380-7fe4-42a7-9831-fff27d76a2b4/image.png" alt=""></p>
<p>도커는 컨테이너를 실행할 수 있는 Container Runtime 중 가장 유명한 오픈 소스 플랫폼입니다. Container Runtime이요? 그게 뭔가요?</p>
<h3 id="컨테이너">컨테이너</h3>
<p>컨테이너는 가상화 기술 중 하나로 컨테이너 이미지가 실행될 때 Host OS로부터 보장받은 <strong>격리된 공간</strong>을 제공해주는 기술입니다.</p>
<blockquote>
<p>참고: 격리된 공간을 보장받는 방법은 리눅스의 cgroups와 namespace들을 이용하는 것이다. 간략히 말해서 cgroups는 프로세스들을 그룹으로 나누는 기술이고 namespace를 이용하면 컴퓨터 자원들의 범위를 논리적으로 구분할 수 있다.</p>
</blockquote>
<h3 id="컨테이너-이미지">컨테이너 이미지</h3>
<p>컨테이너 이미지는 컨테이너를 실행하기 위한 파일과 설정 등을 모아 놓은 패키지입니다. 즉, 컨테이너가 실행되기 위해 필요한 모든 것을 포함하고 있습니다. 따라서 다른 의존성을 설치할 필요가 없습니다.</p>
<p>컨테이너 이미지는 보통 Docker Hub과 같은 이미지 레지스트리에 저장됩니다. 사용자는 이미지 레지스트리에서 이미지를 검색하거나, 이미지를 업로드하고 다운로드하여 사용할 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6efd9747-6d9e-4fb9-949e-2083dfd74919/image.png" alt=""></p>
<h3 id="컨테이너-런타임">컨테이너 런타임</h3>
<p>컨테이너 런타임은 컨테이너를 실행하는 데 필요한 도구와 라이브러리를 포함하는 소프트웨어입니다. 즉 <strong>컨테이너 실행환경</strong>입니다.</p>
<blockquote>
<p>컨테이너 런타임에는 가장 유명한 Docker, 그리고 Containerd, CRI-O등이 있다. </p>
</blockquote>
<h3 id="컨테이너를-사용하는-이유">컨테이너를 사용하는 이유</h3>
<p>컨테이너를 사용하는 이유 중 하나는 개발환경과 운영환경의 통일입니다. 개발환경, 테스트환경, 배포환경에서 Container Runtime만 가지고 있으면 같은 실행 환경을 유지할 수 있습니다. 마치 다른 OS에서 jvm만 가지고 있다면 java 프로그램을 실행할 수 있는 것 처럼요! 키워드는 호환성과 이식성입니다.</p>
<p>컨테이너를 사용하는 또 다른 이유는 철수가 Docker를 사용하는 이유에 있습니다.
Docker같은 컨테이너 런타임은 HostOS의 커널 및 시스템 자원을 공유합니다. 즉 애플리케이션 실행을 위해 새로운 컴퓨터를 설치할 필요가 없다는 것이죠. 한 컨테이너가 실행되려면 Host System의 자원을 커널에게 요청하고 적절한 자원을 할당 받아 컨테이너를 실행합니다. 그리고 컨테이너가 중단되면 시스템 자원을 반납합니다. 따라서 하이퍼바이저를 이용해 애플리케이션에 접근할 때보다 훨씬 가볍고 빠릅니다. 하이퍼바이저 오버헤드가 없기 때문입니다.</p>
<blockquote>
<p>하이퍼바이저 오버헤드란, 가상화를 위해 하이퍼바이저가 처리해야 하는 추가적인 작업으로 인해 발생하는 성능 저하를 의미한다. 하이퍼바이저가 가상 머신에 CPU, 메모리, 디스크 등의 자원을 할당하고, 가상 머신 간의 통신을 관리하면서 추가적인 계산과 I/O 작업을 처리해야 하기 때문에 오버헤드가 발생한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hyeongjun-hub/post/8fbd477f-bab5-4f9c-9187-924d00149193/image.png" alt=""></p>
<p>그림에서도 오른쪽이 훨 가벼워 보이지 않나요?</p>
<p>그런데 Docker는 Linux기반으로 만들어졌고 Linux에서 구동됩니다. 그러면 Windows 운영체제인 철수는 어떻게 Docker를 사용할까요? </p>
<p>정답은 Docker Desktop을 사용하면 됩니다.
Windows환경에서 Docker Desktop을 설치하면 Docker Desktop이 Hyper-V 가상화 기술을 이용해 Linux Kernel(즉 VM)을 설치하고 도커를 구동할 수 있게 만듭니다. 여기서 설치하는 VM은 하이퍼바이저가 설치하는 VM 보다 자원이용량이 적습니다. 도커 엔진을 실행하기 위핸 최소한의 환경만 제공하는 것이죠. 이렇게 구동된 도커 엔진에서 리눅스 기반 앱의 이미지를 실행시킬 수 있는 것입니다.</p>
<h1 id="docker-desktop">Docker Desktop</h1>
<p>Docker Desktop은 Windows 및 macOS 시스템에서 Docker를 설치하고 실행할 수 있는 공식적인 소프트웨어입니다. Docker Desktop을 사용하면 로컬 개발 환경에서 쉽게 Docker 컨테이너를 만들고 실행할 수 있습니다.
<img src="https://velog.velcdn.com/images/hyeongjun-hub/post/6b373cec-c8eb-4a4c-ada2-42b2d4b0df5d/image.png" alt=""></p>
<p>다운로드: <a href="https://www.docker.com/products/docker-desktop/">https://www.docker.com/products/docker-desktop/</a></p>
<p>자 이제 철수가 Apache HTTP Server를 실행하는 절차를 알아봅시다.</p>
<ol>
<li>Docker Desktop을 설치합니다.</li>
<li>Docker Desktop은 도커 엔진과 컨테이너를 실행시킬 Linux VM을 설치해 Docker를 구동할 준비를 마칩니다.</li>
<li>도커 허브에서 Apache HTTP Server 이미지를 받아와 실행시킵니다. 이 때 컨테이너 실행에 필요한 자원들을 Linux VM에 요청하게 되고 Linux VM은 Docker Desktop에게, Docker Desktop은 Windows에게 필요한 자원을 요청해 할당 받습니다.</li>
<li>Apache HTTP Server Container가 실행됩니다.</li>
</ol>
<p>이렇게 Docker Desktop은 Windows 운영체제와 Linux 가상 머신 간의 자원 관리를 수행하면서, 안정적인 컨테이너 실행 환경을 제공합니다. 이를 통해 Windows및 MacOS 운영체제에서도 Linux용 애플리케이션을 실행할 수 있게 됩니다.</p>
<p>또한 Docker Desktop은 쿠버네티스 클러스터를 지원해주는 기능도 있습니다.
재밌는 사실은 Docker Desktop을 이용해 쿠버네티스 클러스터를 이용하고 실제로 container runtime은 도커를 사용하지 않아도 된다는 것입니다.</p>
<p>오늘은 HyperVisor부터 Docker Desktop까지 제가 공부한 내용을 정리해보았습니다. 오타나 틀린 내용은 댓글로 지적해주시면 감사하겠습니다.</p>
<hr>
<p>이미지 출처</p>
<ul>
<li><a href="https://www.docker.com/blog/containers-and-vms-together/">https://www.docker.com/blog/containers-and-vms-together/</a></li>
<li><a href="https://techrecipe.co.kr/posts/33244">https://techrecipe.co.kr/posts/33244</a></li>
</ul>
<p>참고</p>
<ul>
<li><a href="https://cloud.google.com/learn/what-is-a-virtual-machine?hl=ko">https://cloud.google.com/learn/what-is-a-virtual-machine?hl=ko</a></li>
<li><a href="https://docs.docker.com/desktop/windows/wsl/">https://docs.docker.com/desktop/windows/wsl/</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>