<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://hyn128.site/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hyn128.site/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-09-11T15:22:10+09:00</updated><id>https://hyn128.site/feed.xml</id><title type="html">renamed.log</title><subtitle>키보드가 망가지도록 공부하기</subtitle><entry><title type="html">Git 이력 관리와 Branch 작업</title><link href="https://hyn128.site/cloud-native-43-git-history-branch/" rel="alternate" type="text/html" title="Git 이력 관리와 Branch 작업" /><published>2026-09-10T00:00:00+09:00</published><updated>2026-09-10T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-43-git-history-branch</id><content type="html" xml:base="https://hyn128.site/cloud-native-43-git-history-branch/"><![CDATA[<p><a href="/cloud-native-42-git-version-control-basics/">Version Control과 Git 기본 작업 흐름</a>에서 Working Tree, Staging Area와 Local Repository의 관계를 살펴봤다. 이 글에서는 저장된 Commit을 조회하고 비교하는 방법부터 변경 복구, Branch 통합까지 다룬다.</p>

<p>SourceTree를 사용해도 Git의 상태와 History는 CLI를 사용할 때와 같다. GUI 조작과 명령을 함께 확인하면 버튼을 눌렀을 때 Repository에서 무엇이 바뀌는지 이해할 수 있다.</p>

<h2 id="1--sourcetree와-git-cli">1 ) SourceTree와 Git CLI</h2>

<hr />

<p>SourceTree는 Git Repository를 시각적으로 다루는 GUI Client이다. File 상태, Commit Graph와 Branch 관계를 화면에 표시하고 선택한 작업에 해당하는 Git 명령을 실행한다.</p>

<table>
  <thead>
    <tr>
      <th>Git 작업</th>
      <th>SourceTree에서 확인할 영역</th>
      <th>CLI</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Repository 생성</td>
      <td>Local Repository 생성</td>
      <td><code class="language-plaintext highlighter-rouge">git init</code></td>
    </tr>
    <tr>
      <td>File 상태 확인</td>
      <td>File Status·Working Copy</td>
      <td><code class="language-plaintext highlighter-rouge">git status</code></td>
    </tr>
    <tr>
      <td>Stage</td>
      <td>Stage 대상 선택</td>
      <td><code class="language-plaintext highlighter-rouge">git add</code>, <code class="language-plaintext highlighter-rouge">git restore --staged</code></td>
    </tr>
    <tr>
      <td>Commit</td>
      <td>Commit Message 입력 영역</td>
      <td><code class="language-plaintext highlighter-rouge">git commit</code></td>
    </tr>
    <tr>
      <td>History</td>
      <td>Commit Graph·History</td>
      <td><code class="language-plaintext highlighter-rouge">git log --graph</code></td>
    </tr>
    <tr>
      <td>Tag</td>
      <td>Commit의 Tag 메뉴</td>
      <td><code class="language-plaintext highlighter-rouge">git tag</code></td>
    </tr>
    <tr>
      <td>Branch 전환</td>
      <td>Branch 목록</td>
      <td><code class="language-plaintext highlighter-rouge">git switch</code></td>
    </tr>
    <tr>
      <td>통합</td>
      <td>Merge·Rebase 메뉴</td>
      <td><code class="language-plaintext highlighter-rouge">git merge</code>, <code class="language-plaintext highlighter-rouge">git rebase</code></td>
    </tr>
    <tr>
      <td>임시 보관</td>
      <td>Stash 메뉴</td>
      <td><code class="language-plaintext highlighter-rouge">git stash</code></td>
    </tr>
  </tbody>
</table>

<p>버튼 이름과 화면 배치는 SourceTree Version과 운영체제에 따라 달라질 수 있다. 조작 후 <code class="language-plaintext highlighter-rouge">git status</code>, <code class="language-plaintext highlighter-rouge">git log</code>와 <code class="language-plaintext highlighter-rouge">git branch</code>로 실제 Repository 상태를 확인하면 GUI 표시와 Git 개념을 연결할 수 있다.</p>

<h2 id="2--local-repository에서-version-생성">2 ) Local Repository에서 Version 생성</h2>

<hr />

<h3 id="repository-준비">Repository 준비</h3>

<p>SourceTree에서는 Local Repository 생성 화면에서 작업 Directory를 선택한다. CLI에서는 같은 Directory로 이동해 <code class="language-plaintext highlighter-rouge">git init</code>을 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>git-history-lab
<span class="nb">cd </span>git-history-lab
git init <span class="nt">--initial-branch</span><span class="o">=</span>main
</code></pre></div></div>

<p>초기 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
</code></pre></div></div>

<h3 id="첫-번째-commit">첫 번째 Commit</h3>

<p><code class="language-plaintext highlighter-rouge">a.txt</code>, <code class="language-plaintext highlighter-rouge">b.txt</code>, <code class="language-plaintext highlighter-rouge">c.txt</code>를 만들고 각각 <code class="language-plaintext highlighter-rouge">A</code>, <code class="language-plaintext highlighter-rouge">B</code>, <code class="language-plaintext highlighter-rouge">C</code>를 기록한다. File 작성 방식은 사용하는 Editor에 따라 달라지므로 여기서는 Git 상태 변화에 집중한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git-history-lab/
├── a.txt
├── b.txt
└── c.txt
</code></pre></div></div>

<p>새 File은 Untracked 상태이다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
</code></pre></div></div>

<p>세 File을 다음 Commit 대상으로 Stage한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add a.txt b.txt c.txt
git diff <span class="nt">--staged</span>
git status
</code></pre></div></div>

<p>SourceTree에서는 Unstaged File 목록에서 대상을 선택하여 Staged File 영역으로 옮긴다. Commit 전에는 Staged Diff를 읽어 의도한 내용만 포함됐는지 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit <span class="nt">-m</span> <span class="s2">"docs: add initial text files"</span>
</code></pre></div></div>

<h3 id="두-번째-commit">두 번째 Commit</h3>

<p><code class="language-plaintext highlighter-rouge">a.txt</code>를 수정하고 <code class="language-plaintext highlighter-rouge">c.txt</code>를 삭제하면 두 File 모두 Working Tree 변경으로 표시된다. 삭제도 Git이 추적하는 변경이다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git diff
</code></pre></div></div>

<p>변경을 Stage하고 두 번째 Commit을 만든다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add a.txt c.txt
git diff <span class="nt">--staged</span>
git commit <span class="nt">-m</span> <span class="s2">"docs: update A and remove C"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">git add .</code>은 현재 Directory 아래의 추가, 수정과 삭제를 한꺼번에 Stage할 수 있다. 범위가 넓으므로 사용 직후 <code class="language-plaintext highlighter-rouge">git diff --staged</code>로 Commit 대상을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add <span class="nb">.</span>
git diff <span class="nt">--staged</span>
</code></pre></div></div>

<p>이미 추적 중인 File의 수정과 삭제를 Stage하면서 Commit까지 수행할 때는 <code class="language-plaintext highlighter-rouge">-a</code>를 사용할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit <span class="nt">-am</span> <span class="s2">"docs: update tracked files"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">git commit -am</code>은 Untracked File을 포함하지 않는다. 새 File은 먼저 <code class="language-plaintext highlighter-rouge">git add</code>로 추적 대상에 포함해야 한다.</p>

<h2 id="3--commit-history-조회">3 ) Commit History 조회</h2>

<hr />

<p>SourceTree의 History 화면은 Commit을 Graph로 표시한다. CLI에서는 목적에 따라 <code class="language-plaintext highlighter-rouge">git log</code> Option을 조합한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 상세 History</span>
git log

<span class="c"># 한 Commit을 한 줄로 표시</span>
git log <span class="nt">--oneline</span>

<span class="c"># Commit별 Patch 포함</span>
git log <span class="nt">--patch</span>

<span class="c"># Branch와 Tag를 함께 표시</span>
git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span> <span class="nt">--all</span>
</code></pre></div></div>

<p>Commit을 선택하면 작성자, 시각, Message, Parent와 변경된 File을 확인할 수 있다. CLI에서는 <code class="language-plaintext highlighter-rouge">git show</code>를 사용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git show &lt;commit&gt;
git show <span class="nt">--stat</span> &lt;commit&gt;
git show <span class="nt">--name-status</span> &lt;commit&gt;
</code></pre></div></div>

<p>Commit 개수는 작업을 나눈 방식에 따라 크게 달라진다. 많은 Commit이 Project 품질이나 기여 수준을 자동으로 증명하지 않으므로 변경 내용과 Message, Review 결과를 함께 봐야 한다.</p>

<h2 id="4--tag로-version-표시">4 ) Tag로 Version 표시</h2>

<hr />

<blockquote>
  <p><strong>Tag</strong></p>

  <p>특정 Git Object를 읽기 쉬운 이름으로 가리키는 Reference이다. Release Version을 표시할 때 주로 Commit에 연결한다.</p>
</blockquote>

<p>Commit Object ID는 Git이 Content와 Metadata를 바탕으로 생성한다. 사용자가 원하는 문자열로 Object ID를 정할 수 없으므로 <code class="language-plaintext highlighter-rouge">v1.0.0</code> 같은 Tag를 별도로 붙인다.</p>

<h3 id="lightweight-tag">Lightweight Tag</h3>

<p>현재 Commit을 직접 가리키는 Lightweight Tag를 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git tag v1.0.0
</code></pre></div></div>

<h3 id="annotated-tag">Annotated Tag</h3>

<p>Annotated Tag에는 작성자, 생성 시각과 Message가 별도 Tag Object로 기록된다. Release 표시에는 Annotated Tag가 적합하다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git tag <span class="nt">-a</span> v1.0.0 <span class="nt">-m</span> <span class="s2">"release v1.0.0"</span>
</code></pre></div></div>

<p>특정 Commit에 Tag를 붙이려면 Commit을 함께 지정한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span>
git tag <span class="nt">-a</span> v0.9.0 &lt;commit&gt; <span class="nt">-m</span> <span class="s2">"release v0.9.0"</span>
</code></pre></div></div>

<p>Tag 목록과 대상을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git tag <span class="nt">--list</span>
git show v1.0.0
</code></pre></div></div>

<p>Local Tag를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git tag <span class="nt">--delete</span> v1.0.0
</code></pre></div></div>

<p>Tag는 기본 <code class="language-plaintext highlighter-rouge">git push</code>에 항상 포함되지 않는다. Remote에 특정 Tag를 올리거나 삭제하는 작업은 명시적으로 수행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push origin v1.0.0
git push origin <span class="nt">--delete</span> v1.0.0
</code></pre></div></div>

<p>SourceTree에서는 History에서 대상 Commit을 선택해 Tag를 만들 수 있다. 생성 후 Local Tag인지 Remote에도 Push된 Tag인지 구분해서 확인한다.</p>

<h2 id="5--version-비교">5 ) Version 비교</h2>

<hr />

<p><code class="language-plaintext highlighter-rouge">git diff</code>는 어떤 두 상태를 비교하는지에 따라 출력 범위가 달라진다.</p>

<table>
  <thead>
    <tr>
      <th>비교 대상</th>
      <th>명령</th>
      <th>확인하는 내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Working Tree ↔ Staging Area</td>
      <td><code class="language-plaintext highlighter-rouge">git diff</code></td>
      <td>아직 Stage하지 않은 변경</td>
    </tr>
    <tr>
      <td>Staging Area ↔ <code class="language-plaintext highlighter-rouge">HEAD</code></td>
      <td><code class="language-plaintext highlighter-rouge">git diff --staged</code></td>
      <td>다음 Commit에 들어갈 변경</td>
    </tr>
    <tr>
      <td>Commit ↔ Commit</td>
      <td><code class="language-plaintext highlighter-rouge">git diff &lt;commit-a&gt; &lt;commit-b&gt;</code></td>
      <td>두 Commit의 Snapshot 차이</td>
    </tr>
    <tr>
      <td>Branch ↔ Branch</td>
      <td><code class="language-plaintext highlighter-rouge">git diff &lt;branch-a&gt;..&lt;branch-b&gt;</code></td>
      <td>두 Branch Tip의 Snapshot 차이</td>
    </tr>
  </tbody>
</table>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git diff
git diff <span class="nt">--staged</span>
git diff HEAD~1 HEAD
git diff main..feature/login
</code></pre></div></div>

<p>SourceTree에서는 Commit 두 개를 선택하여 변경 File과 Diff를 비교할 수 있다. 다중 선택에 사용하는 보조 Key는 운영체제에 따라 다르므로 선택된 두 Commit의 ID를 화면에서 확인한다.</p>

<h3 id="head-와-"><code class="language-plaintext highlighter-rouge">HEAD</code>, <code class="language-plaintext highlighter-rouge">^</code>와 <code class="language-plaintext highlighter-rouge">~</code></h3>

<p><code class="language-plaintext highlighter-rouge">HEAD</code>는 현재 Checkout한 위치를 가리킨다. Branch에 정상적으로 연결된 상태에서는 현재 Branch의 Tip Commit을 가리킨다.</p>

<table>
  <thead>
    <tr>
      <th>표현</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HEAD</code></td>
      <td>현재 Checkout한 Commit</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HEAD^</code></td>
      <td><code class="language-plaintext highlighter-rouge">HEAD</code>의 첫 번째 Parent</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HEAD^2</code></td>
      <td>Merge Commit인 <code class="language-plaintext highlighter-rouge">HEAD</code>의 두 번째 Parent</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HEAD~2</code></td>
      <td>첫 번째 Parent를 두 번 따라간 Commit</td>
    </tr>
  </tbody>
</table>

<p>일직선 History에서는 <code class="language-plaintext highlighter-rouge">HEAD^</code>와 <code class="language-plaintext highlighter-rouge">HEAD~1</code>이 같은 Commit을 가리킨다. Parent가 여러 개인 Merge Commit에서는 <code class="language-plaintext highlighter-rouge">^&lt;번호&gt;</code>로 어느 Parent를 선택하는지 지정할 수 있다.</p>

<h2 id="6--file-변경-복구">6 ) File 변경 복구</h2>

<hr />

<p>File 복구는 Staging Area만 되돌리는 작업과 Working Tree의 내용을 폐기하는 작업을 구분해야 한다.</p>

<h3 id="stage-취소">Stage 취소</h3>

<p>다음 명령은 File의 Staged 상태를 <code class="language-plaintext highlighter-rouge">HEAD</code> 기준으로 되돌린다. Working Tree의 수정 내용은 유지한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git restore <span class="nt">--staged</span> &lt;file&gt;
git status
</code></pre></div></div>

<p>SourceTree에서는 Staged File을 Unstaged 영역으로 옮기는 동작에 해당한다.</p>

<h3 id="working-tree-변경-폐기">Working Tree 변경 폐기</h3>

<p>다음 명령은 Working Tree의 File을 Staging Area의 내용으로 복원한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git diff <span class="nt">--</span> &lt;file&gt;
git restore &lt;file&gt;
</code></pre></div></div>

<p>저장하지 않은 변경은 사라질 수 있다. 실행 전에 <code class="language-plaintext highlighter-rouge">git diff</code>를 확인하고 보존할 내용이 있으면 Commit이나 Stash로 저장한다.</p>

<p>특정 Commit의 File만 가져올 수도 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git restore <span class="nt">--source</span><span class="o">=</span>&lt;commit&gt; <span class="nt">--</span> &lt;file&gt;
</code></pre></div></div>

<p>이 명령은 Branch 전체를 해당 Commit으로 이동시키지 않고 선택한 File의 Working Tree 내용만 바꾼다.</p>

<h2 id="7--commit-되돌리기">7 ) Commit 되돌리기</h2>

<hr />

<p><code class="language-plaintext highlighter-rouge">reset</code>과 <code class="language-plaintext highlighter-rouge">revert</code>는 결과가 비슷해 보이지만 History 처리 방식이 다르다.</p>

<table>
  <thead>
    <tr>
      <th>명령</th>
      <th>현재 Branch Tip</th>
      <th>Staging Area</th>
      <th>Working Tree</th>
      <th>공유 Branch 사용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">reset --soft</code></td>
      <td>대상 Commit으로 이동</td>
      <td>유지</td>
      <td>유지</td>
      <td>History가 달라지므로 주의</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">reset --mixed</code></td>
      <td>대상 Commit으로 이동</td>
      <td>대상 Commit 기준으로 초기화</td>
      <td>유지</td>
      <td>History가 달라지므로 주의</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">reset --hard</code></td>
      <td>대상 Commit으로 이동</td>
      <td>대상 Commit 기준으로 초기화</td>
      <td>대상 Commit 기준으로 변경</td>
      <td>저장하지 않은 변경 손실 가능</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">revert</code></td>
      <td>새 Commit 추가</td>
      <td>새 Commit 과정에 따라 반영</td>
      <td>취소 결과 반영</td>
      <td>기존 History를 보존하므로 적합</td>
    </tr>
  </tbody>
</table>

<h3 id="soft-reset">Soft Reset</h3>

<p>Commit만 취소하고 변경 내용을 Staging Area와 Working Tree에 유지한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git reset <span class="nt">--soft</span> &lt;target-commit&gt;
git status
git diff <span class="nt">--staged</span>
</code></pre></div></div>

<h3 id="mixed-reset">Mixed Reset</h3>

<p>Option을 생략한 <code class="language-plaintext highlighter-rouge">git reset</code>은 기본적으로 Mixed Mode이다. Branch를 대상 Commit으로 옮기고 Staging Area를 초기화하지만 Working Tree 변경은 유지한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git reset &lt;target-commit&gt;
git status
git diff
</code></pre></div></div>

<h3 id="hard-reset">Hard Reset</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git reset <span class="nt">--hard</span> &lt;target-commit&gt;
</code></pre></div></div>

<p>Hard Reset은 Branch, Staging Area와 추적 중인 Working Tree File을 대상 Commit 상태에 맞춘다. Commit하지 않은 변경이 사라질 수 있으므로 다음 내용을 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git diff
git diff <span class="nt">--staged</span>
git log <span class="nt">--oneline</span> <span class="nt">--decorate</span> <span class="nt">-n</span> 10
</code></pre></div></div>

<p>필요하면 현재 위치를 임시 Branch로 보존한 뒤 Reset한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch backup-before-reset
</code></pre></div></div>

<h3 id="revert">Revert</h3>

<p><code class="language-plaintext highlighter-rouge">git revert</code>는 취소할 Commit의 반대 변경을 적용한 새 Commit을 만든다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git revert &lt;commit-to-cancel&gt;
</code></pre></div></div>

<p>기존 Commit이 History에 남으므로 이미 Remote에 공유한 Branch에서 변경을 취소할 때 사용하기 쉽다. 충돌이 발생하면 File을 수정하고 Stage한 뒤 작업을 계속하거나 중단한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add &lt;resolved-file&gt;
git revert <span class="nt">--continue</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git revert <span class="nt">--abort</span>
</code></pre></div></div>

<h2 id="8--stash로-변경-임시-보관">8 ) Stash로 변경 임시 보관</h2>

<hr />

<p>Stash는 Commit하기 이른 Working Tree와 Staging Area의 변경을 임시로 저장하고 현재 Branch의 작업 공간을 정리할 때 사용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git stash push <span class="nt">-m</span> <span class="s2">"temporary work"</span>
git status
</code></pre></div></div>

<p>기본 Stash에는 추적 중인 File의 Staged·Unstaged 변경이 들어간다. Untracked File은 기본 대상이 아니며 필요할 때 <code class="language-plaintext highlighter-rouge">-u</code>를 명시한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git stash push <span class="nt">-u</span> <span class="nt">-m</span> <span class="s2">"include untracked files"</span>
</code></pre></div></div>

<p>저장된 목록과 변경 내용을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git stash list
git stash show <span class="nt">--patch</span> stash@<span class="o">{</span>0<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">apply</code>는 Stash를 적용한 뒤 목록에 남긴다. Stage 상태까지 복원하려면 <code class="language-plaintext highlighter-rouge">--index</code>를 사용할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git stash apply stash@<span class="o">{</span>0<span class="o">}</span>
git stash apply <span class="nt">--index</span> stash@<span class="o">{</span>0<span class="o">}</span>
</code></pre></div></div>

<p>더 이상 필요하지 않은 Stash를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git stash drop stash@<span class="o">{</span>0<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">pop</code>은 적용에 성공하면 해당 Stash를 목록에서 제거한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git stash pop stash@<span class="o">{</span>0<span class="o">}</span>
</code></pre></div></div>

<p>Stash 적용 중에도 현재 Branch의 변경과 충돌할 수 있다. 적용 후 <code class="language-plaintext highlighter-rouge">git status</code>와 Diff를 확인한다.</p>

<h2 id="9--branch와-head">9 ) Branch와 <code class="language-plaintext highlighter-rouge">HEAD</code></h2>

<hr />

<blockquote>
  <p><strong>Branch</strong></p>

  <p>Commit을 가리키며 새 Commit이 만들어질 때 함께 이동하는 Reference이다.</p>
</blockquote>

<p>Branch는 Project File을 통째로 복사한 Directory가 아니다. 여러 작업 흐름이 서로 다른 Commit을 가리키도록 분기한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>              D──E  feature/login
             /
A──B──C───────────  main
</code></pre></div></div>

<p>현재 Branch와 목록을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch <span class="nt">--show-current</span>
git branch
</code></pre></div></div>

<p>Branch를 만들고 전환한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch feature/login
git switch feature/login
</code></pre></div></div>

<p>생성과 전환을 동시에 수행할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git switch <span class="nt">-c</span> feature/payment
</code></pre></div></div>

<p>기존 환경에서는 다음 <code class="language-plaintext highlighter-rouge">checkout</code> 명령도 계속 볼 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout feature/login
git checkout <span class="nt">-b</span> feature/payment
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">checkout</code>은 Branch 전환과 File 복구를 모두 담당하는 오래된 범용 명령이다. 새 문서에서는 의도가 분명한 <code class="language-plaintext highlighter-rouge">switch</code>와 <code class="language-plaintext highlighter-rouge">restore</code>를 우선 사용한다.</p>

<p>특정 Commit을 직접 Checkout하면 <code class="language-plaintext highlighter-rouge">HEAD</code>가 Branch가 아닌 Commit을 가리키는 Detached HEAD 상태가 될 수 있다. 이 상태에서 만든 Commit을 보존하려면 해당 위치에서 Branch를 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git switch <span class="nt">-c</span> save-detached-work
</code></pre></div></div>

<h3 id="branch-삭제">Branch 삭제</h3>

<p>현재 Checkout한 Branch는 삭제할 수 없다. 다른 Branch로 이동한 뒤 병합이 끝난 Branch를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git switch main
git branch <span class="nt">-d</span> feature/login
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-d</code>는 병합되지 않은 Commit이 있으면 삭제를 거절한다. <code class="language-plaintext highlighter-rouge">-D</code>는 이를 무시하는 강제 삭제이므로 보존할 Commit이 없는지 확인하지 않고 사용하지 않는다.</p>

<h2 id="10--merge">10 ) Merge</h2>

<hr />

<p>Merge는 현재 Branch에 다른 Branch의 History를 통합한다. <code class="language-plaintext highlighter-rouge">feature/login</code>을 <code class="language-plaintext highlighter-rouge">main</code>에 합치려면 결과를 받을 <code class="language-plaintext highlighter-rouge">main</code>으로 먼저 이동한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git switch main
git merge feature/login
</code></pre></div></div>

<h3 id="fast-forward-merge">Fast-forward Merge</h3>

<p>Branch가 분기된 뒤 <code class="language-plaintext highlighter-rouge">main</code>에 새 Commit이 없다면 <code class="language-plaintext highlighter-rouge">main</code> Reference를 <code class="language-plaintext highlighter-rouge">feature/login</code>의 Tip까지 앞으로 이동할 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>병합 전

A──B  main
    \
     C──D  feature/login

병합 후

A──B──C──D  main, feature/login
</code></pre></div></div>

<p>Fast-forward Merge는 별도 Merge Commit을 만들지 않아도 된다.</p>

<h3 id="merge-commit">Merge Commit</h3>

<p>두 Branch가 각각 새 Commit을 가진 경우 Git은 공통 조상을 기준으로 변경을 통합하고 Merge Commit을 만들 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       C──D  feature/login
      /    \
A──B──E─────M  main
</code></pre></div></div>

<p>특정 Commit을 <code class="language-plaintext highlighter-rouge">git merge &lt;commit&gt;</code> 대상으로 지정할 수 있지만 해당 Commit 하나의 Patch만 적용하는 작업은 아니다. Git은 지정한 Commit까지 이어지는 History를 현재 Branch와 병합한다. 하나의 Commit 변경만 적용하려는 경우에는 목적을 확인한 뒤 <code class="language-plaintext highlighter-rouge">git cherry-pick &lt;commit&gt;</code>을 검토한다.</p>

<h3 id="merge-충돌">Merge 충돌</h3>

<p>서로 다른 Branch가 같은 File의 같은 영역을 다르게 수정하면 Git이 결과를 자동으로 결정하지 못할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
</code></pre></div></div>

<p>충돌 File에는 다음과 같은 Marker가 생길 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
현재 Branch의 내용
=======
병합하는 Branch의 내용
&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature/login
</code></pre></div></div>

<p>적용할 내용을 결정하고 Marker를 제거한 뒤 File을 Stage한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add &lt;resolved-file&gt;
git commit
</code></pre></div></div>

<p>Merge를 완료하지 않고 시작 전으로 돌아가려면 다음 명령을 사용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git merge <span class="nt">--abort</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ours</code>와 <code class="language-plaintext highlighter-rouge">theirs</code>는 현재 진행 중인 작업 종류와 방향에 따라 가리키는 쪽을 정확히 확인해야 한다. 이름만 보고 자동으로 한쪽 전체를 선택하지 않는다.</p>

<h2 id="11--rebase">11 ) Rebase</h2>

<hr />

<p>Rebase는 현재 Branch의 Commit을 새로운 Base 위에 다시 적용한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>변경 전

A──B──C  main
    \
     D──E  feature/login

변경 후

A──B──C  main
       \
        D'──E'  feature/login
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">feature/login</code>을 최신 <code class="language-plaintext highlighter-rouge">main</code> 위로 옮긴다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git switch feature/login
git rebase main
</code></pre></div></div>

<p>Rebase는 <code class="language-plaintext highlighter-rouge">D</code>와 <code class="language-plaintext highlighter-rouge">E</code>를 그대로 이동하지 않는다. 같은 변경을 새 Parent 위에 적용하여 <code class="language-plaintext highlighter-rouge">D'</code>, <code class="language-plaintext highlighter-rouge">E'</code>라는 새 Commit을 만들기 때문에 Object ID가 달라진다.</p>

<p>충돌이 발생하면 File을 수정하고 Stage한 뒤 다음 Commit 적용을 계속한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git add &lt;resolved-file&gt;
git rebase <span class="nt">--continue</span>
</code></pre></div></div>

<p>현재 Rebase를 중단하고 시작 전 상태로 돌아간다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git rebase <span class="nt">--abort</span>
</code></pre></div></div>

<p>이미 여러 사람이 사용하는 Remote Branch의 Commit을 Rebase하면 기존 History와 다른 Commit ID가 만들어진다. 공유하기 전의 개인 작업 Branch에서 사용하는 범위와 팀의 History 정책을 먼저 확인한다.</p>

<h2 id="12--non-fast-forward-push와-merge의-차이">12 ) Non-fast-forward Push와 Merge의 차이</h2>

<hr />

<p>Remote Branch에 Local이 가지고 있지 않은 Commit이 있으면 Push가 <code class="language-plaintext highlighter-rouge">non-fast-forward</code>로 거절될 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Local main       A──B──L
                     
Remote main      A──B──R
</code></pre></div></div>

<p>이 오류는 Fast-forward Merge 기능의 실패가 아니다. Remote Branch를 Local Commit까지 단순 이동하면 <code class="language-plaintext highlighter-rouge">R</code>이 History에서 빠질 수 있으므로 Server가 Update를 거절한 것이다.</p>

<p>Remote 상태를 먼저 가져와 History를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch origin
git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span> <span class="nt">--all</span>
</code></pre></div></div>

<p>팀 정책에 따라 Remote 변경을 Merge하거나 Rebase한 뒤 충돌을 해결하고 다시 Push한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git merge origin/main
</code></pre></div></div>

<p>또는 다음과 같이 현재 Commit을 Remote Branch 위에 다시 적용할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git rebase origin/main
</code></pre></div></div>

<p>원격 Repository 연결과 Push 전체 흐름은 <a href="/cloud-native-44-github-remote-workflow/">GitHub Remote Repository 작업 흐름</a>에서 다룬다.</p>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>SourceTree와 CLI는 같은 Git Repository와 상태를 다루므로 GUI 작업 뒤에도 Git 명령으로 결과를 확인할 수 있다.</p>
    </li>
    <li>
      <p>Tag는 Commit Object ID를 바꾸지 않고 특정 Version을 읽기 쉬운 이름으로 가리킨다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">git diff</code>는 Working Tree, Staging Area, Commit과 Branch 중 어떤 두 상태를 비교하는지 구분해야 한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">restore</code>는 File, <code class="language-plaintext highlighter-rouge">reset</code>은 Branch와 Index, <code class="language-plaintext highlighter-rouge">revert</code>는 기존 Commit을 취소하는 새 Commit을 다룬다.</p>
    </li>
    <li>
      <p>기본 Stash는 추적 중인 변경을 저장하며 Untracked File을 포함하려면 <code class="language-plaintext highlighter-rouge">-u</code>가 필요하다.</p>
    </li>
    <li>
      <p>Merge는 History를 통합하고 Rebase는 현재 Branch의 Commit을 새 Base 위에 다시 적용한다.</p>
    </li>
    <li>
      <p>Non-fast-forward Push 거절은 Remote에 Local이 가지지 않은 Commit이 있을 때 History 손실을 막기 위해 발생한다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Git" /><summary type="html"><![CDATA[SourceTree와 Git CLI를 연결하여 Commit 조회, Tag, Diff, Restore, Reset, Revert, Stash, Merge와 Rebase를 다루는 방법]]></summary></entry><entry><title type="html">GitHub Remote Repository 작업 흐름</title><link href="https://hyn128.site/cloud-native-44-github-remote-workflow/" rel="alternate" type="text/html" title="GitHub Remote Repository 작업 흐름" /><published>2026-09-10T00:00:00+09:00</published><updated>2026-09-10T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-44-github-remote-workflow</id><content type="html" xml:base="https://hyn128.site/cloud-native-44-github-remote-workflow/"><![CDATA[<p><a href="/cloud-native-42-git-version-control-basics/">Version Control과 Git 기본 작업 흐름</a>에서 Local Repository의 구조를 정리했고 <a href="/cloud-native-43-git-history-branch/">Git 이력 관리와 Branch 작업</a>에서 Branch를 나누고 통합하는 방법을 다뤘다. 이 글에서는 Local Repository와 GitHub의 Remote Repository 사이에서 Commit과 Branch를 교환한다.</p>

<h2 id="1--github와-git">1 ) GitHub와 Git</h2>

<hr />

<p>Git은 Local에서 Commit과 Branch History를 관리하는 Version Control System이다. GitHub는 Git Repository를 Hosting하고 협업 기능을 제공하는 Service이다.</p>

<table>
  <thead>
    <tr>
      <th>GitHub 기능</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Repository Hosting</td>
      <td>Git Commit, Branch와 Tag 저장·공유</td>
    </tr>
    <tr>
      <td>Pull Request</td>
      <td>Branch 변경 검토와 병합</td>
    </tr>
    <tr>
      <td>Issues</td>
      <td>작업과 문제 추적</td>
    </tr>
    <tr>
      <td>Actions</td>
      <td>Build, Test와 배포 Workflow 실행</td>
    </tr>
    <tr>
      <td>Packages</td>
      <td>Package와 Container Image 저장</td>
    </tr>
    <tr>
      <td>Projects</td>
      <td>Issue와 Pull Request 기반 작업 관리</td>
    </tr>
    <tr>
      <td>Pages</td>
      <td>Repository Content 기반 정적 Site 배포</td>
    </tr>
  </tbody>
</table>

<p>GitHub의 무료 범위, Storage와 Actions 사용량은 Plan에 따라 달라질 수 있으므로 사용 시점의 공식 정책을 확인한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Local Repository
├── Working Tree
├── Staging Area
└── Local Branch와 Commit
          │
          │ fetch·pull·push
          ▼
GitHub Remote Repository
├── Remote Branch
├── Tag
└── Pull Request·Issue·Actions
</code></pre></div></div>

<h2 id="2--remote-repository-인증-방식">2 ) Remote Repository 인증 방식</h2>

<hr />

<p>Private Repository를 Clone하거나 Push할 때 GitHub는 사용자를 인증하고 Repository 권한을 확인한다. Remote URL 형식에 따라 HTTPS 또는 SSH 인증을 사용한다.</p>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>인증 수단</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>HTTPS</td>
      <td>Personal Access Token과 Credential Helper</td>
      <td>HTTPS URL을 사용하며 운영체제 Credential Store와 연동 가능</td>
    </tr>
    <tr>
      <td>SSH</td>
      <td>SSH Private Key와 GitHub에 등록한 Public Key</td>
      <td>GitHub Password나 PAT를 Push마다 전달하지 않음</td>
    </tr>
    <tr>
      <td>GitHub CLI</td>
      <td><code class="language-plaintext highlighter-rouge">gh auth login</code></td>
      <td>Browser, HTTPS 또는 SSH 인증 설정을 안내하고 GitHub API도 함께 사용</td>
    </tr>
  </tbody>
</table>

<h3 id="https와-personal-access-token">HTTPS와 Personal Access Token</h3>

<p>GitHub의 HTTPS Git 작업에서 Account Password를 대신해 Personal Access Token을 사용할 수 있다. Token에는 필요한 Repository와 작업 범위만 부여하고 만료 기간을 설정한다.</p>

<p>Token 값을 다음 위치에 직접 작성하지 않는다.</p>

<ul>
  <li>
    <p>Markdown과 Source Code</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">git remote</code> URL</p>
  </li>
  <li>
    <p>Shell Script</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">.gitconfig</code>의 평문 설정</p>
  </li>
  <li>
    <p>CI Log에 출력되는 명령 인자</p>
  </li>
</ul>

<p>현재 Credential Helper 설정을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--show-origin</span> <span class="nt">--get-all</span> credential.helper
</code></pre></div></div>

<p>Git Credential Manager, macOS Keychain 같은 Helper를 사용하면 Credential을 운영체제의 Credential Store와 연결할 수 있다. 설치 방식과 Helper 이름은 운영체제와 Git 배포판에 따라 다르므로 임의로 덮어쓰기 전에 기존 설정을 확인한다.</p>

<p>저장된 계정을 바꿀 때는 Git 설정만 반복해서 추가하지 않는다. 현재 Helper 또는 운영체제 Credential Manager에서 기존 GitHub Credential을 제거한 뒤 다시 인증한다.</p>

<h3 id="ssh-인증">SSH 인증</h3>

<p>SSH Remote는 다음 형식을 사용한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git@github.com:&lt;owner&gt;/&lt;repository&gt;.git
</code></pre></div></div>

<p>Local Private Key는 외부에 공개하지 않고 대응하는 Public Key만 GitHub 계정이나 조직에 등록한다. SSH Key 생성과 공개키 등록 이유는 <a href="/12-remote-access/">원격 접속</a>의 SSH Key 절에서 다룬다.</p>

<p>GitHub SSH 연결을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-T</span> git@github.com
</code></pre></div></div>

<p>첫 연결에서는 Host Key Fingerprint를 GitHub 공식 문서의 값과 대조한 뒤 신뢰 여부를 결정한다.</p>

<h3 id="github-cli-인증">GitHub CLI 인증</h3>

<p>GitHub CLI를 사용한다면 대화형 Login 절차를 실행할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh auth login
gh auth status
</code></pre></div></div>

<p>필요한 경우 현재 <code class="language-plaintext highlighter-rouge">gh</code> 인증을 Git Credential 설정에 연결한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh auth setup-git
</code></pre></div></div>

<p>계정을 변경할 때는 현재 인증 상태를 확인하고 Logout한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh auth status
gh auth <span class="nb">logout
</span>gh auth login
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">gh</code> 인증과 SSH Agent에 올라간 Key는 별도 상태일 수 있다. 오류가 발생하면 Remote URL이 HTTPS인지 SSH인지 먼저 확인한다.</p>

<h2 id="3--올바른-remote-url">3 ) 올바른 Remote URL</h2>

<hr />

<p>GitHub Repository의 HTTPS와 SSH URL은 형식이 다르다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTPS
https://github.com/&lt;owner&gt;/&lt;repository&gt;.git

SSH
git@github.com:&lt;owner&gt;/&lt;repository&gt;.git
</code></pre></div></div>

<p>다음 주소는 HTTPS Scheme과 SSH 구분자를 섞었으므로 올바른 GitHub Remote URL이 아니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://github.com:&lt;owner&gt;/&lt;repository&gt;.git
</code></pre></div></div>

<p>현재 Repository의 Remote 이름과 URL을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote <span class="nt">-v</span>
git remote get-url origin
</code></pre></div></div>

<p>Remote 이름은 URL 자체가 아니다. <code class="language-plaintext highlighter-rouge">origin</code>은 <code class="language-plaintext highlighter-rouge">git clone</code>이 기본으로 등록하는 관례적인 이름이며 원하는 이름으로 바꿀 수 있다.</p>

<h2 id="4--clone과-fork">4 ) Clone과 Fork</h2>

<hr />

<p>Clone과 Fork는 Repository의 복사 위치와 소유 관계가 다르다.</p>

<table>
  <thead>
    <tr>
      <th>작업</th>
      <th>생성 위치</th>
      <th>결과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Clone</td>
      <td>Local Computer</td>
      <td>Remote Repository의 Local Repository와 Working Tree 생성</td>
    </tr>
    <tr>
      <td>Fork</td>
      <td>GitHub 계정</td>
      <td>원본과 별도로 관리되는 GitHub Remote Repository 생성</td>
    </tr>
  </tbody>
</table>

<h3 id="clone">Clone</h3>

<p>Repository를 현재 Directory 아래에 복제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="se">\</span>
  https://github.com/&lt;owner&gt;/&lt;repository&gt;.git
</code></pre></div></div>

<p>Local Directory 이름을 직접 지정할 수 있다. 대상 Directory는 비어 있거나 존재하지 않아야 한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="se">\</span>
  https://github.com/&lt;owner&gt;/&lt;repository&gt;.git <span class="se">\</span>
  &lt;local-directory&gt;
</code></pre></div></div>

<p>Clone이 끝나면 등록된 Remote와 Branch를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> &lt;local-directory&gt;
git remote <span class="nt">-v</span>
git branch <span class="nt">--all</span>
git log <span class="nt">--oneline</span> <span class="nt">--decorate</span> <span class="nt">-n</span> 5
</code></pre></div></div>

<p>큰 공개 Repository를 예제로 그대로 Clone하면 Network와 Disk를 많이 사용할 수 있다. Git 명령 학습에는 직접 만든 작은 Repository나 전용 실습 Repository를 사용한다.</p>

<h3 id="fork">Fork</h3>

<p>Fork는 GitHub에서 원본 Repository를 자신의 계정이나 조직 아래에 복사하는 기능이다. 원본 Repository에 직접 Push 권한이 없어도 Fork에서 Branch를 Push하고 Pull Request를 만들 수 있다.</p>

<p>Fork를 Clone하면 일반적으로 자신의 Fork가 <code class="language-plaintext highlighter-rouge">origin</code>이 된다. 원본 Repository의 변경을 가져오려면 <code class="language-plaintext highlighter-rouge">upstream</code> Remote를 별도로 등록한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote add upstream <span class="se">\</span>
  https://github.com/&lt;original-owner&gt;/&lt;repository&gt;.git
git remote <span class="nt">-v</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>origin
└── 자신의 GitHub Fork

upstream
└── 원본 GitHub Repository
</code></pre></div></div>

<h2 id="5--remote-연결과-변경">5 ) Remote 연결과 변경</h2>

<hr />

<h3 id="remote-추가">Remote 추가</h3>

<p>기존 Local Repository에 GitHub Repository를 연결한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote add origin <span class="se">\</span>
  https://github.com/&lt;owner&gt;/&lt;repository&gt;.git
git remote <span class="nt">-v</span>
</code></pre></div></div>

<p>같은 이름의 Remote가 이미 있으면 <code class="language-plaintext highlighter-rouge">remote origin already exists</code> 오류가 발생한다. 기존 URL을 확인하고 새 Remote가 필요한지 URL 변경이 필요한지 판단한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote get-url origin
</code></pre></div></div>

<h3 id="remote-url-변경">Remote URL 변경</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote set-url origin <span class="se">\</span>
  git@github.com:&lt;owner&gt;/&lt;repository&gt;.git
git remote <span class="nt">-v</span>
</code></pre></div></div>

<p>이 명령은 Local Repository의 연결 URL을 변경한다. GitHub Repository의 이름이나 소유자를 바꾸는 작업은 아니다.</p>

<h3 id="remote-이름-변경">Remote 이름 변경</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote rename &lt;old-name&gt; &lt;new-name&gt;
git remote <span class="nt">-v</span>
</code></pre></div></div>

<h3 id="remote-연결-제거">Remote 연결 제거</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote remove &lt;remote-name&gt;
git remote <span class="nt">-v</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">git remote remove</code>는 Local <code class="language-plaintext highlighter-rouge">.git/config</code>의 Remote 연결을 제거한다. GitHub의 Remote Repository와 그 안의 Data는 삭제하지 않는다.</p>

<h2 id="6--fetch와-pull">6 ) Fetch와 Pull</h2>

<hr />

<p>Remote의 변경을 가져오는 명령은 Local Branch를 자동으로 바꾸는지에 따라 구분한다.</p>

<h3 id="fetch">Fetch</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch origin
</code></pre></div></div>

<p>Fetch는 Remote의 새 Commit, Tag와 Branch Reference를 Local Repository로 가져온다. 현재 Working Tree와 Local Branch에는 자동으로 통합하지 않는다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GitHub origin/main
        │ git fetch origin
        ▼
Local origin/main 갱신
        │
        └── 현재 Local main은 그대로 유지
</code></pre></div></div>

<p>가져온 상태와 Local Branch를 비교한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span> <span class="nt">--all</span>
git diff main..origin/main
</code></pre></div></div>

<h3 id="pull">Pull</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git pull origin main
</code></pre></div></div>

<p>Pull은 먼저 Fetch한 뒤 가져온 변경을 현재 Branch에 통합한다. 기본 통합 방식은 Git 설정과 Option에 따라 Merge 또는 Rebase가 될 수 있다.</p>

<p>실행 전 현재 Branch와 Working Tree 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch <span class="nt">--show-current</span>
git status
git fetch origin
git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span> <span class="nt">--all</span>
</code></pre></div></div>

<p>통합 방식을 명시하면 의도가 분명해진다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Merge 방식</span>
git pull <span class="nt">--no-rebase</span> origin main

<span class="c"># Rebase 방식</span>
git pull <span class="nt">--rebase</span> origin main

<span class="c"># Fast-forward만 허용</span>
git pull <span class="nt">--ff-only</span> origin main
</code></pre></div></div>

<p>팀이 정한 History 정책과 현재 Branch의 공유 여부에 맞는 방식을 선택한다.</p>

<h2 id="7--local-repository를-github에-push">7 ) Local Repository를 GitHub에 Push</h2>

<hr />

<p>빈 GitHub Repository를 준비한 뒤 기존 Local Repository를 연결하는 흐름은 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Local File 작성
      │ git add
      ▼
Staging Area
      │ git commit
      ▼
Local Branch
      │ git remote add
      ▼
Remote 연결
      │ git push -u
      ▼
GitHub Remote Branch
</code></pre></div></div>

<h3 id="local-repository-확인">Local Repository 확인</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git branch <span class="nt">--show-current</span>
git log <span class="nt">--oneline</span> <span class="nt">--decorate</span> <span class="nt">-n</span> 5
</code></pre></div></div>

<p>아직 Commit이 없다면 File을 Stage하고 Commit한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add &lt;file&gt;
git diff <span class="nt">--staged</span>
git commit <span class="nt">-m</span> <span class="s2">"docs: add initial content"</span>
</code></pre></div></div>

<p>GitHub에서 만든 빈 Repository를 <code class="language-plaintext highlighter-rouge">origin</code>으로 등록한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote add origin <span class="se">\</span>
  https://github.com/&lt;owner&gt;/&lt;repository&gt;.git
git remote <span class="nt">-v</span>
</code></pre></div></div>

<p>현재 Branch 이름을 확인한 뒤 같은 이름의 Remote Branch로 Push한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch <span class="nt">--show-current</span>
git push <span class="nt">-u</span> origin main
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-u</code> 또는 <code class="language-plaintext highlighter-rouge">--set-upstream</code>은 현재 Local Branch가 어느 Remote Branch를 추적할지 설정한다. 관계가 설정된 뒤에는 Remote와 Branch 인자를 생략할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push
git pull <span class="nt">--ff-only</span>
</code></pre></div></div>

<p>새로운 Local Branch를 처음 Push할 때는 해당 Branch의 Upstream을 다시 설정할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git switch <span class="nt">-c</span> feature/login
git push <span class="nt">-u</span> origin feature/login
</code></pre></div></div>

<p>Local Repository의 초기 Branch가 항상 <code class="language-plaintext highlighter-rouge">master</code>이고 GitHub가 항상 <code class="language-plaintext highlighter-rouge">main</code>인 것은 아니다. 실제 이름을 확인하고 필요할 때만 Branch 이름을 변경한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git branch <span class="nt">--show-current</span>
git branch <span class="nt">-M</span> main
</code></pre></div></div>

<p>공유 중인 Branch 이름을 변경하면 Remote 설정, Pull Request와 다른 작업자의 Local Branch에 영향을 줄 수 있다.</p>

<h2 id="8--sourcetree에서-remote-작업">8 ) SourceTree에서 Remote 작업</h2>

<hr />

<p>SourceTree의 Remote 작업도 같은 Git 상태를 변경한다.</p>

<table>
  <thead>
    <tr>
      <th>SourceTree 작업</th>
      <th>Git 동작</th>
      <th>실행 후 확인</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Clone</td>
      <td><code class="language-plaintext highlighter-rouge">git clone</code></td>
      <td><code class="language-plaintext highlighter-rouge">git remote -v</code></td>
    </tr>
    <tr>
      <td>Remote 추가</td>
      <td><code class="language-plaintext highlighter-rouge">git remote add</code></td>
      <td><code class="language-plaintext highlighter-rouge">git remote get-url</code></td>
    </tr>
    <tr>
      <td>Fetch</td>
      <td><code class="language-plaintext highlighter-rouge">git fetch</code></td>
      <td><code class="language-plaintext highlighter-rouge">git log --all --graph</code></td>
    </tr>
    <tr>
      <td>Pull</td>
      <td><code class="language-plaintext highlighter-rouge">git pull</code></td>
      <td><code class="language-plaintext highlighter-rouge">git status</code>, <code class="language-plaintext highlighter-rouge">git log</code></td>
    </tr>
    <tr>
      <td>Push</td>
      <td><code class="language-plaintext highlighter-rouge">git push</code></td>
      <td>Local·Remote Branch 관계</td>
    </tr>
  </tbody>
</table>

<p>Push 화면에서는 대상 Remote와 Branch, Tag 포함 여부를 확인한다. GUI에 저장된 계정과 Repository를 실제로 Push할 권한이 있는 GitHub 계정이 다르면 인증 오류가 발생할 수 있다.</p>

<p>SourceTree에서 작업한 뒤 CLI로 다음 상태를 확인할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git remote <span class="nt">-v</span>
git branch <span class="nt">-vv</span>
git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span> <span class="nt">--all</span>
</code></pre></div></div>

<h2 id="9--push-실패-진단">9 ) Push 실패 진단</h2>

<hr />

<p>오류 Message에서 인증, 권한, URL과 History 문제를 먼저 구분한다.</p>

<table>
  <thead>
    <tr>
      <th>증상</th>
      <th>우선 확인할 내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">non-fast-forward</code></td>
      <td>Remote에 Local이 가지지 않은 Commit이 있는지 확인</td>
    </tr>
    <tr>
      <td>HTTPS <code class="language-plaintext highlighter-rouge">403</code></td>
      <td>현재 계정의 Repository 쓰기 권한과 Token Scope</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Authentication failed</code></td>
      <td>PAT 만료·폐기 여부와 Credential Helper의 저장 계정</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Permission denied (publickey)</code></td>
      <td>SSH Key, SSH Agent, GitHub Public Key 등록과 SSH URL</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Repository not found</code></td>
      <td>Owner·Repository 이름, Private Repository 접근 권한</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">remote origin already exists</code></td>
      <td>기존 <code class="language-plaintext highlighter-rouge">origin</code> URL과 추가하려는 URL 비교</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src refspec ... does not match any</code></td>
      <td>Local Commit 존재 여부와 Branch 이름</td>
    </tr>
  </tbody>
</table>

<h3 id="현재-연결-상태-확인">현재 연결 상태 확인</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote <span class="nt">-v</span>
git branch <span class="nt">--show-current</span>
git branch <span class="nt">-vv</span>
git status
</code></pre></div></div>

<h3 id="github-cli-인증-확인">GitHub CLI 인증 확인</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh auth status
</code></pre></div></div>

<h3 id="ssh-인증-확인">SSH 인증 확인</h3>

<p>Remote가 SSH URL일 때 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-T</span> git@github.com
</code></pre></div></div>

<h3 id="remote-history-확인">Remote History 확인</h3>

<p>Non-fast-forward 오류가 발생하면 Force Push 전에 Remote History를 가져온다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch origin
git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span> <span class="nt">--all</span>
</code></pre></div></div>

<p>Remote 변경을 확인하지 않은 <code class="language-plaintext highlighter-rouge">git push --force</code>는 다른 작업자의 Commit을 Remote Branch에서 제외할 수 있다. History를 다시 써야 하는 명확한 이유와 팀 합의가 없다면 사용하지 않는다.</p>

<h2 id="10--credential-노출-대응">10 ) Credential 노출 대응</h2>

<hr />

<p>PAT, Password나 Private Key가 Source File, Commit 또는 화면 공유에 노출되면 문자열을 지우는 작업만으로 해결되지 않는다.</p>

<ol>
  <li>
    <p>GitHub에서 해당 Credential을 폐기한다.</p>
  </li>
  <li>
    <p>필요한 최소 권한과 만료 기간으로 새 Credential을 발급한다.</p>
  </li>
  <li>
    <p>Local Credential Helper와 CI Secret을 새 값으로 갱신한다.</p>
  </li>
  <li>
    <p>Repository에 Commit됐다면 Git History와 Remote Cache의 정리 범위를 확인한다.</p>
  </li>
  <li>
    <p>Audit Log와 관련 Service Log에서 의심스러운 사용 기록을 확인한다.</p>
  </li>
</ol>

<p>새 Token 값을 문서의 예제에 다시 넣지 않는다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;GITHUB_USERNAME&gt;
&lt;GITHUB_PAT&gt;
&lt;REPOSITORY_URL&gt;
</code></pre></div></div>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>GitHub는 Git Remote Repository를 Hosting하고 Pull Request, Issue와 자동화 기능을 제공한다.</p>
    </li>
    <li>
      <p>HTTPS는 PAT와 Credential Helper를, SSH는 Local Private Key와 GitHub에 등록한 Public Key를 사용한다.</p>
    </li>
    <li>
      <p>Clone은 Local Repository를 만들고 Fork는 GitHub 계정 아래에 별도 Remote Repository를 만든다.</p>
    </li>
    <li>
      <p>Fetch는 Remote 상태를 가져오고 Pull은 가져온 변경을 현재 Branch에 통합한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">git push -u</code>는 현재 Local Branch와 Remote Branch의 Upstream 관계를 설정한다.</p>
    </li>
    <li>
      <p>Push 실패 시 Force Push보다 Remote URL, 인증 계정, 권한, Branch와 Remote History를 먼저 확인한다.</p>
    </li>
    <li>
      <p>노출된 Credential은 문서에서 삭제한 뒤 GitHub에서 폐기하고 새로 발급해야 한다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Git" /><summary type="html"><![CDATA[GitHub 인증 방식과 Clone·Fork·Remote·Fetch·Pull·Push의 차이 및 Local Repository를 Remote와 연결하는 방법]]></summary></entry><entry><title type="html">Container Image Registry와 Kubernetes Image Pull</title><link href="https://hyn128.site/cloud-native-41-container-image-registry/" rel="alternate" type="text/html" title="Container Image Registry와 Kubernetes Image Pull" /><published>2026-09-09T00:00:00+09:00</published><updated>2026-09-09T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-41-container-image-registry</id><content type="html" xml:base="https://hyn128.site/cloud-native-41-container-image-registry/"><![CDATA[<p><a href="/cloud-native-10-docker-images/">Docker 이미지와 Union File System</a>에서는 Image 이름, Tag와 Docker Hub Push를 다뤘다. 이 글에서는 범위를 Registry 전체로 확장하여 Image가 저장되고 배포되는 과정과 Kubernetes Worker가 Registry에서 Image를 가져오는 과정을 연결한다.</p>

<h2 id="1--image-registry">1 ) Image Registry</h2>

<hr />

<blockquote>
  <p><strong>Image Registry</strong></p>

  <p>Container Image와 OCI Artifact를 이름과 Version별로 저장하고 Client의 Push와 Pull 요청에 응답하는 Service이다.</p>
</blockquote>

<p>Registry는 Image File 하나를 그대로 보관하는 단순 File Server가 아니다. Image Manifest, 여러 Layer와 설정 정보를 Content Digest로 관리하고 Repository와 Tag를 통해 이를 찾을 수 있게 한다.</p>

<p>Image의 전체 참조 형식은 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>REGISTRY/NAMESPACE/REPOSITORY:TAG
REGISTRY/NAMESPACE/REPOSITORY@DIGEST
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>예시</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Registry</td>
      <td><code class="language-plaintext highlighter-rouge">docker.io</code></td>
      <td>Image를 제공하는 Registry 주소</td>
    </tr>
    <tr>
      <td>Namespace</td>
      <td><code class="language-plaintext highlighter-rouge">example-user</code></td>
      <td>사용자, 조직 또는 Project 구분</td>
    </tr>
    <tr>
      <td>Repository</td>
      <td><code class="language-plaintext highlighter-rouge">python-app</code></td>
      <td>하나의 Application Image 계열</td>
    </tr>
    <tr>
      <td>Tag</td>
      <td><code class="language-plaintext highlighter-rouge">v1.0</code></td>
      <td>사람이 읽을 수 있는 Version 별칭</td>
    </tr>
    <tr>
      <td>Digest</td>
      <td><code class="language-plaintext highlighter-rouge">sha256:...</code></td>
      <td>Image Content를 식별하는 불변 Hash</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">docker.io/library/nginx:1.29</code>에서 <code class="language-plaintext highlighter-rouge">docker.io</code>는 Registry, <code class="language-plaintext highlighter-rouge">library</code>는 Namespace, <code class="language-plaintext highlighter-rouge">nginx</code>는 Repository, <code class="language-plaintext highlighter-rouge">1.29</code>는 Tag이다. Tag는 다른 Image를 가리키도록 바뀔 수 있지만 Digest는 같은 Content를 계속 가리킨다.</p>

<h2 id="2--public-registry와-private-registry">2 ) Public Registry와 Private Registry</h2>

<hr />

<p>Registry는 접근 범위와 운영 주체에 따라 구분할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>예시</th>
      <th>특징</th>
      <th>확인할 사항</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Public Registry Service</td>
      <td>Docker Hub, GHCR, Quay.io</td>
      <td>Internet에서 바로 사용하기 쉽고 공개 Image 생태계가 큼</td>
      <td>Rate Limit, 공개 범위, 계정 보안</td>
    </tr>
    <tr>
      <td>Cloud Registry Service</td>
      <td>Amazon ECR, Google Artifact Registry 등</td>
      <td>Cloud IAM, Audit와 CI/CD Service 연동</td>
      <td>Region, 권한, Network와 비용 정책</td>
    </tr>
    <tr>
      <td>자체 Private Registry</td>
      <td>CNCF Distribution, Harbor</td>
      <td>내부 Network와 보안 정책에 맞게 직접 운영</td>
      <td>TLS, 인증, Storage, Backup, 고가용성</td>
    </tr>
  </tbody>
</table>

<p>Private Registry가 반드시 사내 Server를 의미하는 것은 아니다. Docker Hub, GHCR와 Cloud Registry에서도 비공개 Repository를 사용할 수 있다. 반대로 자체 Registry를 실행해도 인증과 Network 접근 제어가 없다면 안전한 Private Registry라고 볼 수 없다.</p>

<p>Service의 무료 범위, 저장 용량과 과금 정책은 변경될 수 있으므로 Registry 선택 시 현재 공식 Plan을 확인한다.</p>

<h2 id="3--image-배포-흐름">3 ) Image 배포 흐름</h2>

<hr />

<p>Application을 Registry를 통해 배포하는 기본 흐름은 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Source Code + Dockerfile
          │ docker build
          ▼
      Local Image
          │ docker tag
          ▼
Registry 주소가 포함된 Image 참조
          │ docker push
          ▼
        Registry
          │ docker pull
          ▼
Docker Host 또는 Kubernetes Worker
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>명령</th>
      <th>결과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Build</td>
      <td><code class="language-plaintext highlighter-rouge">docker build</code></td>
      <td>Local Image Store에 Image 생성</td>
    </tr>
    <tr>
      <td>Tag</td>
      <td><code class="language-plaintext highlighter-rouge">docker tag</code></td>
      <td>같은 Image에 Registry용 참조 이름 추가</td>
    </tr>
    <tr>
      <td>Push</td>
      <td><code class="language-plaintext highlighter-rouge">docker push</code></td>
      <td>Registry에 Manifest와 Layer 업로드</td>
    </tr>
    <tr>
      <td>Pull</td>
      <td><code class="language-plaintext highlighter-rouge">docker pull</code></td>
      <td>Registry에서 Manifest와 필요한 Layer 다운로드</td>
    </tr>
  </tbody>
</table>

<p>Application Image를 만드는 방법은 <a href="/cloud-native-14-application-image/">애플리케이션별 Docker 이미지 빌드</a>에서 자세히 다룬다. 여기서는 다음 Image가 Local에 준비되어 있다고 가정한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker image <span class="nb">ls </span>my-python-app
</code></pre></div></div>

<h2 id="4--docker-hub에-image-배포">4 ) Docker Hub에 Image 배포</h2>

<hr />

<h3 id="로그인">로그인</h3>

<p>Docker Hub 계정 이름을 지정하고 로그인한다. Password 대신 Personal Access Token을 사용하는 경우에도 명령 인자에 Token을 직접 작성하지 않고 Prompt에 입력한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker login <span class="nt">-u</span> &lt;docker-hub-username&gt;
</code></pre></div></div>

<p>로그인 결과는 일반적으로 사용자별 Docker 설정 File에 저장된다. 개인 Token을 Markdown, Shell Script, Git Repository나 화면에 표시되는 명령에 넣지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker info
</code></pre></div></div>

<h3 id="tag와-push">Tag와 Push</h3>

<p>Local Image에 Docker Hub Namespace가 포함된 이름을 추가한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker tag my-python-app:latest <span class="se">\</span>
  &lt;docker-hub-username&gt;/python-app:v1.0
</code></pre></div></div>

<p>두 이름이 같은 Image ID를 가리키는지 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker image <span class="nb">ls</span> <span class="se">\</span>
  &lt;docker-hub-username&gt;/python-app:v1.0
</code></pre></div></div>

<p>Registry로 Push한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker push <span class="se">\</span>
  &lt;docker-hub-username&gt;/python-app:v1.0
</code></pre></div></div>

<p>Push가 끝나면 다른 Host 또는 Local Image를 사용하지 않는 격리된 환경에서 Pull하여 Registry 배포를 검증한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull <span class="se">\</span>
  &lt;docker-hub-username&gt;/python-app:v1.0
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">latest</code>는 자동으로 최신 Version을 찾는 기능이 아니라 이름이 <code class="language-plaintext highlighter-rouge">latest</code>인 Tag이다. 배포 Manifest에는 검증한 Version Tag를 사용하고 더 강한 재현성이 필요하면 Digest를 고정한다.</p>

<h3 id="public과-private-repository">Public과 Private Repository</h3>

<p>Public Repository의 Image는 일반적으로 인증 없이 Pull할 수 있다. Private Repository는 Registry가 발급한 Credential이 필요하며 Docker Hub의 제공 범위와 비용 정책은 현재 Plan을 확인해야 한다.</p>

<p>로그인하지 않은 상태에서 Private Image Pull이 실패하면 다음 항목을 확인한다.</p>

<ul>
  <li>
    <p>Image 이름의 Namespace와 Repository가 정확한지 확인한다.</p>
  </li>
  <li>
    <p>계정 또는 Token에 Repository를 읽을 권한이 있는지 확인한다.</p>
  </li>
  <li>
    <p>Token이 만료되거나 폐기되지 않았는지 확인한다.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">docker logout</code> 후 올바른 계정으로 다시 로그인했는지 확인한다.</p>
  </li>
</ul>

<h2 id="5--kubernetes의-image-pull">5 ) Kubernetes의 Image Pull</h2>

<hr />

<p>Kubernetes는 Registry에 Image를 Push하지 않는다. 관리자가 Deployment를 생성하면 Control Plane이 Pod 배치를 결정하고, 선택된 Worker의 kubelet이 Container Runtime을 통해 Image를 Pull한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>관리 Client
    │ Deployment 제출
    ▼
API Server
    │
    ├── Deployment·ReplicaSet Controller가 Pod 생성
    │
    └── Scheduler가 실행할 Worker 결정
                         │
                         ▼
                    Worker kubelet
                         │ CRI 요청
                         ▼
                       containerd
                         │ Image Pull
                         ▼
                       Registry
                         │ Layer 저장
                         ▼
                    Container 실행
</code></pre></div></div>

<h3 id="public-image를-사용하는-deployment">Public Image를 사용하는 Deployment</h3>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">flask-deployment.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">flask-deployment</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">flask-web</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">flask-web</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">flask-container</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">&lt;docker-hub-username&gt;/python-app:v1.0</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
              <span class="na">containerPort</span><span class="pi">:</span> <span class="m">5000</span>
</code></pre></div></div>

<p>Service는 세 Pod의 Port <code class="language-plaintext highlighter-rouge">5000</code>으로 요청을 전달한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">flask-service.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">flask-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">flask-web</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
      <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">5000</span>
</code></pre></div></div>

<p>Control Plane에 접근 가능한 관리 Client에서 적용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> flask-deployment.yaml
kubectl apply <span class="nt">-f</span> flask-service.yaml
kubectl rollout status deployment/flask-deployment
kubectl get pods <span class="nt">-o</span> wide
</code></pre></div></div>

<p>Ingress를 연결하기 전에 Port Forwarding으로 Service와 Pod 응답을 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl port-forward service/flask-service 8080:8080
</code></pre></div></div>

<p>다른 Terminal에서 요청한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://127.0.0.1:8080
</code></pre></div></div>

<p>Application Log와 Pod Event를 함께 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl logs deployment/flask-deployment <span class="nt">--all-pods</span><span class="o">=</span><span class="nb">true
</span>kubectl describe pod &lt;pod-name&gt;
kubectl get events <span class="se">\</span>
  <span class="nt">--sort-by</span><span class="o">=</span>.metadata.creationTimestamp
</code></pre></div></div>

<p>외부 Host 기반 Routing은 <a href="/cloud-native-33-kubernetes-ingress-routing/">Kubernetes Ingress와 HTTP Routing</a>에서 이어서 다룬다. 새 Ingress Manifest는 Legacy Annotation 대신 <code class="language-plaintext highlighter-rouge">spec.ingressClassName</code>을 사용한다.</p>

<h3 id="private-image와-imagepullsecrets">Private Image와 <code class="language-plaintext highlighter-rouge">imagePullSecrets</code></h3>

<p>Private Repository를 사용하는 Pod는 같은 Namespace의 Registry Credential Secret을 참조한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spec</span><span class="pi">:</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">imagePullSecrets</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">regcred</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">flask-container</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">&lt;docker-hub-username&gt;/private-python-app:v1.0</span>
</code></pre></div></div>

<p>동작 관계는 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pod의 imagePullSecrets
        │
        ▼
Worker kubelet이 같은 Namespace의 Secret 확인
        │
        ▼
CRI Image Pull 요청에 Registry 인증 정보 전달
        │
        ▼
containerd가 Private Registry에서 Image Pull
</code></pre></div></div>

<p>Secret 생성과 안전한 Credential 관리 방법은 <a href="/cloud-native-34-kubernetes-env-secret-configmap/">Kubernetes 환경 변수, Secret과 ConfigMap</a>의 Private Registry Secret 절에서 다룬다.</p>

<p><code class="language-plaintext highlighter-rouge">ImagePullBackOff</code>가 발생하면 Application Log보다 Pod Event를 먼저 확인한다. Container가 시작되기 전 Image Pull 단계에서 실패하면 Application Log가 존재하지 않을 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe pod &lt;pod-name&gt;
kubectl get pod &lt;pod-name&gt; <span class="se">\</span>
  <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.containers[*].image}{"\n"}'</span>
</code></pre></div></div>

<h2 id="6--자체-registry-구성">6 ) 자체 Registry 구성</h2>

<hr />

<p>CNCF Distribution은 OCI Distribution API를 제공하는 가벼운 Registry 구현이다. Local File System을 기본 Storage로 사용할 수 있고 필요에 따라 외부 Object Storage와 인증 구성을 연결할 수 있다.</p>

<h3 id="격리된-실습용-registry-실행">격리된 실습용 Registry 실행</h3>

<p>다음 구성은 <code class="language-plaintext highlighter-rouge">localhost</code> 또는 신뢰할 수 있는 폐쇄형 실습 Network에서 Registry API를 확인하기 위한 예제이다. 외부에 공개할 운영 구성으로 사용하지 않는다.</p>

<p>Registry Data를 Container 삭제와 분리하기 위해 Named Volume을 먼저 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker volume create registry-data
</code></pre></div></div>

<p>Registry Container를 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">--name</span> registry <span class="se">\</span>
  <span class="nt">--restart</span><span class="o">=</span>always <span class="se">\</span>
  <span class="nt">-p</span> 5000:5000 <span class="se">\</span>
  <span class="nt">-v</span> registry-data:/var/lib/registry <span class="se">\</span>
  registry:3
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>설정</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-p 5000:5000</code></td>
      <td>Host의 TCP <code class="language-plaintext highlighter-rouge">5000</code>을 Registry API에 연결</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--restart=always</code></td>
      <td>Docker Daemon 재시작 후 Container 재실행</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-v registry-data:/var/lib/registry</code></td>
      <td>Registry Manifest와 Layer를 Volume에 저장</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">registry:3</code></td>
      <td>CNCF Distribution 3.x Image 사용</td>
    </tr>
  </tbody>
</table>

<p>Container 상태, API 응답과 Log를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker ps <span class="nt">--filter</span> <span class="nv">name</span><span class="o">=</span>registry
curl <span class="nt">-i</span> http://127.0.0.1:5000/v2/
docker logs registry
</code></pre></div></div>

<p>인증이 없는 Local Registry의 <code class="language-plaintext highlighter-rouge">/v2/</code>가 정상 동작하면 빈 JSON Object인 <code class="language-plaintext highlighter-rouge">{}</code>가 반환될 수 있다. 인증을 적용한 Registry에서는 <code class="language-plaintext highlighter-rouge">401 Unauthorized</code>와 인증 Challenge가 정상 상태일 수 있다.</p>

<h3 id="local-registry에-push">Local Registry에 Push</h3>

<p>Image에 Registry 주소가 포함된 Tag를 추가한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker tag my-python-app:latest <span class="se">\</span>
  127.0.0.1:5000/python-app:v1
</code></pre></div></div>

<p>Push한 뒤 Registry Catalog와 Tag를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker push 127.0.0.1:5000/python-app:v1
curl http://127.0.0.1:5000/v2/_catalog
curl http://127.0.0.1:5000/v2/python-app/tags/list
</code></pre></div></div>

<p>Registry Container를 재시작하고 Image를 다시 Pull하여 Volume의 Data가 유지되는지 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker restart registry
docker pull 127.0.0.1:5000/python-app:v1
docker logs <span class="nt">--tail</span> 50 registry
</code></pre></div></div>

<h2 id="7--http-registry와-tls-registry">7 ) HTTP Registry와 TLS Registry</h2>

<hr />

<p>Docker와 containerd는 Registry 통신에서 HTTPS를 기본으로 기대한다. Plain HTTP Registry는 Traffic과 Credential을 보호하지 못하므로 격리된 실습 환경에서만 제한적으로 사용한다.</p>

<table>
  <thead>
    <tr>
      <th>환경</th>
      <th>통신 방식</th>
      <th>요구 사항</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Local 단일 Host 실습</td>
      <td><code class="language-plaintext highlighter-rouge">http://127.0.0.1:5000</code></td>
      <td>외부 접근 차단, 실제 Credential 사용 금지</td>
    </tr>
    <tr>
      <td>폐쇄형 다중 Node 실습</td>
      <td>임시 HTTP 또는 내부 CA 기반 HTTPS</td>
      <td>접근 가능한 Node 제한, 사용 목적과 위험 명시</td>
    </tr>
    <tr>
      <td>운영 환경</td>
      <td>신뢰 가능한 TLS 기반 HTTPS</td>
      <td>인증, 권한, 영구 Storage, Backup, Monitoring</td>
    </tr>
  </tbody>
</table>

<p>운영 Registry는 다음 항목을 함께 설계한다.</p>

<ul>
  <li>
    <p>DNS Name과 Server 인증서의 Subject Alternative Name이 일치해야 한다.</p>
  </li>
  <li>
    <p>Registry를 사용하는 모든 Client가 인증서 발급 CA를 신뢰해야 한다.</p>
  </li>
  <li>
    <p>Repository별 Push와 Pull 권한을 분리한다.</p>
  </li>
  <li>
    <p>Registry Metadata와 Blob Storage를 영구 저장하고 Backup한다.</p>
  </li>
  <li>
    <p>Disk 사용량, 오류율, 인증 실패와 취약점 Scan 결과를 Monitoring한다.</p>
  </li>
</ul>

<h3 id="docker-engine의-실습용-http-허용">Docker Engine의 실습용 HTTP 허용</h3>

<p>Docker Engine으로 Remote HTTP Registry에 Push하거나 Pull해야 하는 격리된 실습 환경에서는 <code class="language-plaintext highlighter-rouge">/etc/docker/daemon.json</code>의 <code class="language-plaintext highlighter-rouge">insecure-registries</code>에 정확한 Host와 Port를 등록할 수 있다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"insecure-registries"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"192.168.0.100:5000"</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>기존 <code class="language-plaintext highlighter-rouge">daemon.json</code>에 다른 설정이 있다면 File 전체를 덮어쓰지 않고 JSON Object 안에 항목을 병합한다. 문법을 확인한 뒤 Docker를 재시작한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart docker
docker info
</code></pre></div></div>

<p>이 설정은 해당 Docker Daemon의 동작만 바꾼다. Kubernetes Worker가 containerd를 사용한다면 Docker 설정만으로 kubelet의 Image Pull이 허용되지 않는다.</p>

<h2 id="8--kubernetes-worker의-containerd-registry-설정">8 ) Kubernetes Worker의 containerd Registry 설정</h2>

<hr />

<p>Registry 설정은 실제 Image를 Pull할 가능성이 있는 모든 Worker에 필요하다. Control Plane Node에도 Workload Scheduling을 허용했다면 해당 Node 역시 같은 설정 대상이다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Control Plane
└── Pod를 어느 Worker에 배치할지 결정

Worker
├── kubelet이 PodSpec의 Image 참조 확인
├── containerd에 Pull 요청
└── Worker Local Storage에 Image Layer 저장
</code></pre></div></div>

<h3 id="hoststoml-작성"><code class="language-plaintext highlighter-rouge">hosts.toml</code> 작성</h3>

<p>각 Worker에서 Registry Host와 Port에 해당하는 Directory를 만든다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> <span class="se">\</span>
  /etc/containerd/certs.d/192.168.0.100:5000
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/etc/containerd/certs.d/192.168.0.100:5000/hosts.toml</code>에 격리된 HTTP 실습 Registry를 등록한다.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">server</span> <span class="p">=</span> <span class="s">"http://192.168.0.100:5000"</span>

<span class="nn">[host."http://192.168.0.100:5000"]</span>
  <span class="py">capabilities</span> <span class="p">=</span> <span class="p">[</span><span class="s">"pull"</span><span class="p">,</span> <span class="s">"resolve"</span><span class="p">]</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">skip_verify</code>는 HTTPS 인증서 검증을 건너뛰는 Option이다. Plain HTTP를 지정하는 설정에 습관적으로 추가하지 않는다. 운영 환경에서는 <code class="language-plaintext highlighter-rouge">http://</code> 대신 <code class="language-plaintext highlighter-rouge">https://</code>를 사용하고 필요한 CA 인증서를 배포한다.</p>

<h3 id="containerd-1x의-config_path">containerd 1.x의 <code class="language-plaintext highlighter-rouge">config_path</code></h3>

<p><code class="language-plaintext highlighter-rouge">/etc/containerd/config.toml</code>이 Version 2 형식이고 containerd 1.x를 사용한다면 다음 Section을 확인한다.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">version</span> <span class="p">=</span> <span class="mi">2</span>

<span class="nn">[plugins."io.containerd.grpc.v1.cri".registry]</span>
  <span class="py">config_path</span> <span class="p">=</span> <span class="s">"/etc/containerd/certs.d"</span>
</code></pre></div></div>

<h3 id="containerd-2x의-config_path">containerd 2.x의 <code class="language-plaintext highlighter-rouge">config_path</code></h3>

<p>containerd 2.x의 Version 3 설정에서는 Plugin 경로가 다르다.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">version</span> <span class="p">=</span> <span class="mi">3</span>

<span class="nn">[plugins."io.containerd.cri.v1.images".registry]</span>
  <span class="py">config_path</span> <span class="p">=</span> <span class="s">"/etc/containerd/certs.d"</span>
</code></pre></div></div>

<p>기존 설정의 <code class="language-plaintext highlighter-rouge">version</code>과 Plugin Section을 먼저 확인하고 서로 다른 Version 예제를 한 File에 동시에 넣지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>containerd <span class="nt">--version</span>
<span class="nb">sudo </span>containerd config dump | less
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">config_path</code>를 처음 활성화하도록 <code class="language-plaintext highlighter-rouge">config.toml</code>을 변경했다면 containerd를 재시작하고 상태와 Log를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart containerd
<span class="nb">sudo </span>systemctl status containerd <span class="nt">--no-pager</span>
<span class="nb">sudo </span>journalctl <span class="nt">-u</span> containerd <span class="nt">-n</span> 100 <span class="nt">--no-pager</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">config_path</code>가 이미 설정된 환경에서 Registry별 <code class="language-plaintext highlighter-rouge">hosts.toml</code>만 변경하는 경우에는 containerd가 해당 Directory의 변경을 읽으므로 일반적으로 Daemon 재시작이 필요하지 않다.</p>

<p>각 Worker에서 CRI 경로를 직접 검증한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>crictl pull <span class="se">\</span>
  192.168.0.100:5000/python-app:v1
<span class="nb">sudo </span>crictl images
</code></pre></div></div>

<p>그다음 Control Plane에 접근 가능한 관리 Client에서 Pod를 다시 생성하거나 Deployment Rollout을 수행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl rollout restart deployment/flask-deployment
kubectl rollout status deployment/flask-deployment
kubectl get pods <span class="nt">-o</span> wide
</code></pre></div></div>

<h3 id="deprecated-registry-설정">Deprecated Registry 설정</h3>

<p>다음과 같이 CRI Plugin 아래에 <code class="language-plaintext highlighter-rouge">registry.mirrors</code>와 <code class="language-plaintext highlighter-rouge">registry.configs</code>를 직접 작성하는 방식은 Deprecated 상태이다.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.0.100:5000"]</span>
  <span class="py">endpoint</span> <span class="p">=</span> <span class="nn">["http://192.168.0.100:5000"]</span>
</code></pre></div></div>

<p>기존 환경을 확인할 때는 볼 수 있지만 신규 구성은 <code class="language-plaintext highlighter-rouge">config_path</code>와 Registry별 <code class="language-plaintext highlighter-rouge">hosts.toml</code>을 사용한다.</p>

<h2 id="9--legacy-registry-web-ui">9 ) Legacy Registry Web UI</h2>

<hr />

<p>Registry API 자체는 Image 검색과 권한 관리를 위한 완성된 Web Portal을 제공하지 않는다. 과거에는 <code class="language-plaintext highlighter-rouge">hyper/docker-registry-web</code> Container를 <code class="language-plaintext highlighter-rouge">--link</code>로 Registry에 연결하는 예제가 사용되었다.</p>

<p>이 구성은 다음 이유로 신규 환경에 그대로 적용하지 않는다.</p>

<ul>
  <li>
    <p>Docker의 Legacy Container Link에 의존한다.</p>
  </li>
  <li>
    <p>오래된 별도 Image의 유지보수와 보안 상태를 추가로 검증해야 한다.</p>
  </li>
  <li>
    <p>인증, 권한, 취약점 Scan과 Project 관리가 Registry와 분리된다.</p>
  </li>
</ul>

<p>단순 API 확인은 <code class="language-plaintext highlighter-rouge">/v2/</code>, <code class="language-plaintext highlighter-rouge">/v2/_catalog</code>와 <code class="language-plaintext highlighter-rouge">/tags/list</code>를 사용하고 관리 UI와 보안 기능이 필요하면 Harbor 같은 Registry Platform을 검토한다.</p>

<h2 id="10--harbor">10 ) Harbor</h2>

<hr />

<p>Harbor는 OCI 호환 Registry를 기반으로 내부 Image와 Artifact를 관리하는 Open Source Registry Platform이다.</p>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Project</td>
      <td>Repository와 접근 범위를 조직 단위로 구분</td>
    </tr>
    <tr>
      <td>RBAC</td>
      <td>사용자와 Robot Account의 Push·Pull 권한 관리</td>
    </tr>
    <tr>
      <td>Web UI</td>
      <td>Repository, Artifact, Tag와 Scan 결과 확인</td>
    </tr>
    <tr>
      <td>Vulnerability Scan</td>
      <td>저장된 Artifact의 알려진 취약점 검사</td>
    </tr>
    <tr>
      <td>Replication</td>
      <td>다른 Registry와 Artifact 복제</td>
    </tr>
    <tr>
      <td>OCI Artifact</td>
      <td>Container Image 외 Helm Chart 등의 Artifact 저장</td>
    </tr>
  </tbody>
</table>

<p>CNCF Distribution이 Registry API의 가벼운 기반을 제공한다면 Harbor는 조직에서 필요한 사용자 관리, 보안 검사와 운영 UI를 함께 제공한다. 그 대신 구성 요소와 운영 부담이 더 크므로 단일 개발 Host의 단순 저장소에는 Distribution이 적합할 수 있고, 여러 사용자와 Project를 관리해야 하는 환경에는 Harbor가 적합할 수 있다.</p>

<p>Helm Chart도 OCI Artifact로 Registry에 저장할 수 있다. Chart Package의 실제 Push와 Pull 과정은 <a href="/cloud-native-40-kubernetes-helm/">Kubernetes Helm Chart와 Release 관리</a>에서 이어서 다룬다.</p>

<h2 id="11--registry-문제-진단-순서">11 ) Registry 문제 진단 순서</h2>

<hr />

<p>Image Pull 실패는 Application 실행 이전 단계의 문제이다. 다음 순서로 범위를 좁힌다.</p>

<ol>
  <li>
    <p>Image 참조의 Registry, Namespace, Repository와 Tag가 정확한지 확인한다.</p>
  </li>
  <li>
    <p>실행 주체가 Public Image인지 Private Image인지 확인한다.</p>
  </li>
  <li>
    <p>Registry API와 TCP Port에 접근할 수 있는지 확인한다.</p>
  </li>
  <li>
    <p>TLS 인증서와 Client의 CA 신뢰 상태를 확인한다.</p>
  </li>
  <li>
    <p>Docker 또는 containerd 중 실제 Pull을 수행하는 Runtime 설정을 확인한다.</p>
  </li>
  <li>
    <p>Kubernetes에서는 Secret의 Namespace와 <code class="language-plaintext highlighter-rouge">imagePullSecrets</code> 이름을 확인한다.</p>
  </li>
  <li>
    <p>Pod Event, containerd Log와 Registry Log의 같은 시간대 요청을 대조한다.</p>
  </li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Registry Host</span>
docker logs <span class="nt">--since</span> 10m registry

<span class="c"># Worker</span>
<span class="nb">sudo </span>journalctl <span class="nt">-u</span> containerd <span class="nt">--since</span> <span class="s2">"10 minutes ago"</span>

<span class="c"># Control Plane에 접근 가능한 관리 Client</span>
kubectl describe pod &lt;pod-name&gt;
kubectl get events <span class="se">\</span>
  <span class="nt">--sort-by</span><span class="o">=</span>.metadata.creationTimestamp
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>증상</th>
      <th>우선 확인 대상</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ImagePullBackOff</code></td>
      <td>직전 Pull 실패 원인을 보여주는 Pod Event</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">unauthorized</code></td>
      <td>Registry Credential과 Repository 권한</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">manifest unknown</code></td>
      <td>Repository, Tag와 Platform Manifest</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">x509: certificate signed by unknown authority</code></td>
      <td>Worker의 CA Trust와 Registry 인증서 Chain</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">http: server gave HTTP response to HTTPS client</code></td>
      <td>HTTP Registry 허용 설정과 실제 Protocol</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">connection refused</code></td>
      <td>Registry Process, Listen Port, Firewall와 주소</td>
    </tr>
  </tbody>
</table>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>Registry는 Image Manifest와 Layer를 Repository, Tag와 Digest로 관리하고 Push와 Pull API를 제공한다.</p>
    </li>
    <li>
      <p>Kubernetes에서는 Scheduler가 Worker를 선택하고 해당 Worker의 kubelet과 containerd가 Registry에서 Image를 Pull한다.</p>
    </li>
    <li>
      <p>Private Image는 같은 Namespace의 <code class="language-plaintext highlighter-rouge">imagePullSecrets</code>와 Registry 읽기 권한이 필요하다.</p>
    </li>
    <li>
      <p>Docker Engine의 Registry 설정과 Kubernetes Worker의 containerd Registry 설정은 서로 다른 대상이다.</p>
    </li>
    <li>
      <p>Plain HTTP와 인증서 검증 생략은 격리된 실습에만 제한하고 운영 Registry에는 TLS, 인증, 권한과 영구 Storage를 구성한다.</p>
    </li>
    <li>
      <p>containerd의 Legacy <code class="language-plaintext highlighter-rouge">registry.mirrors</code> 방식은 Deprecated 상태이며 신규 구성은 <code class="language-plaintext highlighter-rouge">config_path</code>와 <code class="language-plaintext highlighter-rouge">hosts.toml</code>을 사용한다.</p>
    </li>
    <li>
      <p>Harbor는 OCI Registry에 Project, RBAC, Web UI, 취약점 Scan과 Replication을 결합한 Platform이다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Docker" /><summary type="html"><![CDATA[Public·Private Image Registry의 구조와 Docker Hub 배포, 자체 Registry 구성, Kubernetes Worker의 Image Pull 및 Harbor의 역할]]></summary></entry><entry><title type="html">Version Control과 Git 기본 작업 흐름</title><link href="https://hyn128.site/cloud-native-42-git-version-control-basics/" rel="alternate" type="text/html" title="Version Control과 Git 기본 작업 흐름" /><published>2026-09-09T00:00:00+09:00</published><updated>2026-09-09T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-42-git-version-control-basics</id><content type="html" xml:base="https://hyn128.site/cloud-native-42-git-version-control-basics/"><![CDATA[<p>Container Image, Kubernetes Manifest와 Helm Chart도 Source Code와 함께 계속 변경된다. Git을 사용하면 이러한 File의 변경 이력을 기록하고, 특정 시점의 상태를 다시 확인하며, 여러 사람이 변경 내용을 교환하고 병합할 수 있다.</p>

<p>이 글에서 Local Repository와 Commit의 기본 구조를 익힌 뒤 <a href="/cloud-native-43-git-history-branch/">Git 이력 관리와 Branch 작업</a>에서 이력 비교, 복구와 Branch 통합을 다룬다. Remote Repository와의 작업은 <a href="/cloud-native-44-github-remote-workflow/">GitHub Remote Repository 작업 흐름</a>으로 이어진다.</p>

<h2 id="1--version-control-system">1 ) Version Control System</h2>

<hr />

<blockquote>
  <p><strong>Version Control System, VCS</strong></p>

  <p>File의 변경 내용을 시간 순서대로 기록하여 특정 Version을 확인하거나 복원하고 여러 작업자의 변경을 조정하는 System이다.</p>
</blockquote>

<p>Version 관리 대상은 Source Code로 제한되지 않는다. Text로 표현할 수 있는 설정 File, 문서, Kubernetes Manifest와 Script도 관리할 수 있다. Binary File도 저장할 수 있지만 작은 변경에도 File 전체가 크게 달라질 수 있어 저장 공간과 변경 비교 측면에서 별도 관리 전략이 필요하다.</p>

<p>Version 관리로 해결하려는 주요 문제는 다음과 같다.</p>

<ul>
  <li>
    <p>누가 언제 어떤 내용을 변경했는지 추적한다.</p>
  </li>
  <li>
    <p>현재 상태와 이전 상태의 차이를 비교한다.</p>
  </li>
  <li>
    <p>문제가 생긴 변경의 범위를 찾고 필요한 Version으로 복원한다.</p>
  </li>
  <li>
    <p>여러 작업자가 만든 변경을 하나의 History로 병합한다.</p>
  </li>
  <li>
    <p>Local 작업과 Server의 공유 상태를 분리한다.</p>
  </li>
</ul>

<h2 id="2--version-control-system의-종류">2 ) Version Control System의 종류</h2>

<hr />

<p>Version Control System은 History를 저장하고 공유하는 구조에 따라 Local, Centralized와 Distributed 방식으로 구분할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>History 저장 위치</th>
      <th>협업 방식</th>
      <th>특징</th>
      <th>대표 예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Local VCS</td>
      <td>한 Computer의 Local Database</td>
      <td>File을 별도 수단으로 전달</td>
      <td>개인 변경 추적에는 사용할 수 있지만 중앙 협업 기능이 부족</td>
      <td>RCS</td>
    </tr>
    <tr>
      <td>Centralized VCS</td>
      <td>중앙 Server</td>
      <td>Client가 Server에서 Checkout·Commit</td>
      <td>권한과 History를 중앙에서 관리하지만 Server 의존도가 큼</td>
      <td>CVS, Subversion, Perforce</td>
    </tr>
    <tr>
      <td>Distributed VCS</td>
      <td>각 Clone에 Repository History 저장</td>
      <td>Remote Repository와 Fetch·Push</td>
      <td>Network 연결 없이도 Local Commit과 History 조회 가능</td>
      <td>Git, Mercurial</td>
    </tr>
  </tbody>
</table>

<h3 id="local-version-control">Local Version Control</h3>

<p>Local VCS는 같은 Computer 안의 Database에 Revision이나 Patch를 기록한다. 수동으로 날짜별 Directory를 복사하는 것보다 변경 이력을 체계적으로 관리할 수 있지만 다른 작업자와 History를 공유하고 병합하기에는 적합하지 않다.</p>

<p>RCS는 대표적인 Local Version Control System이다. 특정 운영체제나 개발 도구 설치 여부만으로 항상 제공된다고 가정하지 않고 현재 환경에서 Package 존재 여부를 확인한다.</p>

<h3 id="centralized-version-control">Centralized Version Control</h3>

<p>Centralized VCS는 중앙 Server가 기준 Repository를 관리한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client A ─┐
          ├── Central VCS Server
Client B ─┘
</code></pre></div></div>

<p>모든 사용자가 같은 중앙 History를 기준으로 작업하기 쉬운 반면 Server에 접속할 수 없으면 Update나 Commit 같은 주요 작업이 제한될 수 있다. Server 장애가 곧바로 모든 Local 작업 File의 소실을 의미하지는 않지만 중앙 Repository의 Backup과 가용성이 중요하다.</p>

<h3 id="distributed-version-control">Distributed Version Control</h3>

<p>Distributed VCS의 Clone에는 Working File만 아니라 Repository History와 Metadata가 함께 저장된다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Developer A Local Repository
              │ fetch·push
              ▼
        Remote Repository
              ▲
              │ fetch·push
Developer B Local Repository
</code></pre></div></div>

<p>각 작업자는 Network가 없어도 Local Commit, Branch 생성과 History 조회를 수행할 수 있다. <code class="language-plaintext highlighter-rouge">checkout</code>할 때마다 Repository 전체를 다시 Backup하는 방식은 아니다. 일반적으로 <code class="language-plaintext highlighter-rouge">clone</code>이 Repository를 처음 가져오고, 이후 <code class="language-plaintext highlighter-rouge">fetch</code>가 Remote의 새로운 Object와 Reference를 전달하며, <code class="language-plaintext highlighter-rouge">checkout</code> 또는 <code class="language-plaintext highlighter-rouge">switch</code>는 가지고 있는 Object를 이용해 Working Tree의 상태를 바꾼다.</p>

<h2 id="3--git">3 ) Git</h2>

<hr />

<p>Git은 File 변경을 추적하고 여러 작업자의 변경을 병합할 수 있는 Open Source Distributed Version Control System이다. Linux Kernel 개발을 지원하기 위해 2005년에 Linus Torvalds를 중심으로 처음 개발되었다.</p>

<p>Git의 주요 특징은 다음과 같다.</p>

<ul>
  <li>
    <p>대부분의 작업을 Local Repository에서 수행할 수 있다.</p>
  </li>
  <li>
    <p>Commit 단위로 변경 이력과 작성자 정보를 추적한다.</p>
  </li>
  <li>
    <p>Branch를 이용해 서로 다른 작업 흐름을 분리한다.</p>
  </li>
  <li>
    <p>Remote Repository를 통해 다른 작업자와 History를 교환한다.</p>
  </li>
  <li>
    <p>같은 기반에서 갈라진 변경을 Merge하거나 Rebase할 수 있다.</p>
  </li>
</ul>

<p>Git은 GitHub와 같은 Service의 이름이 아니다. Git은 Local에서 실행하는 Version Control 도구이고 GitHub, GitLab과 Bitbucket은 Git Repository Hosting과 협업 기능을 제공하는 Service이다.</p>

<h2 id="4--git-hosting-service">4 ) Git Hosting Service</h2>

<hr />

<table>
  <thead>
    <tr>
      <th>Service</th>
      <th>Git 이외의 주요 기능</th>
      <th>자체 설치 선택지</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GitHub</td>
      <td>Pull Request, Issues, Actions, Packages, Pages와 Project 기능</td>
      <td>GitHub Enterprise Server</td>
    </tr>
    <tr>
      <td>GitLab</td>
      <td>Merge Request, Issues, CI/CD, Package·Container Registry</td>
      <td>GitLab Self-Managed</td>
    </tr>
    <tr>
      <td>Bitbucket</td>
      <td>Pull Request, Pipeline과 Jira 등 Atlassian 제품 연동</td>
      <td>제공 형태와 Plan 확인 필요</td>
    </tr>
  </tbody>
</table>

<p>Hosting Service의 무료 사용자 수, Private Repository 범위, CI 실행 시간과 Storage 한도는 변경될 수 있다. 문서에 특정 수치를 고정하지 않고 사용할 시점의 공식 Plan과 조직 정책을 확인한다.</p>

<h3 id="remote-repository와-local-repository">Remote Repository와 Local Repository</h3>

<blockquote>
  <p><strong>Local Repository</strong></p>

  <p>현재 Computer의 <code class="language-plaintext highlighter-rouge">.git</code> Directory에 저장된 Git Object, Reference와 설정이다.</p>
</blockquote>

<blockquote>
  <p><strong>Remote Repository</strong></p>

  <p>다른 Repository와 History를 교환하기 위해 이름과 URL로 등록한 Repository이다.</p>
</blockquote>

<p>Remote Repository가 반드시 Cloud Service에 있어야 하는 것은 아니다. 접근 가능한 Server의 Bare Repository도 Remote로 사용할 수 있다. <code class="language-plaintext highlighter-rouge">origin</code>은 특별한 Protocol이 아니라 <code class="language-plaintext highlighter-rouge">git clone</code>이 기본적으로 등록하는 Remote 이름이다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote <span class="nt">-v</span>
</code></pre></div></div>

<h2 id="5--git-설치와-version-확인">5 ) Git 설치와 Version 확인</h2>

<hr />

<p>Git 공식 배포 경로나 운영체제의 Package Manager를 사용하여 설치한다. Linux 배포판에서는 일반적으로 <code class="language-plaintext highlighter-rouge">git</code> Package로 제공된다.</p>

<p>설치된 Git Version을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nt">--version</span>
</code></pre></div></div>

<p>GitHub 기능을 Terminal에서 사용해야 한다면 GitHub CLI인 <code class="language-plaintext highlighter-rouge">gh</code>를 별도로 설치할 수 있다. <code class="language-plaintext highlighter-rouge">gh</code>는 Git 자체를 대체하지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh <span class="nt">--version</span>
</code></pre></div></div>

<p>GUI Client가 필요하면 SourceTree 같은 도구를 사용할 수 있다. GUI에서 Stage, Commit과 Push를 실행해도 내부적으로 다루는 Git Repository와 기본 개념은 같다.</p>

<h2 id="6--사용자-정보-설정">6 ) 사용자 정보 설정</h2>

<hr />

<p>Git Commit에는 작성자 이름과 Email이 Metadata로 기록된다. 이 값은 Hosting Service Login 정보와 자동으로 같아지는 것이 아니므로 직접 설정을 확인한다.</p>

<p>모든 Local Repository의 기본값으로 사용할 이름과 Email을 설정한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--global</span> user.name <span class="s2">"&lt;name&gt;"</span>
git config <span class="nt">--global</span> user.email <span class="s2">"&lt;email&gt;"</span>
</code></pre></div></div>

<p>현재 Repository에서만 다른 값을 사용하려면 <code class="language-plaintext highlighter-rouge">--global</code>을 제외한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config user.name <span class="s2">"&lt;project-name&gt;"</span>
git config user.email <span class="s2">"&lt;project-email&gt;"</span>
</code></pre></div></div>

<p>설정 값을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config user.name
git config user.email
git config <span class="nt">--list</span> <span class="nt">--show-origin</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>범위</th>
      <th>Option</th>
      <th>일반적인 저장 위치</th>
      <th>우선순위</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>System</td>
      <td><code class="language-plaintext highlighter-rouge">--system</code></td>
      <td>System Git 설정 File</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>사용자</td>
      <td><code class="language-plaintext highlighter-rouge">--global</code></td>
      <td>사용자 Git 설정 File</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>현재 Repository</td>
      <td><code class="language-plaintext highlighter-rouge">--local</code> 또는 생략</td>
      <td><code class="language-plaintext highlighter-rouge">.git/config</code></td>
      <td>높음</td>
    </tr>
  </tbody>
</table>

<p>같은 Key가 여러 범위에 있으면 더 구체적인 Repository 설정이 사용자 기본값보다 우선한다.</p>

<h2 id="7--초기-branch-이름">7 ) 초기 Branch 이름</h2>

<hr />

<p>Git이 새 Repository에 사용할 초기 Branch 이름은 Git Version, 배포판 설정과 사용자 설정에 따라 달라질 수 있다. Hosting Service가 새 Remote Repository에 선택하는 기본 Branch 이름과 Local <code class="language-plaintext highlighter-rouge">git init</code>의 결과도 항상 같다고 가정할 수 없다.</p>

<p>새 Local Repository의 초기 Branch 이름을 <code class="language-plaintext highlighter-rouge">main</code>으로 통일하려면 다음과 같이 설정한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--global</span> init.defaultBranch main
</code></pre></div></div>

<p>특정 Repository를 만들 때 직접 지정할 수도 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git init <span class="nt">--initial-branch</span><span class="o">=</span>main
</code></pre></div></div>

<p>이미 Commit이 있는 Repository의 Branch 이름을 바꾸는 작업은 Remote Branch와 협업자에게 영향을 줄 수 있으므로 단순 초기 설정과 구분한다.</p>

<h2 id="8--git의-세-작업-영역">8 ) Git의 세 작업 영역</h2>

<hr />

<p>Git의 기본 흐름을 이해하려면 Working Tree, Staging Area와 Local Repository를 구분해야 한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Working Tree
    │ git add
    ▼
Staging Area(Index)
    │ git commit
    ▼
Local Repository(.git)
    │ git push
    ▼
Remote Repository
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>영역</th>
      <th>저장되는 내용</th>
      <th>주요 확인 명령</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Working Tree</td>
      <td>사용자가 현재 편집하는 File</td>
      <td><code class="language-plaintext highlighter-rouge">git status</code>, <code class="language-plaintext highlighter-rouge">git diff</code></td>
    </tr>
    <tr>
      <td>Staging Area</td>
      <td>다음 Commit에 넣기로 선택한 내용</td>
      <td><code class="language-plaintext highlighter-rouge">git diff --staged</code></td>
    </tr>
    <tr>
      <td>Local Repository</td>
      <td>Commit, Tree, Blob와 Reference 등 Git Object</td>
      <td><code class="language-plaintext highlighter-rouge">git log</code>, <code class="language-plaintext highlighter-rouge">git show</code></td>
    </tr>
    <tr>
      <td>Remote Repository</td>
      <td>다른 Repository와 교환한 Commit과 Branch Reference</td>
      <td><code class="language-plaintext highlighter-rouge">git remote -v</code>, <code class="language-plaintext highlighter-rouge">git branch -r</code></td>
    </tr>
  </tbody>
</table>

<h3 id="working-tree">Working Tree</h3>

<p>Working Tree는 특정 Commit에서 Checkout한 File을 실제로 편집하는 공간이다. 일반 Repository에서는 <code class="language-plaintext highlighter-rouge">.git</code> Directory를 제외한 Project File 영역으로 볼 수 있다.</p>

<h3 id="staging-area">Staging Area</h3>

<p>Staging Area는 다음 Commit에 포함할 내용을 선택하는 영역이며 Index라고도 한다. File을 단순히 다른 Directory로 이동시키는 것이 아니라 다음 Commit의 Snapshot에 포함할 내용을 Git Index에 기록한다.</p>

<h3 id="local-repository">Local Repository</h3>

<p><code class="language-plaintext highlighter-rouge">.git</code> Directory에는 Git Object Database, Branch와 Tag Reference, Repository 설정 등이 저장된다. <code class="language-plaintext highlighter-rouge">.git</code>을 삭제하면 Working File이 남아 있더라도 해당 Local Repository의 History와 설정을 잃을 수 있다.</p>

<h2 id="9--file-상태">9 ) File 상태</h2>

<hr />

<table>
  <thead>
    <tr>
      <th>상태</th>
      <th>의미</th>
      <th>다음에 주로 사용하는 명령</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Untracked</td>
      <td>Working Tree에는 있지만 현재 Commit이 추적하지 않는 File</td>
      <td><code class="language-plaintext highlighter-rouge">git add</code> 또는 <code class="language-plaintext highlighter-rouge">.gitignore</code> 등록</td>
    </tr>
    <tr>
      <td>Modified</td>
      <td>추적 중인 File이 Index 또는 현재 Commit과 달라진 상태</td>
      <td><code class="language-plaintext highlighter-rouge">git diff</code>, <code class="language-plaintext highlighter-rouge">git add</code></td>
    </tr>
    <tr>
      <td>Staged</td>
      <td>다음 Commit에 포함할 내용이 Index에 기록된 상태</td>
      <td><code class="language-plaintext highlighter-rouge">git diff --staged</code>, <code class="language-plaintext highlighter-rouge">git commit</code></td>
    </tr>
    <tr>
      <td>Committed</td>
      <td>내용이 Local Repository의 Commit에 기록된 상태</td>
      <td><code class="language-plaintext highlighter-rouge">git log</code>, <code class="language-plaintext highlighter-rouge">git show</code></td>
    </tr>
  </tbody>
</table>

<p>하나의 File이 항상 한 상태만 가지는 것은 아니다. File을 Stage한 후 다시 수정하면 같은 File에 Staged 변경과 아직 Stage하지 않은 변경이 동시에 존재할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git diff
git diff <span class="nt">--staged</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">git status</code>는 상태를 요약하고 <code class="language-plaintext highlighter-rouge">git diff</code>는 Working Tree와 Index의 차이, <code class="language-plaintext highlighter-rouge">git diff --staged</code>는 Index와 현재 Commit의 차이를 보여준다.</p>

<h3 id="gitignore"><code class="language-plaintext highlighter-rouge">.gitignore</code></h3>

<p><code class="language-plaintext highlighter-rouge">.gitignore</code>는 Git이 추적하지 않을 File과 Directory Pattern을 기록한다. Build 결과, Package 설치 Directory, 가상 환경과 Local Credential처럼 Repository에 포함할 필요가 없는 항목을 제외할 때 사용한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Python
.venv/
__pycache__/
*.pyc

# Node.js
node_modules/

# Java·Spring
.gradle/
build/
target/

# Local 환경과 Credential
.env
*.key
</code></pre></div></div>

<p>Project 구성원이 공통으로 제외할 Pattern은 Repository의 <code class="language-plaintext highlighter-rouge">.gitignore</code>에 기록하고 함께 Commit한다. 개인 Editor의 임시 File처럼 Project에 공유할 필요가 없는 Pattern은 Repository의 <code class="language-plaintext highlighter-rouge">.git/info/exclude</code>나 사용자 전역 Ignore File에서 관리할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">.gitignore</code>는 Untracked File에 적용된다. 이미 Commit했거나 Stage하여 Git이 추적 중인 File은 Pattern을 추가해도 자동으로 추적 대상에서 빠지지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git check-ignore <span class="nt">-v</span> &lt;file&gt;
git ls-files &lt;file&gt;
</code></pre></div></div>

<p>추적 중인 File을 Working Tree에는 남겨두고 이후 Commit 대상에서 제외하려면 먼저 변경 범위와 Repository 정책을 확인한 뒤 Index에서 제거한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">rm</span> <span class="nt">--cached</span> &lt;file&gt;
git status
git diff <span class="nt">--staged</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">git rm --cached</code> 결과는 다음 Commit에 반영되는 변경이다. Directory 전체나 넓은 Pattern에 실행하기 전에 <code class="language-plaintext highlighter-rouge">git status</code>와 <code class="language-plaintext highlighter-rouge">git diff --staged</code>로 제거 대상을 확인한다. 이미 History에 들어간 Password나 Token은 <code class="language-plaintext highlighter-rouge">.gitignore</code>로 제거되지 않으므로 Credential을 폐기하고 History 정리 필요 여부를 별도로 판단해야 한다.</p>

<h2 id="10--local-repository-기본-실습">10 ) Local Repository 기본 실습</h2>

<hr />

<h3 id="repository-생성">Repository 생성</h3>

<p>새 실습 Directory를 만들고 이동한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>git-basic-lab
<span class="nb">cd </span>git-basic-lab
git init <span class="nt">--initial-branch</span><span class="o">=</span>main
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.git</code> Directory와 현재 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
<span class="nb">ls</span> <span class="nt">-la</span>
</code></pre></div></div>

<h3 id="untracked-file-생성">Untracked File 생성</h3>

<p><code class="language-plaintext highlighter-rouge">README.md</code>를 작성한 뒤 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>README.md
git status
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">README.md</code>는 아직 Commit이 추적하지 않는 Untracked 상태이다.</p>

<h3 id="staging-area에-등록">Staging Area에 등록</h3>

<p>다음 Commit에 포함할 File을 선택한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add README.md
git status
git diff <span class="nt">--staged</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">git add</code>는 File을 Repository History에 즉시 기록하지 않는다. 현재 내용을 Staging Area에 올려 다음 Commit의 입력으로 만든다.</p>

<h3 id="첫-commit-생성">첫 Commit 생성</h3>

<p>Editor를 열어 Commit Message를 작성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit
</code></pre></div></div>

<p>짧은 실습에서는 <code class="language-plaintext highlighter-rouge">-m</code>으로 제목을 지정할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git commit <span class="nt">-m</span> <span class="s2">"docs: add project README"</span>
</code></pre></div></div>

<p>Commit과 Working Tree 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span> <span class="nt">--decorate</span>
git status
</code></pre></div></div>

<h3 id="수정-후-두-번째-commit">수정 후 두 번째 Commit</h3>

<p><code class="language-plaintext highlighter-rouge">README.md</code>를 수정한 뒤 Working Tree와 현재 Commit의 차이를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git status
git diff
</code></pre></div></div>

<p>변경 내용을 Stage하고 실제로 Commit될 차이를 다시 검토한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add README.md
git diff <span class="nt">--staged</span>
git commit <span class="nt">-m</span> <span class="s2">"docs: explain project purpose"</span>
</code></pre></div></div>

<p>History를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span>
</code></pre></div></div>

<h2 id="11--commit">11 ) Commit</h2>

<hr />

<p>Commit은 Staging Area에 선택한 Project 상태를 가리키는 Snapshot과 다음 Metadata를 Local Repository에 기록한다.</p>

<ul>
  <li>
    <p>작성자와 Committer 정보</p>
  </li>
  <li>
    <p>작성 시각</p>
  </li>
  <li>
    <p>Commit Message</p>
  </li>
  <li>
    <p>Project File과 Directory 구조를 가리키는 Tree</p>
  </li>
  <li>
    <p>이전 History를 연결하는 Parent Commit</p>
  </li>
</ul>

<p>Git은 File의 전체 복사본을 날짜별 Directory에 반복 저장하는 방식처럼 보이지 않는다. Content를 Object로 관리하고 같은 Content는 같은 Object를 재사용하며 Commit이 Project Snapshot을 가리키도록 구성한다.</p>

<h3 id="commit-message">Commit Message</h3>

<p>Commit Message는 변경한 결과뿐 아니라 변경 이유를 History에 남기는 정보이다. 여러 줄 Message는 다음 구조로 작성할 수 있다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>변경 내용을 요약한 제목

변경한 이유, 고려한 제약과 필요한 추가 설명
</code></pre></div></div>

<p>제목과 본문 사이를 빈 줄로 구분하면 <code class="language-plaintext highlighter-rouge">git log</code>, Hosting Service와 여러 Git 도구가 두 영역을 구분해서 표시할 수 있다.</p>

<h3 id="commit-object-id">Commit Object ID</h3>

<p>Commit은 Git Object ID로 식별된다. 일반적인 SHA-1 Repository에서는 40자리 16진수이고 SHA-256 형식의 Repository에서는 64자리 16진수이다. 화면에서는 식별에 필요한 앞부분만 축약하여 보여주는 경우가 많다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--oneline</span>
git rev-parse HEAD
</code></pre></div></div>

<p>짧은 ID는 현재 Repository에서 다른 Object와 구분될 때 편리하게 사용할 수 있지만 고정 길이의 전역 Identifier라고 가정하지 않는다.</p>

<h2 id="12--remote-repository와-push">12 ) Remote Repository와 Push</h2>

<hr />

<p>Local Commit은 <code class="language-plaintext highlighter-rouge">git commit</code>만으로 Remote에 전송되지 않는다. Remote Repository URL을 등록하고 Push해야 한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote add origin &lt;remote-repository-url&gt;
git remote <span class="nt">-v</span>
</code></pre></div></div>

<p>현재 <code class="language-plaintext highlighter-rouge">main</code> Branch를 <code class="language-plaintext highlighter-rouge">origin</code>에 Push하고 Upstream 관계를 설정한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push <span class="nt">-u</span> origin main
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Working Tree 변경
      │ git add
      ▼
Staging Area
      │ git commit
      ▼
Local main Branch
      │ git push
      ▼
Remote의 main Branch
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">git push</code>는 Working Tree File을 바로 Server로 복사하는 명령이 아니다. Local Repository의 Commit과 Reference를 Remote와 교환하여 Remote Branch를 갱신한다.</p>

<p>Push가 거절되면 강제 Push부터 실행하지 않는다. Remote에 Local이 가지고 있지 않은 Commit이 있는지 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch origin
git log <span class="nt">--oneline</span> <span class="nt">--graph</span> <span class="nt">--decorate</span> <span class="nt">--all</span>
</code></pre></div></div>

<h2 id="13--containerkubernetes-작업과-git">13 ) Container·Kubernetes 작업과 Git</h2>

<hr />

<p>Git Repository에는 Container Image Binary 자체보다 Image를 재현할 수 있는 입력을 저장한다.</p>

<table>
  <thead>
    <tr>
      <th>저장 대상</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Application Source</td>
      <td>Python, Java, Go와 JavaScript Source File</td>
    </tr>
    <tr>
      <td>Image Build 정의</td>
      <td><code class="language-plaintext highlighter-rouge">Dockerfile</code>, <code class="language-plaintext highlighter-rouge">.dockerignore</code></td>
    </tr>
    <tr>
      <td>Package 의존성</td>
      <td><code class="language-plaintext highlighter-rouge">requirements.txt</code>, <code class="language-plaintext highlighter-rouge">package.json</code>, Build 설정</td>
    </tr>
    <tr>
      <td>Kubernetes 설정</td>
      <td>Deployment, Service, ConfigMap Manifest</td>
    </tr>
    <tr>
      <td>배포 구성</td>
      <td>Kustomize Base·Overlay, Helm Chart와 Values</td>
    </tr>
    <tr>
      <td>자동화</td>
      <td>CI/CD Workflow와 Shell Script</td>
    </tr>
  </tbody>
</table>

<p>다음 정보는 Repository에 직접 Commit하지 않는다.</p>

<ul>
  <li>
    <p>Docker Hub Personal Access Token</p>
  </li>
  <li>
    <p>Private Key</p>
  </li>
  <li>
    <p>실제 Password가 들어 있는 Secret Manifest</p>
  </li>
  <li>
    <p>Cloud Access Key</p>
  </li>
  <li>
    <p>Local 개발 환경의 Credential File</p>
  </li>
</ul>

<p>Secret을 <code class="language-plaintext highlighter-rouge">.gitignore</code>에 넣는 것은 실수 방지 수단일 뿐 이미 Commit된 Secret을 History에서 자동으로 제거하지 않는다. Credential이 Commit되거나 외부에 노출됐다면 값을 삭제하는 것과 별개로 해당 Credential을 폐기하고 새로 발급해야 한다.</p>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>Version Control System은 File의 변경 이력을 기록하고 특정 Version의 비교, 복원과 협업을 지원한다.</p>
    </li>
    <li>
      <p>Git은 각 Clone이 Local Repository와 History를 가지는 Distributed Version Control System이다.</p>
    </li>
    <li>
      <p>Git과 GitHub·GitLab·Bitbucket 같은 Hosting Service는 서로 다른 대상이다.</p>
    </li>
    <li>
      <p>Git의 기본 작업 영역은 Working Tree, Staging Area와 Local Repository이다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">git add</code>는 다음 Commit에 포함할 내용을 선택하고 <code class="language-plaintext highlighter-rouge">git commit</code>은 이를 Local Repository에 기록한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">git push</code>는 Local Commit과 Branch Reference를 Remote Repository로 전송한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">.gitignore</code>는 의도적으로 추적하지 않을 File을 지정하며 이미 추적 중인 File에는 자동으로 적용되지 않는다.</p>
    </li>
    <li>
      <p>초기 Branch 이름과 Object ID 길이는 Version과 Repository 형식에 따라 달라질 수 있으므로 고정된 값으로 단정하지 않는다.</p>
    </li>
    <li>
      <p>Container와 Kubernetes 작업에서는 재현 가능한 설정을 Git에 저장하고 Token, Password와 Private Key는 Commit하지 않는다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Git" /><summary type="html"><![CDATA[Version Control System의 종류와 Git의 Working Tree·Staging Area·Repository 구조, File 상태 및 기본 Commit 흐름]]></summary></entry><entry><title type="html">Kubernetes Kustomize로 Manifest 구성 관리</title><link href="https://hyn128.site/cloud-native-39-kubernetes-kustomize/" rel="alternate" type="text/html" title="Kubernetes Kustomize로 Manifest 구성 관리" /><published>2026-09-08T00:00:00+09:00</published><updated>2026-09-08T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-39-kubernetes-kustomize</id><content type="html" xml:base="https://hyn128.site/cloud-native-39-kubernetes-kustomize/"><![CDATA[<p>Kubernetes Application은 Deployment 하나만으로 끝나지 않고 Service, Ingress, ConfigMap과 Secret 등 여러 Manifest로 구성된다. Kustomize는 원본 YAML을 Template 문법으로 바꾸지 않고 Resource를 조합하고 공통 설정과 환경별 차이를 적용하여 최종 Manifest를 만든다.</p>

<h2 id="1--kustomize">1 ) Kustomize</h2>

<hr />

<blockquote>
  <p><strong>Kustomize</strong></p>

  <p>Kubernetes Resource YAML을 Base로 유지하면서 Label, Image, Namespace와 Patch를 조합하여 환경별 Manifest를 생성하는 구성 관리 도구이다.</p>
</blockquote>

<p>Kustomize가 제공하는 주요 기능은 다음과 같다.</p>

<ul>
  <li>
    <p>여러 Manifest를 하나의 구성 단위로 조합한다.</p>
  </li>
  <li>
    <p>여러 Resource에 공통 Label, Annotation, Namespace와 이름 Prefix를 적용한다.</p>
  </li>
  <li>
    <p>Base를 재사용하고 개발·검증·운영 환경의 차이만 Overlay에 기록한다.</p>
  </li>
  <li>
    <p>Strategic Merge Patch와 JSON Patch로 필요한 Field만 변경한다.</p>
  </li>
  <li>
    <p>File이나 Literal을 이용하여 ConfigMap과 Secret Manifest를 생성한다.</p>
  </li>
</ul>

<p>Kustomize는 Cluster 안에서 실행되는 Controller가 아니다. Master 또는 kubeconfig가 설정된 관리 Client에서 Manifest를 Rendering하고, 결과를 API Server에 적용하는 Client 도구이다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Base Resources + Overlay + Generator
                 │
                 ▼
        Kustomize Rendering
                 │ 최종 Kubernetes Manifest
                 ▼
             API Server
                 │
                 ▼
Deployment·Service 등 각 Controller
                 │
                 ▼
Scheduler ──▶ Worker의 kubelet ──▶ Container Runtime
</code></pre></div></div>

<h2 id="2--kubectl-내장-기능과-독립-실행형-kustomize">2 ) kubectl 내장 기능과 독립 실행형 Kustomize</h2>

<hr />

<p>Kustomize는 <code class="language-plaintext highlighter-rouge">kubectl</code>에 내장된 기능과 별도 설치하는 독립 실행형 Binary로 사용할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Rendering</th>
      <th>적용</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>kubectl 내장</td>
      <td><code class="language-plaintext highlighter-rouge">kubectl kustomize &lt;directory&gt;</code></td>
      <td><code class="language-plaintext highlighter-rouge">kubectl apply -k &lt;directory&gt;</code></td>
      <td>별도 도구 없이 기본 기능 사용</td>
    </tr>
    <tr>
      <td>독립 실행형</td>
      <td><code class="language-plaintext highlighter-rouge">kustomize build &lt;directory&gt;</code></td>
      <td>Rendering 결과를 kubectl에 전달</td>
      <td>최신 Kustomize 기능과 편집 명령을 별도로 선택 가능</td>
    </tr>
  </tbody>
</table>

<p>두 도구에 포함된 Kustomize Version이 다르면 지원 Field나 Rendering 결과가 달라질 수 있다. 팀과 CI에서는 어느 도구와 Version을 기준으로 하는지 고정한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl version <span class="nt">--client</span>
kubectl kustomize <span class="nt">--help</span>
kustomize version
</code></pre></div></div>

<p>기본적인 Build와 배포만 필요하다면 kubectl의 <code class="language-plaintext highlighter-rouge">-k</code> 기능으로 시작할 수 있다. 독립 실행형의 특정 기능이나 Version 통일이 필요할 때 별도 Binary를 설치한다.</p>

<h2 id="3--asdf를-이용한-version-관리">3 ) asdf를 이용한 Version 관리</h2>

<hr />

<p>asdf는 여러 언어와 CLI 도구의 Version을 <code class="language-plaintext highlighter-rouge">.tool-versions</code> File로 관리하는 Version Manager이다. 현재 asdf 0.16 이상은 Go로 다시 작성된 Binary이며, 0.15 이하의 Shell Script 방식과 설치 및 명령 체계가 다르다.</p>

<h3 id="asdf-016-이상">asdf 0.16 이상</h3>

<p>asdf Binary를 운영체제와 Architecture에 맞게 설치하고 <code class="language-plaintext highlighter-rouge">$ASDF_DATA_DIR/shims</code>를 <code class="language-plaintext highlighter-rouge">PATH</code> 앞에 추가한다. 설치 방식은 배포판 Package Manager나 공식 Release 절차를 따른다.</p>

<p>Kustomize Plugin과 사용할 Version을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin add kustomize
asdf list all kustomize
asdf <span class="nb">install </span>kustomize &lt;kustomize-version&gt;
asdf <span class="nb">set</span> <span class="nt">--home</span> kustomize &lt;kustomize-version&gt;
kustomize version
</code></pre></div></div>

<p>Project Directory에만 Version을 고정하려면 해당 Directory에서 <code class="language-plaintext highlighter-rouge">--home</code> 없이 <code class="language-plaintext highlighter-rouge">asdf set kustomize &lt;version&gt;</code>을 실행한다. 생성된 <code class="language-plaintext highlighter-rouge">.tool-versions</code>를 함께 관리하면 작업자와 CI가 같은 Version을 선택할 수 있다.</p>

<h3 id="asdf-015-이하의-legacy-방식">asdf 0.15 이하의 Legacy 방식</h3>

<p>다음 방식은 asdf <code class="language-plaintext highlighter-rouge">v0.14.0</code>처럼 Bash로 구현된 Version에서 사용하던 설치 방식이다. 현재 설치 절차로 사용하지 않으며, 기존 환경을 이해하거나 Migration할 때만 참고한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/asdf-vm/asdf.git ~/.asdf <span class="se">\</span>
  <span class="nt">--branch</span> v0.14.0

<span class="nb">echo</span> <span class="s1">'. "$HOME/.asdf/asdf.sh"'</span> <span class="o">&gt;&gt;</span> ~/.bashrc
<span class="nb">source</span> ~/.bashrc

asdf plugin add kustomize
asdf <span class="nb">install </span>kustomize 5.3.0
asdf global kustomize 5.3.0
</code></pre></div></div>

<p>asdf 0.16 이상에서는 <code class="language-plaintext highlighter-rouge">asdf global</code>과 <code class="language-plaintext highlighter-rouge">asdf local</code>이 제거되고 <code class="language-plaintext highlighter-rouge">asdf set</code>으로 대체됐다. 기존 설치를 Migration할 때는 Shell RC File의 <code class="language-plaintext highlighter-rouge">. "$HOME/.asdf/asdf.sh"</code> 설정도 현재 Binary와 Shim 경로 설정으로 변경해야 한다.</p>

<h2 id="4--base-directory와-resource-작성">4 ) Base Directory와 Resource 작성</h2>

<hr />

<p>Echo Application의 공통 Resource를 다음 구조로 작성한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo/
└── base/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    └── kustomization.yaml
</code></pre></div></div>

<p>Directory를 생성하고 이동한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nb">echo</span>/base
<span class="nb">cd echo</span>/base
</code></pre></div></div>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">deployment.yaml</code>로 저장한다. nginx Container가 같은 Pod의 Echo Container에 <code class="language-plaintext highlighter-rouge">localhost:8080</code>으로 요청을 전달한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">echo</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">echo</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/jpubdocker/simple-nginx-proxy:v0.1.0</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">NGINX_PORT</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">80"</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SERVER_NAME</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">localhost</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">BACKEND_HOST</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">localhost:8080</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">BACKEND_MAX_FAILS</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">BACKEND_FAIL_TIMEOUT</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">10s</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
              <span class="na">containerPort</span><span class="pi">:</span> <span class="m">80</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/jpubdocker/echo:v0.1.0</span>
</code></pre></div></div>

<p>같은 Pod의 Container는 Network Namespace를 공유하므로 nginx는 Echo Container를 <code class="language-plaintext highlighter-rouge">localhost:8080</code>으로 호출할 수 있다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">service.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">echo</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="s">http</span>
      <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
</code></pre></div></div>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">ingress.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ingressClassName</span><span class="pi">:</span> <span class="s">nginx</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">echo.jpub.local</span>
      <span class="na">http</span><span class="pi">:</span>
        <span class="na">paths</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
            <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
            <span class="na">backend</span><span class="pi">:</span>
              <span class="na">service</span><span class="pi">:</span>
                <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
                <span class="na">port</span><span class="pi">:</span>
                  <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>Ingress Resource가 실제 Traffic을 처리하려면 <code class="language-plaintext highlighter-rouge">nginx</code> IngressClass를 담당하는 Controller가 필요하다. ingress-nginx Controller의 현재 상태와 실습 조건은 <a href="/cloud-native-33-kubernetes-ingress-routing/">Kubernetes Ingress Resource와 HTTP Routing</a>에서 확인할 수 있다.</p>

<h2 id="5--kustomizationyaml로-resource-조합">5 ) kustomization.yaml로 Resource 조합</h2>

<hr />

<p>독립 실행형 Kustomize는 현재 Directory의 Resource를 검색하여 <code class="language-plaintext highlighter-rouge">kustomization.yaml</code>을 만들 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kustomize create <span class="nt">--autodetect</span>
</code></pre></div></div>

<p>직접 작성한다면 다음과 같다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kustomize.config.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Kustomization</span>
<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">deployment.yaml</span>
  <span class="pi">-</span> <span class="s">service.yaml</span>
  <span class="pi">-</span> <span class="s">ingress.yaml</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">resources</code>의 경로는 <code class="language-plaintext highlighter-rouge">kustomization.yaml</code>을 기준으로 해석된다. 조합 결과를 화면에 출력한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kustomize build <span class="nb">.</span>
</code></pre></div></div>

<p>kubectl 내장 기능으로 같은 구성을 Rendering할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl kustomize <span class="nb">.</span>
</code></pre></div></div>

<p>Rendering 결과를 File로 저장하지 않아도 Directory를 직접 적용하고 삭제할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl diff <span class="nt">-k</span> <span class="nb">.</span>
kubectl apply <span class="nt">-k</span> <span class="nb">.</span>
kubectl delete <span class="nt">-k</span> <span class="nb">.</span>
</code></pre></div></div>

<p>독립 실행형 Kustomize 결과를 Pipe로 전달할 때는 kubectl이 표준 입력의 Manifest를 읽도록 <code class="language-plaintext highlighter-rouge">-f -</code>를 사용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kustomize build <span class="nb">.</span> | kubectl apply <span class="nt">-f</span> -
kustomize build <span class="nb">.</span> | kubectl delete <span class="nt">-f</span> -
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">kustomize build . | kubectl apply -k .</code>처럼 사용하면 앞 명령의 표준 출력은 적용 대상이 되지 않는다. <code class="language-plaintext highlighter-rouge">-k</code>는 표준 입력이 아니라 지정한 Kustomization Directory를 읽는 Option이다.</p>

<h2 id="6--공통-label-적용">6 ) 공통 Label 적용</h2>

<hr />

<p>여러 Resource에 같은 Label을 반복하지 않고 <code class="language-plaintext highlighter-rouge">labels</code> Transformer로 추가할 수 있다. <code class="language-plaintext highlighter-rouge">kustomization.yaml</code>에 다음 내용을 추가한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">labels</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">pairs</span><span class="pi">:</span>
      <span class="na">app.kubernetes.io/part-of</span><span class="pi">:</span> <span class="s">echo-system</span>
    <span class="na">includeSelectors</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">includeSelectors: false</code>는 Resource의 Metadata와 Pod Template 등에 Label을 추가하되 기존 Selector를 바꾸지 않는다. Application을 식별하는 <code class="language-plaintext highlighter-rouge">app.kubernetes.io/name: echo</code>는 Deployment Selector, Pod Label과 Service Selector의 연결 조건이므로 Base Manifest에 명시적으로 유지한다.</p>

<p>독립 실행형 Kustomize의 편집 명령으로도 Label을 설정할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kustomize edit <span class="nb">set </span>label <span class="se">\</span>
  <span class="s1">'app.kubernetes.io/part-of:echo-system'</span>
</code></pre></div></div>

<p>편집 명령이 생성하는 Field 형태는 Kustomize Version에 따라 달라질 수 있으므로 변경된 <code class="language-plaintext highlighter-rouge">kustomization.yaml</code>과 Build 결과를 함께 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sed</span> <span class="nt">-n</span> <span class="s1">'1,160p'</span> kustomization.yaml
kustomize build <span class="nb">.</span>
</code></pre></div></div>

<p>Deployment의 <code class="language-plaintext highlighter-rouge">spec.selector</code>는 필수이며 <code class="language-plaintext highlighter-rouge">spec.template.metadata.labels</code>와 일치해야 한다. 공통 Label 기능을 사용한다는 이유로 Base Deployment의 필수 Selector를 제거하지 않는다.</p>

<h2 id="7--base와-overlay">7 ) Base와 Overlay</h2>

<hr />

<p>Base에는 환경에 공통인 Resource를 두고 Overlay에는 개발·검증·운영 환경별 차이만 둔다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   └── kustomization.yaml
└── overlays/
    └── dev/
        ├── kustomization.yaml
        ├── patch-deployment.yaml
        └── patch-ingress.yaml
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">echo/base</code>에서 상위 Directory로 이동한 뒤 개발 Overlay를 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ..
<span class="nb">mkdir</span> <span class="nt">-p</span> overlays/dev
<span class="nb">cd </span>overlays/dev
kustomize create <span class="nt">--resources</span> ../../base
</code></pre></div></div>

<p>생성되는 <code class="language-plaintext highlighter-rouge">overlays/dev/kustomization.yaml</code>의 기본 형태는 다음과 같다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kustomize.config.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Kustomization</span>
<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">../../base</span>
</code></pre></div></div>

<p>Base Directory 자체도 <code class="language-plaintext highlighter-rouge">kustomization.yaml</code>을 가지므로 Overlay의 <code class="language-plaintext highlighter-rouge">resources</code>에서 하나의 구성 단위로 참조할 수 있다. Version Control에는 <code class="language-plaintext highlighter-rouge">kustomize build</code> 결과보다 Base, Overlay와 Patch 원본을 저장한다.</p>

<h2 id="8--strategic-merge-방식의-patch">8 ) Strategic Merge 방식의 Patch</h2>

<hr />

<p>개발 환경에서 Replica를 2개로 늘리고 nginx의 실패 허용 횟수를 변경한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">patch-deployment.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">BACKEND_MAX_FAILS</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">5"</span>
</code></pre></div></div>

<p>전체 Deployment를 복사하지 않고 변경할 Field와 병합 기준이 되는 Resource·Container 이름만 작성한다. <code class="language-plaintext highlighter-rouge">overlays/dev/kustomization.yaml</code>에 Patch를 추가한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">patches</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">patch-deployment.yaml</span>
    <span class="na">target</span><span class="pi">:</span>
      <span class="na">group</span><span class="pi">:</span> <span class="s">apps</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
      <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">patchesStrategicMerge</code>는 기존 구성에서 볼 수 있는 Legacy Field이다. 현재 문서에서는 Strategic Merge Patch와 JSON Patch를 모두 통합해서 표현할 수 있는 <code class="language-plaintext highlighter-rouge">patches</code> Field를 사용한다.</p>

<h2 id="9--json-patch">9 ) JSON Patch</h2>

<hr />

<p>Ingress Host처럼 배열 안의 특정 Field를 경로로 지정할 때 JSON Patch를 사용할 수 있다. 다음 내용을 <code class="language-plaintext highlighter-rouge">patch-ingress.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">op</span><span class="pi">:</span> <span class="s">replace</span>
  <span class="na">path</span><span class="pi">:</span> <span class="s">/spec/rules/0/host</span>
  <span class="na">value</span><span class="pi">:</span> <span class="s">dev-echo.jpub.local</span>
</code></pre></div></div>

<p>JSON Pointer의 <code class="language-plaintext highlighter-rouge">/spec/rules/0/host</code>는 첫 번째 Rule의 <code class="language-plaintext highlighter-rouge">host</code> Field를 의미한다. 마지막에 <code class="language-plaintext highlighter-rouge">/</code>를 추가하면 다른 경로로 해석되므로 붙이지 않는다.</p>

<p><code class="language-plaintext highlighter-rouge">overlays/dev/kustomization.yaml</code>의 <code class="language-plaintext highlighter-rouge">patches</code>에 다음 항목을 추가한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">patches</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">patch-deployment.yaml</span>
    <span class="na">target</span><span class="pi">:</span>
      <span class="na">group</span><span class="pi">:</span> <span class="s">apps</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
      <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
  <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">patch-ingress.yaml</span>
    <span class="na">target</span><span class="pi">:</span>
      <span class="na">group</span><span class="pi">:</span> <span class="s">networking.k8s.io</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
      <span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">echo</span>
</code></pre></div></div>

<p>개발 Overlay의 최종 결과와 변경 사항을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kustomize build <span class="nb">.</span>
kubectl diff <span class="nt">-k</span> <span class="nb">.</span>
</code></pre></div></div>

<p>Build 결과에서 Deployment의 Replica가 <code class="language-plaintext highlighter-rouge">2</code>, <code class="language-plaintext highlighter-rouge">BACKEND_MAX_FAILS</code>가 <code class="language-plaintext highlighter-rouge">5</code>, Ingress Host가 <code class="language-plaintext highlighter-rouge">dev-echo.jpub.local</code>인지 확인한다.</p>

<h2 id="10--secret-generator">10 ) Secret Generator</h2>

<hr />

<p>Kustomize의 <code class="language-plaintext highlighter-rouge">secretGenerator</code>는 File, Env File 또는 Literal을 읽어 Secret Manifest를 생성한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">secret.env</code>로 저장한다.</p>

<pre><code class="language-dotenv">API_USERNAME=echo-user
API_PASSWORD=REPLACE_WITH_SECRET
</code></pre>

<p>실제 Secret 값이 있는 File은 Version Control 대상에서 제외한다.</p>

<pre><code class="language-gitignore">secret.env
</code></pre>

<p><code class="language-plaintext highlighter-rouge">overlays/dev/kustomization.yaml</code>에 Generator를 추가한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">secretGenerator</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">echo-secret</span>
    <span class="na">envs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">secret.env</span>
</code></pre></div></div>

<p>생성 결과를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kustomize build <span class="nb">.</span>
</code></pre></div></div>

<p>생성된 이름에는 <code class="language-plaintext highlighter-rouge">echo-secret-&lt;content-hash&gt;</code> 형태의 Suffix가 붙는다. Secret을 참조하는 Workload가 같은 Kustomization 안에 있으면 Kustomize가 알려진 참조 Field의 이름도 함께 변환한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">envFrom</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">secretRef</span><span class="pi">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">echo-secret</span>
</code></pre></div></div>

<p>Hash Suffix는 Secret 내용이 바뀌었을 때 Pod Template의 참조 이름도 바뀌게 하여 새 Rollout을 유도한다. <code class="language-plaintext highlighter-rouge">generatorOptions.disableNameSuffixHash: true</code>로 끌 수 있지만 자동 갱신 동작도 사라지므로 이유 없이 비활성화하지 않는다.</p>

<p>Secret을 Git에서 제외해도 보안이 완성되는 것은 아니다. <code class="language-plaintext highlighter-rouge">kustomize build</code> 결과에는 Base64로 표현된 값이 포함되며 Base64는 암호화가 아니다. Terminal 출력, CI Log, 임시 File과 Cluster 접근 권한을 함께 관리해야 한다.</p>

<h2 id="11--remote-resource">11 ) Remote Resource</h2>

<hr />

<p>공개된 Git Repository나 URL의 Kustomization을 <code class="language-plaintext highlighter-rouge">resources</code>에서 참조할 수 있다. Remote Resource는 재사용에 편리하지만 외부 Network와 공급자의 변경에 의존한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">https://example.com/application/kustomization.yaml</span>
</code></pre></div></div>

<p>운영 구성에서는 움직이는 Branch나 변경 가능한 URL보다 검증한 Commit이나 Release Tag를 고정하고, 적용 전 Build 결과와 Resource 권한을 검토한다.</p>

<p>ingress-nginx <code class="language-plaintext highlighter-rouge">controller-v1.8.1</code>의 Remote Kustomization은 Kustomize의 Network Resource 예제로 사용된 적이 있지만 현재 설치 대상으로 사용하지 않는다. ingress-nginx는 2026년 3월 24일 이후 신규 Release와 보안 수정이 없는 Retirement 상태이다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.1/deploy/static/provider/cloud/kustomization.yaml
</code></pre></div></div>

<p>위 명령은 과거 Kustomization File의 내용을 확인할 뿐 Cluster에 배포하지 않는다. 신규 환경에서는 유지보수 중인 Controller나 Gateway API 구현을 선택한다.</p>

<h2 id="12--개발-overlay-적용과-확인">12 ) 개발 Overlay 적용과 확인</h2>

<hr />

<p>Master 또는 관리 Client의 <code class="language-plaintext highlighter-rouge">echo/overlays/dev</code>에서 최종 Manifest를 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl kustomize <span class="nb">.</span>
kubectl diff <span class="nt">-k</span> <span class="nb">.</span>
kubectl apply <span class="nt">-k</span> <span class="nb">.</span>
</code></pre></div></div>

<p>Control Plane은 Rendering된 결과만 받으며 Base와 Overlay Directory 구조를 알지 못한다. API Server에 저장된 Deployment와 Service를 각 Controller가 조정하고, Scheduler가 선택한 Worker에서 kubelet이 Pod를 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl rollout status deployment/echo
kubectl get deployment,service,ingress
kubectl get pods <span class="se">\</span>
  <span class="nt">-l</span> app.kubernetes.io/name<span class="o">=</span><span class="nb">echo</span> <span class="se">\</span>
  <span class="nt">-o</span> wide
kubectl get endpointslice <span class="se">\</span>
  <span class="nt">-l</span> kubernetes.io/service-name<span class="o">=</span><span class="nb">echo</span>
</code></pre></div></div>

<p>문제가 있으면 적용된 Resource만 보지 말고 Rendering 결과가 예상한 Selector, Image, Host와 Secret 이름을 포함하는지 먼저 확인한다.</p>

<h2 id="13--실습-resource-정리">13 ) 실습 Resource 정리</h2>

<hr />

<p>적용에 사용한 같은 Overlay Directory에서 Resource를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-k</span> <span class="nb">echo</span>/overlays/dev
</code></pre></div></div>

<p>현재 Directory가 <code class="language-plaintext highlighter-rouge">echo/overlays/dev</code>라면 다음과 같이 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-k</span> <span class="nb">.</span>
</code></pre></div></div>

<p>Secret Generator가 만든 Secret도 같은 Kustomization의 Resource이므로 함께 삭제된다.</p>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>Kustomize는 Base Resource에 공통 설정과 환경별 Patch를 적용하여 최종 Kubernetes Manifest를 만든다.</p>
    </li>
    <li>
      <p>kubectl 내장 기능은 <code class="language-plaintext highlighter-rouge">kubectl kustomize</code>와 <code class="language-plaintext highlighter-rouge">kubectl apply -k</code>로 사용하고 독립 실행형은 <code class="language-plaintext highlighter-rouge">kustomize build</code>로 Rendering한다.</p>
    </li>
    <li>
      <p>Base에는 공통 구성을, Overlay에는 환경별 차이를 두며 Build 결과보다 Base와 Patch 원본을 Version Control에서 관리한다.</p>
    </li>
    <li>
      <p>Deployment Selector와 Pod Label, Service Selector의 연결은 Kustomize를 사용해도 유지해야 한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">patches</code>에서 Strategic Merge Patch와 JSON Patch를 사용하여 필요한 Field만 변경할 수 있다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">secretGenerator</code>는 Secret 이름에 Content Hash를 추가하지만 Secret 값을 암호화하지는 않는다.</p>
    </li>
    <li>
      <p>Control Plane은 Rendering된 Kubernetes Resource만 처리하고 Worker의 kubelet이 최종 Pod Spec을 실행한다.</p>
    </li>
    <li>
      <p>다음 글인 <a href="/cloud-native-40-kubernetes-helm/">Kubernetes Helm Chart와 Release 관리</a>에서는 여러 Resource를 Version이 있는 Package로 묶고 설치·Upgrade·Rollback하는 방법을 다룬다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Kubernetes" /><summary type="html"><![CDATA[Kustomize의 Resource 조합, 공통 Label, Base·Overlay, Patch와 Secret Generator를 이용한 환경별 Manifest 관리 정리]]></summary></entry><entry><title type="html">Kubernetes Helm Chart와 Release 관리</title><link href="https://hyn128.site/cloud-native-40-kubernetes-helm/" rel="alternate" type="text/html" title="Kubernetes Helm Chart와 Release 관리" /><published>2026-09-08T00:00:00+09:00</published><updated>2026-09-08T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-40-kubernetes-helm</id><content type="html" xml:base="https://hyn128.site/cloud-native-40-kubernetes-helm/"><![CDATA[<p><a href="/cloud-native-39-kubernetes-kustomize/">Kubernetes Kustomize로 Manifest 구성 관리</a>에서 다룬 Kustomize는 원본 Manifest를 조합하고 환경별 차이를 Overlay로 관리한다. Helm은 여러 Kubernetes Resource와 기본 설정을 Version이 있는 Chart로 묶고, Chart를 Cluster에 설치한 결과를 Release 단위로 관리한다.</p>

<h2 id="1--helm">1 ) Helm</h2>

<hr />

<blockquote>
  <p><strong>Helm</strong></p>

  <p>Kubernetes Application을 Chart로 Packaging하고 설치, Upgrade, Rollback과 제거를 Release 단위로 관리하는 CNCF Project이다.</p>
</blockquote>

<p>Helm의 주요 구성 요소는 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Helm Client</td>
      <td>Chart를 읽고 Template을 Rendering하여 Kubernetes API Server에 적용</td>
    </tr>
    <tr>
      <td>Chart</td>
      <td>Kubernetes Resource Template, 기본값과 Metadata를 묶은 Package</td>
    </tr>
    <tr>
      <td>Values</td>
      <td>같은 Chart에서 변경할 Image, Replica, Port와 Storage 등의 입력값</td>
    </tr>
    <tr>
      <td>Manifest</td>
      <td>Chart와 Values를 Rendering한 최종 Kubernetes YAML</td>
    </tr>
    <tr>
      <td>Release</td>
      <td>특정 Namespace에 설치한 Chart Instance와 Revision History</td>
    </tr>
    <tr>
      <td>Repository·OCI Registry</td>
      <td>Version별 Chart Package를 배포하는 저장소</td>
    </tr>
  </tbody>
</table>

<p>관계는 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Chart Templates + Default values.yaml + 사용자 Values
                         │
                         ▼
                   Helm Rendering
                         │
                         ▼
             Kubernetes Manifest 집합
                         │ API Server에 적용
                         ▼
                    Helm Release
                         │ Revision 기록
                         ▼
Deployment·StatefulSet·Service 등의 Controller
                         │
                         ▼
              Worker의 kubelet과 Pod
</code></pre></div></div>

<p>Helm은 Application Container를 직접 실행하지 않는다. Helm이 Rendering한 Resource를 API Server에 저장하면 Control Plane의 각 Controller가 원하는 상태를 조정하고 Worker의 kubelet이 실제 Container를 실행한다.</p>

<h2 id="2--helm-version과-kubernetes-호환성">2 ) Helm Version과 Kubernetes 호환성</h2>

<hr />

<p>2026년 9월 기준 Helm의 현재 Stable Major Version은 Helm 4이다. Helm 3은 지원 종료 단계에 있으므로 신규 환경에서는 Helm 4와 Cluster의 호환 범위를 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm version
kubectl version
</code></pre></div></div>

<p>Helm은 내부에 포함된 Kubernetes Client Library를 통해 API Server와 통신한다. Cluster보다 지나치게 오래된 Helm이나 Helm이 보장하지 않는 더 새로운 Kubernetes Cluster를 조합하면 API 호환 문제가 생길 수 있다.</p>

<p>Helm <code class="language-plaintext highlighter-rouge">3.13.3</code>을 고정하는 다음 명령은 당시 환경을 재현하기 위한 Legacy 예제이다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin add helm
asdf <span class="nb">install </span>helm 3.13.3
asdf global helm 3.13.3
</code></pre></div></div>

<p>asdf 0.16 이상에서는 <code class="language-plaintext highlighter-rouge">asdf global</code> 대신 <code class="language-plaintext highlighter-rouge">asdf set</code>을 사용한다. 실제 Version은 Cluster와 Helm의 공식 Version Skew를 확인한 뒤 선택한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asdf plugin add helm
asdf list all helm
asdf <span class="nb">install </span>helm &lt;compatible-helm-version&gt;
asdf <span class="nb">set</span> <span class="nt">--home</span> helm &lt;compatible-helm-version&gt;
helm version
</code></pre></div></div>

<h2 id="3--debianubuntu에서-helm-설치">3 ) Debian·Ubuntu에서 Helm 설치</h2>

<hr />

<p>Helm의 Debian·Ubuntu Package Repository를 이용하려면 먼저 필요한 Package를 설치한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>curl gpg apt-transport-https <span class="nt">--yes</span>
</code></pre></div></div>

<p>Repository Signing Key를 내려받아 Fingerprint를 확인하고 Keyring으로 변환한다. Fingerprint 값은 실행 전에 Helm 공식 설치 문서의 현재 값과 다시 대조한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">HELM_APT_KEY_ID</span><span class="o">=</span><span class="s1">'DDF78C3E6EBB2D2CC223C95C62BA89D07698DBC6'</span>

curl <span class="nt">-fsSL</span> <span class="se">\</span>
  https://packages.buildkite.com/helm-linux/helm-debian/gpgkey <span class="se">\</span>
  <span class="nt">-o</span> /tmp/helm.gpg

<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="si">$(</span>gpg <span class="nt">--show-keys</span> <span class="nt">--with-colons</span> /tmp/helm.gpg | <span class="nb">awk</span> <span class="nt">-F</span>: <span class="s1">'$1 == "fpr" {print $10}'</span> | <span class="nb">head</span> <span class="nt">-n</span> 1<span class="si">)</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">HELM_APT_KEY_ID</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s1">'ERROR: unexpected Helm APT signing key'</span>
  <span class="nb">exit </span>1
<span class="k">fi</span>
</code></pre></div></div>

<p>확인한 Key를 등록하고 Repository를 추가한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpg <span class="nt">--dearmor</span> &lt; /tmp/helm.gpg | <span class="se">\</span>
  <span class="nb">sudo tee</span> /usr/share/keyrings/helm.gpg <span class="o">&gt;</span> /dev/null

<span class="nb">echo</span> <span class="s1">'deb [signed-by=/usr/share/keyrings/helm.gpg] https://packages.buildkite.com/helm-linux/helm-debian/any/ any main'</span> | <span class="se">\</span>
  <span class="nb">sudo tee</span> /etc/apt/sources.list.d/helm-stable-debian.list

<span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install </span>helm
helm version
</code></pre></div></div>

<p>Package Repository에서 최신 Major Version이 설치될 수 있으므로 운영 Cluster에 적용하기 전에 <code class="language-plaintext highlighter-rouge">helm version</code>과 Kubernetes Version 지원 범위를 확인한다.</p>

<h2 id="4--chart-repository와-oci-registry">4 ) Chart, Repository와 OCI Registry</h2>

<hr />

<p>전통적인 Helm Repository는 <code class="language-plaintext highlighter-rouge">index.yaml</code>과 Chart Package를 HTTP Server에서 제공한다. OCI Registry는 Container Image와 유사한 방식으로 Chart Artifact를 저장한다.</p>

<table>
  <thead>
    <tr>
      <th>배포 위치</th>
      <th>검색·사용 방식</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Helm Repository</td>
      <td><code class="language-plaintext highlighter-rouge">helm repo add</code>, <code class="language-plaintext highlighter-rouge">helm repo update</code>, <code class="language-plaintext highlighter-rouge">helm search repo</code></td>
    </tr>
    <tr>
      <td>Artifact Hub</td>
      <td>Browser 또는 <code class="language-plaintext highlighter-rouge">helm search hub</code>로 여러 공급자의 Chart 검색</td>
    </tr>
    <tr>
      <td>OCI Registry</td>
      <td><code class="language-plaintext highlighter-rouge">oci://&lt;registry&gt;/&lt;path&gt;/&lt;chart&gt;</code>를 직접 참조</td>
    </tr>
  </tbody>
</table>

<p>등록된 Repository를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo list
</code></pre></div></div>

<p>Bitnami Repository를 등록하고 Index를 갱신한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/nginx
helm search repo bitnami/mariadb <span class="nt">--versions</span>
</code></pre></div></div>

<p>Artifact Hub 전체에서 Chart를 찾을 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm search hub nginx
helm search hub mariadb
</code></pre></div></div>

<p>과거 Helm의 기본 Chart 모음이었던 <code class="language-plaintext highlighter-rouge">stable</code>과 <code class="language-plaintext highlighter-rouge">incubator</code> Repository는 2020년 11월부터 읽기 전용 Archive이며 신규 Chart 선택지로 사용하지 않는다. 현재 Chart는 Artifact Hub에서 공급자, 최근 Release, Source Repository와 보안 정보를 확인한 뒤 선택한다.</p>

<h3 id="oci-registry에-사용자-chart-배포">OCI Registry에 사용자 Chart 배포</h3>

<p>OCI Registry는 Container Image뿐 아니라 Helm Chart Package도 저장할 수 있다. Chart Directory를 직접 Push하지 않고 <code class="language-plaintext highlighter-rouge">helm package</code>로 만든 <code class="language-plaintext highlighter-rouge">.tgz</code> File을 업로드한다.</p>

<p>Registry의 Image 참조 구조, Public·Private Registry와 인증 방식은 <a href="/cloud-native-41-container-image-registry/">Container Image Registry와 Kubernetes Image Pull</a>에서 다룬다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Chart Directory
      │ helm lint
      ▼
검증된 Chart
      │ helm package
      ▼
Chart Archive(.tgz)
      │ helm push
      ▼
OCI Registry
      │ helm pull·helm install
      ▼
Helm Client
</code></pre></div></div>

<p>샘플 Chart를 생성한다. 기존 <code class="language-plaintext highlighter-rouge">echo</code> Directory가 있다면 덮어쓰지 말고 내용을 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm create <span class="nb">echo
</span>helm lint ./echo
helm template echo-preview ./echo
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Chart.yaml</code>의 <code class="language-plaintext highlighter-rouge">name</code>과 <code class="language-plaintext highlighter-rouge">version</code>을 확인한 뒤 Package를 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm show chart ./echo
helm package ./echo
</code></pre></div></div>

<p>기본 생성값을 사용했다면 현재 Directory에 <code class="language-plaintext highlighter-rouge">echo-0.1.0.tgz</code>가 만들어진다. 실제 File 이름은 <code class="language-plaintext highlighter-rouge">Chart.yaml</code>의 Version에 따라 달라진다.</p>

<p>Docker Hub 같은 OCI Registry에 로그인한다. Password 또는 Personal Access Token은 명령 인자에 작성하지 않고 Prompt에 입력한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm registry login registry-1.docker.io <span class="se">\</span>
  <span class="nt">-u</span> &lt;registry-username&gt;
</code></pre></div></div>

<p>Package를 Registry Namespace로 Push한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm push echo-0.1.0.tgz <span class="se">\</span>
  oci://registry-1.docker.io/&lt;registry-username&gt;
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">helm push</code>의 대상 경로에는 Chart 이름과 Tag를 붙이지 않는다. Helm이 <code class="language-plaintext highlighter-rouge">Chart.yaml</code>의 <code class="language-plaintext highlighter-rouge">name</code>을 OCI Repository 이름으로, <code class="language-plaintext highlighter-rouge">version</code>을 Tag로 사용하므로 위 명령의 결과 참조는 다음 형태가 된다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>registry-1.docker.io/&lt;registry-username&gt;/echo:0.1.0
</code></pre></div></div>

<p>OCI Chart 참조는 <code class="language-plaintext highlighter-rouge">oci://</code> Prefix를 사용하며 <code class="language-plaintext highlighter-rouge">helm show</code>, <code class="language-plaintext highlighter-rouge">helm pull</code>과 <code class="language-plaintext highlighter-rouge">helm install</code>에서 Chart Version을 명시할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm show chart <span class="se">\</span>
  oci://registry-1.docker.io/&lt;registry-username&gt;/echo <span class="se">\</span>
  <span class="nt">--version</span> 0.1.0

helm pull <span class="se">\</span>
  oci://registry-1.docker.io/&lt;registry-username&gt;/echo <span class="se">\</span>
  <span class="nt">--version</span> 0.1.0
</code></pre></div></div>

<p>Cluster에 설치하기 전 Rendering 결과를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm template echo-oci-preview <span class="se">\</span>
  oci://registry-1.docker.io/&lt;registry-username&gt;/echo <span class="se">\</span>
  <span class="nt">--version</span> 0.1.0
</code></pre></div></div>

<p>검증이 끝난 Chart를 별도 Namespace에 설치하고 Helm Release와 Kubernetes Resource를 함께 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">install </span>echo-oci <span class="se">\</span>
  oci://registry-1.docker.io/&lt;registry-username&gt;/echo <span class="se">\</span>
  <span class="nt">--version</span> 0.1.0 <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--create-namespace</span>

helm status echo-oci <span class="nt">--namespace</span> helm-lab
kubectl get pods,services <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p>Chart를 Registry에 Push하는 과정은 Cluster를 변경하지 않는다. <code class="language-plaintext highlighter-rouge">helm install</code>을 실행해야 Helm Client가 Chart를 Pull하고 Manifest를 Rendering하여 API Server에 제출한다. 그 이후에는 Controller와 Worker의 kubelet이 Resource를 조정한다.</p>

<p>설치한 Chart가 Service를 생성했다면 외부 공개 설정을 추가하기 전에 Port Forwarding으로 응답을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl port-forward <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  service/echo-oci 8080:80
</code></pre></div></div>

<p>Chart의 실제 Service 이름과 Port는 다음 명령으로 확인한 뒤 예제 값을 바꾼다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get service <span class="nt">--namespace</span> helm-lab
kubectl describe service echo-oci <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p>Pod가 시작되지 않으면 Release 상태만 보지 않고 Event와 Container Log를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get events <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--sort-by</span><span class="o">=</span>.metadata.creationTimestamp
kubectl logs <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  deployment/echo-oci <span class="se">\</span>
  <span class="nt">--all-pods</span><span class="o">=</span><span class="nb">true</span>
</code></pre></div></div>

<p>Resource 이름과 Workload Kind는 Chart Template에 따라 다를 수 있다. <code class="language-plaintext highlighter-rouge">helm get manifest echo-oci --namespace helm-lab</code>으로 실제 생성된 Resource를 확인하고 Log 명령의 대상을 선택한다.</p>

<p>실습이 끝나면 Release와 Registry Login Session을 정리한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm uninstall echo-oci <span class="nt">--namespace</span> helm-lab
helm registry <span class="nb">logout </span>registry-1.docker.io
</code></pre></div></div>

<h2 id="5--chart-설치-전-확인">5 ) Chart 설치 전 확인</h2>

<hr />

<p>외부 Chart를 바로 설치하지 않고 Metadata, 기본 Values와 Rendering 결과를 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm show chart bitnami/nginx
helm show values bitnami/nginx
helm template my-nginx bitnami/nginx
helm <span class="nb">install </span>my-nginx bitnami/nginx <span class="nt">--dry-run</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>명령</th>
      <th>확인 대상</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">helm show chart</code></td>
      <td>Chart 이름, Version, Dependency와 Metadata</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">helm show values</code></td>
      <td>변경 가능한 기본 설정</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">helm template</code></td>
      <td>Cluster에 연결하지 않고 생성되는 Manifest</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">helm install --dry-run</code></td>
      <td>Release 이름과 Values를 적용한 설치 결과</td>
    </tr>
  </tbody>
</table>

<p>Rendering 결과에서 다음 항목을 확인한다.</p>

<ul>
  <li>
    <p>사용할 Container Image Registry와 Tag</p>
  </li>
  <li>
    <p>생성되는 RBAC Resource와 ServiceAccount 권한</p>
  </li>
  <li>
    <p>Service Type과 외부 공개 여부</p>
  </li>
  <li>
    <p>PVC, StorageClass와 요청 용량</p>
  </li>
  <li>
    <p>Secret에 들어갈 값과 생성 방식</p>
  </li>
  <li>
    <p>Namespace와 Cluster Scope Resource</p>
  </li>
</ul>

<p>Chart Version을 생략하면 Repository 갱신 시 다른 Version이 선택될 수 있다. 재현 가능한 설치에는 검증한 Chart Version을 명시한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">install </span>my-nginx bitnami/nginx <span class="se">\</span>
  <span class="nt">--version</span> &lt;verified-chart-version&gt;
</code></pre></div></div>

<h2 id="6--bitnami-chart의-현재-배포-상태">6 ) Bitnami Chart의 현재 배포 상태</h2>

<hr />

<p>Bitnami는 2025년부터 Public Container와 Chart 배포 정책을 변경했다. Chart Source는 공개되어 있어도 Chart가 참조하는 Versioned Image가 기존 <code class="language-plaintext highlighter-rouge">docker.io/bitnami</code> Registry에 없을 수 있으며, 이 경우 설치된 Pod는 <code class="language-plaintext highlighter-rouge">ImagePullBackOff</code> 상태가 된다.</p>

<p>따라서 다음 명령이 성공적으로 Chart를 Rendering했다는 사실만으로 Container Image도 Pull할 수 있다고 판단하지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm show chart bitnami/mariadb
helm show values bitnami/mariadb
helm template my-mariadb bitnami/mariadb
</code></pre></div></div>

<p>Chart가 사용하는 Image를 Rendering 결과에서 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm template my-mariadb bitnami/mariadb | <span class="se">\</span>
  <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'^[[:space:]]*image:'</span>
</code></pre></div></div>

<p>설치 후 Pod가 시작되지 않으면 Event와 Image를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods
kubectl describe pod &lt;mariadb-pod-name&gt;
kubectl get pod &lt;mariadb-pod-name&gt; <span class="se">\</span>
  <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.containers[*].image}{"\n"}'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">bitnamilegacy</code> Registry는 이전 Image를 보관하는 Migration 용도이며 신규 수정과 보안 Update가 제공되지 않는다. 단순히 Repository 이름을 Legacy로 바꿔 운영 문제를 해결했다고 판단하지 않는다.</p>

<h2 id="7--nginx-chart-release-관리">7 ) nginx Chart Release 관리</h2>

<hr />

<p>검증한 Chart Version과 Image를 사용할 수 있을 때 nginx Chart를 설치한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">install </span>my-nginx bitnami/nginx <span class="se">\</span>
  <span class="nt">--version</span> &lt;verified-chart-version&gt; <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--create-namespace</span>
</code></pre></div></div>

<p>Helm Release와 Kubernetes Resource를 함께 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm list <span class="nt">--namespace</span> helm-lab
helm status my-nginx <span class="nt">--namespace</span> helm-lab
kubectl get deployments,pods,services <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p>Helm은 Release 상태를 기록하지만 Pod가 Ready인지 대신 보장하지 않는다. <code class="language-plaintext highlighter-rouge">helm status</code>와 함께 Deployment Rollout, Pod Event와 Container Log를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl rollout status deployment/my-nginx <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
kubectl get events <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--sort-by</span><span class="o">=</span>.metadata.creationTimestamp
</code></pre></div></div>

<p>실습이 끝나면 Release를 제거한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm uninstall my-nginx <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p>Release를 제거해도 Chart가 생성한 PVC가 보존 정책에 따라 남을 수 있으므로 Namespace의 Resource를 확인한다.</p>

<h2 id="8--mariadb-chart와-pvc-pending-진단">8 ) MariaDB Chart와 PVC Pending 진단</h2>

<hr />

<p>MariaDB 같은 Stateful Application은 기본 Values에서 Persistence가 활성화되어 있을 수 있다. Cluster에 Default StorageClass나 조건에 맞는 PV가 없으면 PVC가 <code class="language-plaintext highlighter-rouge">Pending</code> 상태로 남고 MariaDB Pod도 정상 시작하지 못한다.</p>

<p>설치 전에 Storage 설정을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm show values bitnami/mariadb | <span class="se">\</span>
  <span class="nb">grep</span> <span class="nt">-A</span> 20 <span class="s1">'persistence:'</span>
kubectl get storageclasses
kubectl get persistentvolumes
</code></pre></div></div>

<p>검증한 Chart와 Image를 사용할 수 있는 환경에서 MariaDB를 설치한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">install </span>my-mariadb bitnami/mariadb <span class="se">\</span>
  <span class="nt">--version</span> &lt;verified-chart-version&gt; <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--create-namespace</span>
</code></pre></div></div>

<p>설치 상태를 Resource 계층별로 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm status my-mariadb <span class="nt">--namespace</span> helm-lab
kubectl get statefulsets,pods <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
kubectl get persistentvolumeclaims <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
kubectl get persistentvolumes
</code></pre></div></div>

<p>PVC가 <code class="language-plaintext highlighter-rouge">Pending</code>이면 Pod Log보다 PVC Event와 StorageClass를 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe persistentvolumeclaim <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  &lt;mariadb-pvc-name&gt;
kubectl get storageclasses
kubectl get events <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--sort-by</span><span class="o">=</span>.metadata.creationTimestamp
</code></pre></div></div>

<p>PV, PVC와 StorageClass의 연결은 <a href="/cloud-native-35-kubernetes-volume-persistent-storage/">Kubernetes Volume과 Persistent Storage</a>에서 자세히 설명한다.</p>

<h2 id="9--persistence를-사용하지-않는-임시-실습">9 ) Persistence를 사용하지 않는 임시 실습</h2>

<hr />

<p>Storage 구성이 없는 격리 실습에서는 Persistence를 끄고 <code class="language-plaintext highlighter-rouge">emptyDir</code>를 사용하도록 Chart 값을 변경할 수 있다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">install </span>my-mariadb bitnami/mariadb <span class="se">\</span>
  <span class="nt">--version</span> &lt;verified-chart-version&gt; <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--set</span> primary.persistence.enabled<span class="o">=</span><span class="nb">false</span> <span class="se">\</span>
  <span class="nt">--set-string</span> auth.rootPassword<span class="o">=</span><span class="s1">'&lt;temporary-password&gt;'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--set</code>에 입력한 Password는 Shell History와 Process 정보에 노출될 수 있다. 위 명령은 Option 구조를 설명하기 위한 임시 실습이며 실제 Credential은 별도 Secret이나 보호된 Values 전달 방식을 사용한다.</p>

<p>Persistence를 끄면 Pod가 교체될 때 Database Data를 잃을 수 있으므로 운영 Database 구성으로 사용하지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">--namespace</span> helm-lab
kubectl <span class="nb">exec</span> <span class="nt">-it</span> <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  statefulset/my-mariadb <span class="nt">--</span> bash
</code></pre></div></div>

<p>실습 Release를 제거한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm uninstall my-mariadb <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<h2 id="10--node-local-pv와-pvc-준비">10 ) Node Local PV와 PVC 준비</h2>

<hr />

<p>직접 구축한 Cluster에서 Dynamic Provisioner가 없다면 특정 Worker의 Directory를 사용하는 정적 PV를 만들 수 있다. 이 방식은 Node 장애 시 다른 Worker로 자동 이동하는 공유 Storage가 아니므로 실습 범위로 제한한다.</p>

<p>먼저 MariaDB Pod를 배치할 Worker 이름을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get nodes
</code></pre></div></div>

<p>다음 명령은 선택한 <code class="language-plaintext highlighter-rouge">worker1</code>에서 실행한다. UID와 GID <code class="language-plaintext highlighter-rouge">1001</code>은 Bitnami MariaDB Image 계열에서 사용하는 값이므로 실제 선택한 Image의 실행 사용자를 먼저 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /data/mariadb
<span class="nb">sudo chown </span>1001:1001 /data/mariadb
<span class="nb">sudo chmod </span>0750 /data/mariadb
</code></pre></div></div>

<p>모든 사용자에게 쓰기 권한을 주는 <code class="language-plaintext highlighter-rouge">chmod 777</code>은 사용하지 않는다. 다음 내용을 <code class="language-plaintext highlighter-rouge">mariadb-pv.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolume</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb-local-pv</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">storageClassName</span><span class="pi">:</span> <span class="s">manual</span>
  <span class="na">persistentVolumeReclaimPolicy</span><span class="pi">:</span> <span class="s">Retain</span>
  <span class="na">capacity</span><span class="pi">:</span>
    <span class="na">storage</span><span class="pi">:</span> <span class="s">1Gi</span>
  <span class="na">volumeMode</span><span class="pi">:</span> <span class="s">Filesystem</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">local</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/data/mariadb</span>
  <span class="na">nodeAffinity</span><span class="pi">:</span>
    <span class="na">required</span><span class="pi">:</span>
      <span class="na">nodeSelectorTerms</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">matchExpressions</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s">kubernetes.io/hostname</span>
              <span class="na">operator</span><span class="pi">:</span> <span class="s">In</span>
              <span class="na">values</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="s">worker1</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb-pvc</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">helm-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">storageClassName</span><span class="pi">:</span> <span class="s">manual</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">1Gi</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hostPath</code>도 Node Directory를 직접 사용하지만 PV와 Pod의 Node 관계를 자동으로 표현하지 않는다. 이 예제는 <code class="language-plaintext highlighter-rouge">local</code> Volume과 <code class="language-plaintext highlighter-rouge">nodeAffinity</code>를 사용해 PV가 <code class="language-plaintext highlighter-rouge">worker1</code>에 종속됨을 Scheduler가 알 수 있게 한다.</p>

<p>Master 또는 관리 Client에서 PV와 PVC를 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create namespace helm-lab <span class="se">\</span>
  <span class="nt">--dry-run</span><span class="o">=</span>client <span class="nt">-o</span> yaml | kubectl apply <span class="nt">-f</span> -
kubectl apply <span class="nt">-f</span> mariadb-pv.yaml
kubectl get persistentvolume
kubectl get persistentvolumeclaim <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<h2 id="11--기존-pvc와-secret을-mariadb-chart에-연결">11 ) 기존 PVC와 Secret을 MariaDB Chart에 연결</h2>

<hr />

<p>MariaDB Credential을 별도 Secret으로 준비하고 Chart에는 Secret 이름만 전달한다. 현재 Bitnami MariaDB Chart를 기준으로 Secret Key 요구 사항은 선택한 Chart Version의 README와 <code class="language-plaintext highlighter-rouge">values.yaml</code>에서 다시 확인한다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">mariadb-auth-secret.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb-auth</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">helm-lab</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">stringData</span><span class="pi">:</span>
  <span class="na">mariadb-root-password</span><span class="pi">:</span> <span class="s">REPLACE_WITH_ROOT_PASSWORD</span>
  <span class="na">mariadb-password</span><span class="pi">:</span> <span class="s">REPLACE_WITH_USER_PASSWORD</span>
  <span class="na">mariadb-replication-password</span><span class="pi">:</span> <span class="s">REPLACE_WITH_REPLICATION_PASSWORD</span>
</code></pre></div></div>

<p>실제 Password가 채워진 File은 공개 Repository에 Commit하지 않는다. Secret을 적용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> mariadb-auth-secret.yaml
</code></pre></div></div>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">mariadb-values.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">architecture</span><span class="pi">:</span> <span class="s">standalone</span>

<span class="na">auth</span><span class="pi">:</span>
  <span class="na">existingSecret</span><span class="pi">:</span> <span class="s">mariadb-auth</span>
  <span class="na">database</span><span class="pi">:</span> <span class="s">testdb</span>
  <span class="na">username</span><span class="pi">:</span> <span class="s">dbuser</span>

<span class="na">primary</span><span class="pi">:</span>
  <span class="na">persistence</span><span class="pi">:</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">existingClaim</span><span class="pi">:</span> <span class="s">mariadb-pvc</span>
  <span class="na">nodeSelector</span><span class="pi">:</span>
    <span class="na">kubernetes.io/hostname</span><span class="pi">:</span> <span class="s">worker1</span>
</code></pre></div></div>

<p>PVC와 Pod 모두 <code class="language-plaintext highlighter-rouge">worker1</code>에 고정되는지 Rendering 결과를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm template my-mariadb bitnami/mariadb <span class="se">\</span>
  <span class="nt">--version</span> &lt;verified-chart-version&gt; <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--values</span> mariadb-values.yaml
</code></pre></div></div>

<p>Chart와 Image를 검증한 환경에서 설치한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">install </span>my-mariadb bitnami/mariadb <span class="se">\</span>
  <span class="nt">--version</span> &lt;verified-chart-version&gt; <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--values</span> mariadb-values.yaml
</code></pre></div></div>

<p>Pod가 선택한 Worker에서 실행되고 PVC가 연결됐는지 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">-o</span> wide
kubectl get persistentvolume
kubectl get persistentvolumeclaim <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
kubectl describe pod <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  &lt;mariadb-pod-name&gt;
</code></pre></div></div>

<h2 id="12--사용자-helm-chart-구조">12 ) 사용자 Helm Chart 구조</h2>

<hr />

<p><code class="language-plaintext highlighter-rouge">helm create</code>는 기본 Chart Directory를 자동으로 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm create custom-mariadb
</code></pre></div></div>

<p>기본 Sample을 목적에 맞게 정리하면 다음 구조가 된다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>custom-mariadb/
├── Chart.yaml
├── values.yaml
├── .helmignore
└── templates/
    ├── secret.yaml
    ├── service.yaml
    ├── statefulset.yaml
    └── pvc.yaml
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>File</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Chart.yaml</code></td>
      <td>Chart 이름, Type, Chart Version과 Application Version 정의</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">values.yaml</code></td>
      <td>Template에서 사용할 기본값 정의</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">templates/</code></td>
      <td>Rendering할 Kubernetes Resource Template 저장</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.helmignore</code></td>
      <td>Chart Package에서 제외할 File Pattern 정의</td>
    </tr>
  </tbody>
</table>

<p>Helm은 <code class="language-plaintext highlighter-rouge">templates/</code> 아래의 Template을 Rendering하므로 Directory 이름을 정확하게 사용해야 한다.</p>

<h2 id="13--chart-metadata와-values">13 ) Chart Metadata와 Values</h2>

<hr />

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">custom-mariadb/Chart.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">A custom MariaDB Helm chart</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">application</span>
<span class="na">version</span><span class="pi">:</span> <span class="s">0.1.0</span>
<span class="na">appVersion</span><span class="pi">:</span> <span class="s2">"</span><span class="s">11.4"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">version</code>은 Chart Package Version이고 <code class="language-plaintext highlighter-rouge">appVersion</code>은 Chart가 배포하는 Application Version을 설명하는 값이다. 둘은 같은 의미가 아니다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">custom-mariadb/values.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">replicaCount</span><span class="pi">:</span> <span class="m">1</span>

<span class="na">image</span><span class="pi">:</span>
  <span class="na">repository</span><span class="pi">:</span> <span class="s">mariadb</span>
  <span class="na">tag</span><span class="pi">:</span> <span class="s2">"</span><span class="s">11.4"</span>
  <span class="na">pullPolicy</span><span class="pi">:</span> <span class="s">IfNotPresent</span>

<span class="na">mariadb</span><span class="pi">:</span>
  <span class="na">existingSecret</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
  <span class="na">rootPassword</span><span class="pi">:</span> <span class="s">REPLACE_WITH_ROOT_PASSWORD</span>
  <span class="na">database</span><span class="pi">:</span> <span class="s">appdb</span>
  <span class="na">user</span><span class="pi">:</span> <span class="s">appuser</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s">REPLACE_WITH_USER_PASSWORD</span>

<span class="na">persistence</span><span class="pi">:</span>
  <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">existingClaim</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
  <span class="na">storageClassName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
  <span class="na">storageSize</span><span class="pi">:</span> <span class="s">1Gi</span>

<span class="na">service</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">3306</span>

<span class="na">nodeSelector</span><span class="pi">:</span> <span class="pi">{}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">mariadb.rootPassword</code>는 Secret Template이 참조하는 Field이고 <code class="language-plaintext highlighter-rouge">service.type</code>은 Kubernetes가 인식하는 <code class="language-plaintext highlighter-rouge">ClusterIP</code> 값을 사용한다. 실제 운영용 Values에는 평문 Password를 저장하지 않고 <code class="language-plaintext highlighter-rouge">mariadb.existingSecret</code>으로 이미 생성한 Secret을 참조한다.</p>

<h2 id="14--secret-template">14 ) Secret Template</h2>

<hr />

<p>Helm Template의 <code class="language-plaintext highlighter-rouge">{{ ... }}</code> 표현은 Jekyll Liquid와 충돌하므로 Code Block 전체를 Raw Tag로 감싼다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">custom-mariadb/templates/secret.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{{</span><span class="nv">- if not .Values.mariadb.existingSecret</span> <span class="pi">}}</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span><span class="s">-secret</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
    <span class="na">app.kubernetes.io/instance</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">stringData</span><span class="pi">:</span>
  <span class="na">mariadb-root-password</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.mariadb.rootPassword | quote</span> <span class="pi">}}</span>
  <span class="na">mariadb-password</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.mariadb.password | quote</span> <span class="pi">}}</span>
<span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">mariadb.existingSecret</code>이 비어 있을 때만 Chart가 Secret을 생성한다. 외부 Secret 이름을 지정하면 이 Template은 Resource를 만들지 않는다.</p>

<h2 id="15--service와-pvc-template">15 ) Service와 PVC Template</h2>

<hr />

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">custom-mariadb/templates/service.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
    <span class="na">app.kubernetes.io/instance</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.service.type</span> <span class="pi">}}</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
    <span class="na">app.kubernetes.io/instance</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb</span>
      <span class="na">port</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.service.port</span> <span class="pi">}}</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="s">mariadb</span>
</code></pre></div></div>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">custom-mariadb/templates/pvc.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{{</span><span class="nv">- if and .Values.persistence.enabled (not .Values.persistence.existingClaim)</span> <span class="pi">}}</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span><span class="s">-data</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
    <span class="na">app.kubernetes.io/instance</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="pi">{{</span><span class="nv">- if .Values.persistence.storageClassName</span> <span class="pi">}}</span>
  <span class="na">storageClassName</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.persistence.storageClassName | quote</span> <span class="pi">}}</span>
  <span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.persistence.storageSize</span> <span class="pi">}}</span>
<span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
</code></pre></div></div>

<p>Persistence가 활성화되고 <code class="language-plaintext highlighter-rouge">existingClaim</code>이 비어 있을 때만 PVC를 생성한다. <code class="language-plaintext highlighter-rouge">storageClassName</code>을 비워 두면 Cluster의 Default StorageClass 선택 규칙이 적용된다.</p>

<h2 id="16--statefulset-template">16 ) StatefulSet Template</h2>

<hr />

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">custom-mariadb/templates/statefulset.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">StatefulSet</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
    <span class="na">app.kubernetes.io/instance</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">serviceName</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.replicaCount</span> <span class="pi">}}</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
      <span class="na">app.kubernetes.io/instance</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">custom-mariadb</span>
        <span class="na">app.kubernetes.io/instance</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Release.Name</span> <span class="pi">}}</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="pi">{{</span><span class="nv">- with .Values.nodeSelector</span> <span class="pi">}}</span>
      <span class="na">nodeSelector</span><span class="pi">:</span>
        <span class="pi">{{</span><span class="nv">- toYaml . | nindent 8</span> <span class="pi">}}</span>
      <span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">.Values.image.repository</span><span class="nv"> </span><span class="s">}}:{{</span><span class="nv"> </span><span class="s">.Values.image.tag</span><span class="nv"> </span><span class="s">}}"</span>
          <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.image.pullPolicy</span> <span class="pi">}}</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_ROOT_PASSWORD</span>
              <span class="na">valueFrom</span><span class="pi">:</span>
                <span class="na">secretKeyRef</span><span class="pi">:</span>
                  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">default (printf "%s-secret" .Release.Name) .Values.mariadb.existingSecret</span> <span class="pi">}}</span>
                  <span class="na">key</span><span class="pi">:</span> <span class="s">mariadb-root-password</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_DATABASE</span>
              <span class="na">value</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.mariadb.database | quote</span> <span class="pi">}}</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_USER</span>
              <span class="na">value</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">.Values.mariadb.user | quote</span> <span class="pi">}}</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_PASSWORD</span>
              <span class="na">valueFrom</span><span class="pi">:</span>
                <span class="na">secretKeyRef</span><span class="pi">:</span>
                  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">default (printf "%s-secret" .Release.Name) .Values.mariadb.existingSecret</span> <span class="pi">}}</span>
                  <span class="na">key</span><span class="pi">:</span> <span class="s">mariadb-password</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb</span>
              <span class="na">containerPort</span><span class="pi">:</span> <span class="m">3306</span>
          <span class="pi">{{</span><span class="nv">- if .Values.persistence.enabled</span> <span class="pi">}}</span>
          <span class="na">volumeMounts</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">data</span>
              <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/var/lib/mysql</span>
          <span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
      <span class="pi">{{</span><span class="nv">- if .Values.persistence.enabled</span> <span class="pi">}}</span>
      <span class="na">volumes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">data</span>
          <span class="na">persistentVolumeClaim</span><span class="pi">:</span>
            <span class="na">claimName</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">default (printf "%s-data" .Release.Name) .Values.persistence.existingClaim</span> <span class="pi">}}</span>
      <span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
</code></pre></div></div>

<p>StatefulSet의 Selector와 Pod Label은 Chart의 Release 이름까지 포함하여 다른 Release의 Pod를 관리하지 않게 한다. <code class="language-plaintext highlighter-rouge">existingSecret</code>과 <code class="language-plaintext highlighter-rouge">existingClaim</code>이 있으면 외부 Resource를 사용하고, 비어 있으면 Chart가 만든 Secret과 PVC 이름을 참조한다.</p>

<p>이 Chart는 하나의 MariaDB Instance를 위한 학습 예제이다. <code class="language-plaintext highlighter-rouge">replicaCount</code>를 1보다 크게 설정하면 여러 MariaDB Process가 같은 PVC를 사용하게 되므로 복제 구성이 되지 않으며 Data가 손상될 수 있다. MariaDB 복제는 Database의 복제 설정과 Pod별 Volume을 함께 제공하는 별도 Chart 구조가 필요하다.</p>

<h2 id="17--chart-검증과-설치">17 ) Chart 검증과 설치</h2>

<hr />

<p>Chart를 Cluster에 적용하기 전에 구조와 Rendering 결과를 검사한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm lint ./custom-mariadb
helm template my-custom-mariadb ./custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p>앞에서 만든 <code class="language-plaintext highlighter-rouge">mariadb-auth</code> Secret과 <code class="language-plaintext highlighter-rouge">mariadb-pvc</code>를 사용하려면 다음 내용을 <code class="language-plaintext highlighter-rouge">custom-mariadb-lab-values.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">mariadb</span><span class="pi">:</span>
  <span class="na">existingSecret</span><span class="pi">:</span> <span class="s">mariadb-auth</span>

<span class="na">persistence</span><span class="pi">:</span>
  <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">existingClaim</span><span class="pi">:</span> <span class="s">mariadb-pvc</span>

<span class="na">nodeSelector</span><span class="pi">:</span>
  <span class="na">kubernetes.io/hostname</span><span class="pi">:</span> <span class="s">worker1</span>
</code></pre></div></div>

<p>외부 Secret, PVC와 Worker 배치 조건을 적용한 결과도 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm template my-custom-mariadb ./custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--values</span> custom-mariadb-lab-values.yaml
</code></pre></div></div>

<p>Server 측 API 검증까지 수행하려면 Rendering 결과를 <code class="language-plaintext highlighter-rouge">kubectl apply --dry-run=server</code>에 전달한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm template my-custom-mariadb ./custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--values</span> custom-mariadb-lab-values.yaml | <span class="se">\</span>
  kubectl apply <span class="se">\</span>
    <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
    <span class="nt">--dry-run</span><span class="o">=</span>server <span class="se">\</span>
    <span class="nt">-f</span> -
</code></pre></div></div>

<p>검증 후 Chart를 설치한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">install </span>my-custom-mariadb ./custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--create-namespace</span> <span class="se">\</span>
  <span class="nt">--values</span> custom-mariadb-lab-values.yaml
</code></pre></div></div>

<p>Release와 StatefulSet, Pod, Service와 PVC를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm status my-custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
kubectl get statefulsets,pods,services,persistentvolumeclaims <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<h2 id="18--upgrade와-rollback">18 ) Upgrade와 Rollback</h2>

<hr />

<p>Values를 변경한 뒤 Upgrade하면 Release Revision이 증가한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm upgrade my-custom-mariadb ./custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab <span class="se">\</span>
  <span class="nt">--values</span> custom-mariadb-lab-values.yaml <span class="se">\</span>
  <span class="nt">--set</span> image.tag<span class="o">=</span><span class="s1">'&lt;verified-mariadb-version&gt;'</span>
</code></pre></div></div>

<p>History와 적용된 Values를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm <span class="nb">history </span>my-custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
helm get values my-custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
helm get manifest my-custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p>문제가 있으면 이전 Revision으로 Rollback한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm rollback my-custom-mariadb &lt;revision&gt; <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p>Helm Rollback은 Kubernetes Resource Spec을 과거 Release Revision으로 되돌린다. Database File Format이나 이미 변경된 Data를 자동으로 복구하지 않으므로 Stateful Application은 Backup과 Migration 호환성을 별도로 확인해야 한다.</p>

<h2 id="19--실습-resource-정리">19 ) 실습 Resource 정리</h2>

<hr />

<p>Helm으로 설치한 Release를 확인하고 제거한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm list <span class="nt">--all-namespaces</span>
helm uninstall my-custom-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
helm uninstall my-mariadb <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
helm uninstall my-nginx <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">helm list --namespace helm-lab</code>에서 존재하는 Release만 제거한다. 생성하지 않은 Release를 <code class="language-plaintext highlighter-rouge">helm uninstall</code>하면 찾을 수 없다는 오류가 발생한다.</p>

<p>정적 PV와 PVC는 Data 보존 여부를 확인한 뒤 별도로 처리한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get persistentvolume
kubectl get persistentvolumeclaim <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
kubectl delete <span class="nt">-f</span> mariadb-pv.yaml
kubectl delete <span class="nt">-f</span> mariadb-auth-secret.yaml <span class="se">\</span>
  <span class="nt">--ignore-not-found</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">persistentVolumeReclaimPolicy: Retain</code>인 PV를 삭제해도 Worker의 <code class="language-plaintext highlighter-rouge">/data/mariadb</code> Data는 자동으로 삭제되지 않는다. Data가 더 이상 필요하지 않은지 확인한 뒤 해당 Worker에서 별도로 정리한다.</p>

<p><code class="language-plaintext highlighter-rouge">helm-lab</code>이 이 실습에만 사용됐는지 확인한 뒤 Namespace를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get all,secret,persistentvolumeclaim <span class="se">\</span>
  <span class="nt">--namespace</span> helm-lab
kubectl delete namespace helm-lab
</code></pre></div></div>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>Helm은 여러 Kubernetes Resource를 Chart로 Packaging하고 설치 결과를 Release와 Revision으로 관리한다.</p>
    </li>
    <li>
      <p>Chart와 Values를 Rendering한 결과가 Manifest이며 Control Plane은 이 최종 Resource만 처리한다.</p>
    </li>
    <li>
      <p>외부 Chart는 Version, Image Registry, RBAC, Service와 Storage 설정을 확인한 뒤 설치한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">stable</code>과 <code class="language-plaintext highlighter-rouge">incubator</code> Repository는 Archive이며 현재 Chart는 Artifact Hub와 유지보수되는 Repository·OCI Registry에서 찾는다.</p>
    </li>
    <li>
      <p>사용자 Chart는 <code class="language-plaintext highlighter-rouge">helm package</code>로 <code class="language-plaintext highlighter-rouge">.tgz</code> Archive를 만든 뒤 <code class="language-plaintext highlighter-rouge">helm push</code>로 OCI Registry에 배포하며 Chart 이름과 Version은 <code class="language-plaintext highlighter-rouge">Chart.yaml</code>에서 결정된다.</p>
    </li>
    <li>
      <p>Bitnami Chart Source와 기존 Image의 제공 상태는 같지 않으므로 Rendering된 Image를 실제로 Pull할 수 있는지 확인해야 한다.</p>
    </li>
    <li>
      <p>MariaDB PVC가 Pending이면 Pod Log보다 PVC Event, PV와 StorageClass를 먼저 확인한다.</p>
    </li>
    <li>
      <p>Node Local Storage는 PV Node Affinity와 Pod 배치 조건을 같은 Worker에 맞춰야 한다.</p>
    </li>
    <li>
      <p>Helm Template은 Jekyll Liquid와 충돌하지 않도록 Raw Tag로 감싸고 <code class="language-plaintext highlighter-rouge">helm lint</code>와 <code class="language-plaintext highlighter-rouge">helm template</code>로 검증한다.</p>
    </li>
    <li>
      <p>Helm Rollback은 Resource Spec을 되돌리며 Database Data와 Schema를 자동으로 복원하지 않는다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Kubernetes" /><summary type="html"><![CDATA[Helm의 Chart·Release 구조, Repository와 OCI Chart 사용, MariaDB Storage 연결 및 사용자 Chart 작성 정리]]></summary></entry><entry><title type="html">Kubernetes Blue-Green과 Canary 배포</title><link href="https://hyn128.site/cloud-native-38-kubernetes-blue-green-canary/" rel="alternate" type="text/html" title="Kubernetes Blue-Green과 Canary 배포" /><published>2026-09-07T00:00:00+09:00</published><updated>2026-09-07T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-38-kubernetes-blue-green-canary</id><content type="html" xml:base="https://hyn128.site/cloud-native-38-kubernetes-blue-green-canary/"><![CDATA[<p>Rolling Update는 이전 Version과 새 Version의 Pod를 점진적으로 교체한다. Blue-Green은 두 Version을 별도 환경으로 유지한 뒤 Service의 연결 대상을 한 번에 바꾸고, Canary는 새 Version에 전달되는 Traffic을 제한하여 점진적으로 확대한다.</p>

<p>이 문서는 <a href="/cloud-native-28-replicaset-deployment/">Kubernetes ReplicaSet과 Deployment</a>의 Rollout과 <a href="/cloud-native-31-kubernetes-service-network/">Kubernetes Service Discovery와 외부 노출</a>의 Service Selector를 알고 있다고 가정한다.</p>

<h2 id="1--배포-전략-비교">1 ) 배포 전략 비교</h2>

<hr />

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>Version 전환 방식</th>
      <th>추가 Resource</th>
      <th>Rollback 방식</th>
      <th>주요 고려 사항</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Rolling Update</td>
      <td>이전 Pod를 줄이고 새 Pod를 늘림</td>
      <td><code class="language-plaintext highlighter-rouge">maxSurge</code>만큼 추가 Pod 가능</td>
      <td>이전 Revision으로 Rollout</td>
      <td>두 Version이 잠시 같은 Service에 존재</td>
    </tr>
    <tr>
      <td>Blue-Green</td>
      <td>두 환경을 모두 준비하고 Traffic 대상을 한 번에 변경</td>
      <td>두 Version을 동시에 실행할 Resource 필요</td>
      <td>Service Selector를 이전 환경으로 복원</td>
      <td>전환 전 새 환경 검증과 Data 호환성 필요</td>
    </tr>
    <tr>
      <td>Canary</td>
      <td>새 Version의 Traffic 비율을 조금씩 확대</td>
      <td>두 Version과 Traffic 제어 수단 필요</td>
      <td>새 Version Traffic을 0으로 축소</td>
      <td>지표, 판정 기준과 Session 처리 필요</td>
    </tr>
  </tbody>
</table>

<p>배포 전략은 Pod 교체 방식만 결정한다. Database Schema, Message Format과 외부 API가 두 Version에서 호환되는지도 별도로 검토해야 한다.</p>

<h2 id="2--blue-green-동작-구조">2 ) Blue-Green 동작 구조</h2>

<hr />

<p>Blue-Green에서는 현재 운영 중인 Version을 Blue, 새 Version을 Green으로 구분한다. 두 Deployment는 서로 다른 <code class="language-plaintext highlighter-rouge">color</code> Label을 사용하며 Service는 그중 하나만 선택한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                       ┌─▶ Blue Deployment: v0.0.1
Client ─▶ Service ─────┤    color: blue
 selector.color=blue   │
                       └── Green Deployment: v0.0.2
                            color: green
                            전환 전 별도 검증
</code></pre></div></div>

<p>전환할 때 Pod를 다시 생성하는 것이 아니라 Service의 Selector를 <code class="language-plaintext highlighter-rouge">blue</code>에서 <code class="language-plaintext highlighter-rouge">green</code>으로 변경한다. EndpointSlice Controller가 새 Selector와 일치하는 Green Pod를 Endpoint로 반영하면 이후 Traffic이 Green으로 전달된다.</p>

<h2 id="3--blue와-green-deployment-생성">3 ) Blue와 Green Deployment 생성</h2>

<hr />

<p>현재 Version을 <code class="language-plaintext highlighter-rouge">print-version-blue.yaml</code>로 작성한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">print-version-blue</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
    <span class="na">color</span><span class="pi">:</span> <span class="s">blue</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
      <span class="na">color</span><span class="pi">:</span> <span class="s">blue</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
        <span class="na">color</span><span class="pi">:</span> <span class="s">blue</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">print-version</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/jpubdocker/print-version:v0.0.1</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>

<p>새 Version을 <code class="language-plaintext highlighter-rouge">print-version-green.yaml</code>로 작성한다. Resource 이름, <code class="language-plaintext highlighter-rouge">color</code> Label과 Image Version이 Blue와 다르다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">print-version-green</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
    <span class="na">color</span><span class="pi">:</span> <span class="s">green</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
      <span class="na">color</span><span class="pi">:</span> <span class="s">green</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
        <span class="na">color</span><span class="pi">:</span> <span class="s">green</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">print-version</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/jpubdocker/print-version:v0.0.2</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>

<p>Master 또는 kubeconfig가 설정된 관리 Client에서 두 Version을 배포한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> print-version-blue.yaml
kubectl apply <span class="nt">-f</span> print-version-green.yaml
kubectl rollout status deployment/print-version-blue
kubectl rollout status deployment/print-version-green
kubectl get pods <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>print-version <span class="nt">--show-labels</span>
</code></pre></div></div>

<p>두 Deployment가 모두 Ready여도 Service가 선택하지 않은 Green Pod에는 운영 Traffic이 전달되지 않는다. 전환 전에는 <code class="language-plaintext highlighter-rouge">kubectl port-forward</code>나 별도 검증용 Service를 이용하여 Green Version을 확인할 수 있다.</p>

<h2 id="4--service-selector로-version-전환">4 ) Service Selector로 Version 전환</h2>

<hr />

<p>Blue Version을 선택하는 Service를 <code class="language-plaintext highlighter-rouge">print-version-service-color.yaml</code>로 작성한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">print-version</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
    <span class="na">color</span><span class="pi">:</span> <span class="s">blue</span>
</code></pre></div></div>

<p>Service를 적용하고 Endpoint가 Blue Pod를 가리키는지 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> print-version-service-color.yaml
kubectl get service print-version
kubectl get endpointslice <span class="se">\</span>
  <span class="nt">-l</span> kubernetes.io/service-name<span class="o">=</span>print-version <span class="se">\</span>
  <span class="nt">-o</span> wide
</code></pre></div></div>

<p>지속적으로 Version을 확인할 Pod를 <code class="language-plaintext highlighter-rouge">update-checker.yaml</code>로 작성한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">update-checker</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">update-checker</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">update-checker</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/jpubdocker/debug:v0.1.0</span>
      <span class="na">command</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">sh</span>
        <span class="pi">-</span> <span class="s">-c</span>
        <span class="pi">-</span> <span class="pi">|</span>
          <span class="s">while true</span>
          <span class="s">do</span>
            <span class="s">VERSION=$(curl -s http://print-version/)</span>
            <span class="s">echo "[$(date)] ${VERSION}"</span>
            <span class="s">sleep 1</span>
          <span class="s">done</span>
</code></pre></div></div>

<p>Pod를 생성하고 응답 Version을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> update-checker.yaml
kubectl logs <span class="nt">-f</span> pod/update-checker
</code></pre></div></div>

<p>Green으로 전환하려면 <code class="language-plaintext highlighter-rouge">print-version-service-color.yaml</code>의 <code class="language-plaintext highlighter-rouge">color</code>를 다음과 같이 변경한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">selector</span><span class="pi">:</span>
  <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
  <span class="na">color</span><span class="pi">:</span> <span class="s">green</span>
</code></pre></div></div>

<p>변경된 Service를 적용하고 Endpoint와 Log를 다시 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> print-version-service-color.yaml
kubectl get endpointslice <span class="se">\</span>
  <span class="nt">-l</span> kubernetes.io/service-name<span class="o">=</span>print-version <span class="se">\</span>
  <span class="nt">-o</span> wide
kubectl logs <span class="nt">-f</span> pod/update-checker
</code></pre></div></div>

<p>문제가 발견되면 같은 File의 <code class="language-plaintext highlighter-rouge">color</code>를 <code class="language-plaintext highlighter-rouge">blue</code>로 되돌려 다시 적용한다. 빠른 전환이 가능하려면 검증 기간 동안 Blue Deployment를 삭제하거나 축소하지 않아야 한다.</p>

<h2 id="5--blue-green-전환-시-확인-사항">5 ) Blue-Green 전환 시 확인 사항</h2>

<hr />

<p>Service Selector 변경은 Application 내부 상태까지 전환하지 않는다. 다음 항목을 함께 확인한다.</p>

<ul>
  <li>
    <p>Green Pod가 모두 Ready인지 확인한다.</p>
  </li>
  <li>
    <p>Blue와 Green이 같은 Database나 Message Broker를 사용할 때 Schema와 Message가 양쪽 Version에서 호환되는지 확인한다.</p>
  </li>
  <li>
    <p>기존 Connection과 처리 중인 요청이 Blue Pod에 남을 수 있음을 고려한다.</p>
  </li>
  <li>
    <p><a href="/cloud-native-37-kubernetes-health-check-restart-policy/">Kubernetes Health Check와 restartPolicy</a>의 Readiness Probe와 Graceful Shutdown을 구성한다.</p>
  </li>
  <li>
    <p>Rollback 판단 기준과 Blue 환경을 유지할 시간을 배포 전에 정한다.</p>
  </li>
</ul>

<h2 id="6--canary-배포">6 ) Canary 배포</h2>

<hr />

<blockquote>
  <p><strong>Canary 배포</strong></p>

  <p>새 Version에 제한된 Traffic만 전달하여 오류율과 지연 시간 등의 지표를 확인한 뒤 Traffic 비율을 단계적으로 확대하는 방식이다.</p>
</blockquote>

<p>Canary에는 다음 세 요소가 필요하다.</p>

<ol>
  <li>
    <p>동시에 실행되는 Stable Version과 Canary Version</p>
  </li>
  <li>
    <p>Version별 Traffic을 구분하거나 비율을 조정할 수단</p>
  </li>
  <li>
    <p>다음 단계 진행 또는 Rollback을 결정할 관찰 지표와 기준</p>
  </li>
</ol>

<p>단순히 Canary Pod가 Running 상태인지만 확인해서는 사용자 요청이 정상 처리되는지 판단할 수 없다. HTTP 오류율, 응답 시간, Application 오류와 주요 업무 지표를 함께 관찰해야 한다.</p>

<h2 id="7--replica-비율을-이용한-단순-canary">7 ) Replica 비율을 이용한 단순 Canary</h2>

<hr />

<p>가장 단순한 방식은 Stable과 Canary Deployment에 같은 <code class="language-plaintext highlighter-rouge">app</code> Label을 부여하고 하나의 Service가 두 Version을 모두 선택하게 하는 것이다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Service selector: app=print-version
        │
        ├─ Stable Deployment: 9 Pods
        └─ Canary Deployment: 1 Pod
</code></pre></div></div>

<p>Stable과 Canary의 Deployment Selector는 <code class="language-plaintext highlighter-rouge">track</code>까지 포함하여 각 Controller가 자신의 Pod만 관리하게 하고, Service Selector는 공통 <code class="language-plaintext highlighter-rouge">app</code> Label만 사용한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Stable Deployment의 핵심 Label</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">9</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
      <span class="na">track</span><span class="pi">:</span> <span class="s">stable</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
        <span class="na">track</span><span class="pi">:</span> <span class="s">stable</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Canary Deployment의 핵심 Label</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
      <span class="na">track</span><span class="pi">:</span> <span class="s">canary</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
        <span class="na">track</span><span class="pi">:</span> <span class="s">canary</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 두 Version을 함께 선택하는 Service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">print-version</span>
</code></pre></div></div>

<p>Pod가 9대와 1대라고 해서 모든 구간에서 요청이 정확히 90%와 10%로 분배된다고 보장되지는 않는다. Service는 요청 수를 기준으로 정밀한 가중치를 적용하는 Traffic Router가 아니며, Connection 재사용과 Client 동작도 실제 비율에 영향을 준다. 이 방식은 단순한 실습이나 낮은 정밀도의 검증에 적합하다.</p>

<h2 id="8--ingress-annotation을-이용한-가중치-분배">8 ) Ingress Annotation을 이용한 가중치 분배</h2>

<hr />

<p>ingress-nginx는 Stable Ingress와 같은 Host·Path를 사용하는 Canary Ingress에 Annotation을 지정하여 일부 Traffic을 Canary Service로 전달할 수 있다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">print-version-canary</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">nginx.ingress.kubernetes.io/canary</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">nginx.ingress.kubernetes.io/canary-weight</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ingressClassName</span><span class="pi">:</span> <span class="s">nginx</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">print-version.example.com</span>
      <span class="na">http</span><span class="pi">:</span>
        <span class="na">paths</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
            <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
            <span class="na">backend</span><span class="pi">:</span>
              <span class="na">service</span><span class="pi">:</span>
                <span class="na">name</span><span class="pi">:</span> <span class="s">print-version-canary</span>
                <span class="na">port</span><span class="pi">:</span>
                  <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>이 Annotation은 Kubernetes Ingress의 공통 기능이 아니라 ingress-nginx 구현 전용 기능이다. ingress-nginx Controller는 2026년 3월 24일부로 유지보수가 종료되어 신규 Release와 보안 수정이 제공되지 않는다. 따라서 기존 격리 학습 환경의 동작 이해에만 사용하고 신규 운영 환경의 Canary 구성으로 선택하지 않는다. Ingress API와 Controller의 구분은 <a href="/cloud-native-33-kubernetes-ingress-routing/">Kubernetes Ingress Resource와 HTTP Routing</a>에서 확인할 수 있다.</p>

<p>운영 환경에서는 현재 유지보수되는 Gateway API 구현, Service Mesh 또는 배포 Controller가 지원하는 Traffic Router의 기능과 제약을 확인한다.</p>

<h2 id="9--argo-rollouts를-이용한-canary-단계-관리">9 ) Argo Rollouts를 이용한 Canary 단계 관리</h2>

<hr />

<p>Argo Rollouts는 Kubernetes의 기본 Deployment가 아니라 별도 Controller와 CRD가 제공하는 <code class="language-plaintext highlighter-rouge">Rollout</code> Resource이다. 다음 Manifest를 적용하려면 Cluster에 Argo Rollouts Controller와 CRD가 먼저 설치되어 있어야 한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">argoproj.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Rollout</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">print-version-rollout</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">strategy</span><span class="pi">:</span>
    <span class="na">canary</span><span class="pi">:</span>
      <span class="na">steps</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">setWeight</span><span class="pi">:</span> <span class="m">10</span>
        <span class="pi">-</span> <span class="na">pause</span><span class="pi">:</span>
            <span class="na">duration</span><span class="pi">:</span> <span class="s">1h</span>
        <span class="pi">-</span> <span class="na">setWeight</span><span class="pi">:</span> <span class="m">50</span>
        <span class="pi">-</span> <span class="na">pause</span><span class="pi">:</span> <span class="pi">{}</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">print-version-rollout</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">print-version-rollout</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">print-version</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/jpubdocker/print-version:v0.0.2</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th>동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">setWeight: 10</code></td>
      <td>Canary 비중을 10으로 설정</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pause.duration: 1h</code></td>
      <td>1시간 동안 자동으로 대기하여 지표를 관찰할 시간 확보</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">setWeight: 50</code></td>
      <td>다음 단계에서 Canary 비중을 50으로 확대</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pause: {}</code></td>
      <td>사용자가 재개할 때까지 무기한 대기</td>
    </tr>
  </tbody>
</table>

<p>Traffic Router를 연결하지 않은 기본 Canary에서는 Argo Rollouts도 Replica 수로 Weight를 근사한다. 정밀한 Traffic 비율이 필요하면 선택한 Gateway API, Ingress 또는 Service Mesh Provider와 연동해야 한다.</p>

<p>Controller와 CRD가 준비된 환경에서는 다음과 같이 Resource 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> print-version-rollout.yaml
kubectl get rollouts.argoproj.io print-version-rollout
kubectl describe rollouts.argoproj.io print-version-rollout
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">pause: {}</code> 단계에서는 관찰 지표를 확인한 뒤 수동으로 진행하거나 중단한다. 자동화된 배포에서는 AnalysisTemplate 등 별도의 판정 구성을 연결하여 다음 단계 진행 여부를 결정할 수 있다.</p>

<h2 id="10--session과-canary-traffic">10 ) Session과 Canary Traffic</h2>

<hr />

<p>여러 Version이 동시에 요청을 처리할 때 Server Memory에 Session을 저장하면 사용자가 요청마다 다른 Version에 연결되어 상태가 끊길 수 있다. Sticky Session으로 같은 Backend 연결을 유지할 수 있지만 장기적으로는 다음 항목을 함께 검토한다.</p>

<ul>
  <li>
    <p>공유 Session Store 사용 여부</p>
  </li>
  <li>
    <p>Version 간 Cookie와 Session Format 호환성</p>
  </li>
  <li>
    <p>Sticky Session 때문에 Canary Traffic 비율과 사용자 표본이 왜곡되는지 여부</p>
  </li>
  <li>
    <p>Canary Pod 장애 시 Session Failover 방식</p>
  </li>
</ul>

<p>Sticky Session은 Canary의 필수 조건이 아니라 Application의 Session 저장 방식에 따라 선택하는 기능이다.</p>

<h2 id="11--전략-선택-기준">11 ) 전략 선택 기준</h2>

<hr />

<table>
  <thead>
    <tr>
      <th>조건</th>
      <th>적합한 출발점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Kubernetes 기본 Rollout으로 가용성을 유지하며 교체</td>
      <td>Deployment Rolling Update</td>
    </tr>
    <tr>
      <td>두 Version의 혼재를 피하고 즉시 전환·복원이 필요</td>
      <td>Blue-Green과 Service Selector 전환</td>
    </tr>
    <tr>
      <td>간단한 환경에서 대략적인 소수 사용자 검증</td>
      <td>Replica 비율 기반 Canary</td>
    </tr>
    <tr>
      <td>정밀한 Weight, 자동 분석과 단계 승격 필요</td>
      <td>Argo Rollouts와 유지보수되는 Traffic Router 연동</td>
    </tr>
  </tbody>
</table>

<p>어떤 전략을 선택하더라도 Readiness Probe, Graceful Shutdown, 관찰 지표, Rollback 조건과 Data 호환성을 먼저 준비해야 한다.</p>

<h2 id="12--실습-resource-정리">12 ) 실습 Resource 정리</h2>

<hr />

<p>Blue-Green 실습 Resource를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-f</span> update-checker.yaml <span class="nt">--ignore-not-found</span>
kubectl delete <span class="nt">-f</span> print-version-service-color.yaml <span class="nt">--ignore-not-found</span>
kubectl delete <span class="nt">-f</span> print-version-green.yaml <span class="nt">--ignore-not-found</span>
kubectl delete <span class="nt">-f</span> print-version-blue.yaml <span class="nt">--ignore-not-found</span>
</code></pre></div></div>

<p>Argo Rollouts 실습을 수행했다면 Rollout Resource도 삭제한다. Controller와 CRD는 다른 Rollout에서 사용할 수 있으므로 함께 제거하지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-f</span> print-version-rollout.yaml <span class="nt">--ignore-not-found</span>
</code></pre></div></div>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>Rolling Update는 두 ReplicaSet을 점진적으로 조정하고 Blue-Green은 Service Selector로 운영 대상을 한 번에 전환한다.</p>
    </li>
    <li>
      <p>Blue-Green의 빠른 Rollback을 위해서는 이전 환경을 유지하고 두 Version의 Data 호환성을 확인해야 한다.</p>
    </li>
    <li>
      <p>Canary는 새 Version의 Traffic을 제한하여 지표를 확인한 뒤 비율을 확대한다.</p>
    </li>
    <li>
      <p>Replica 수를 이용한 Canary 비율은 근사치이며 정밀한 제어에는 별도 Traffic Router가 필요하다.</p>
    </li>
    <li>
      <p>ingress-nginx Canary Annotation은 해당 Controller 전용이며 Controller의 유지보수가 종료되어 신규 운영 구성에 사용하지 않는다.</p>
    </li>
    <li>
      <p>Argo Rollouts는 별도 CRD와 Controller가 필요한 확장 Resource이며 단계별 Weight와 Pause를 관리한다.</p>
    </li>
    <li>
      <p>Readiness Probe, Graceful Shutdown, Session, 관찰 지표와 Rollback 조건이 배포 전략의 안전성을 결정한다.</p>
    </li>
    <li>
      <p>다음 글인 <a href="/cloud-native-39-kubernetes-kustomize/">Kubernetes Kustomize로 Manifest 구성 관리</a>에서는 여러 Manifest를 조합하고 환경별 차이를 Overlay로 관리하는 방법을 다룬다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Kubernetes" /><summary type="html"><![CDATA[Service Selector를 이용한 Blue-Green 전환, Replica 비율과 Traffic Router를 이용한 Canary 및 Argo Rollouts 정리]]></summary></entry><entry><title type="html">VirtualBox Kubernetes Lab에 접속할 수 없었던 이유</title><link href="https://hyn128.site/notes-08-troubleshooting-virtualbox-bridge-network/" rel="alternate" type="text/html" title="VirtualBox Kubernetes Lab에 접속할 수 없었던 이유" /><published>2026-09-06T00:00:00+09:00</published><updated>2026-09-06T00:00:00+09:00</updated><id>https://hyn128.site/notes-08-troubleshooting-virtualbox-bridge-network</id><content type="html" xml:base="https://hyn128.site/notes-08-troubleshooting-virtualbox-bridge-network/"><![CDATA[<p>노트북에서 실행하던 Kubernetes 실습 VM을 별도의 데스크톱으로 이전하기 위해 노트북의 유선 LAN Cable을 데스크톱으로 옮겼다. 노트북은 Wi-Fi로 전환했지만 인터넷 연결부터 VirtualBox VM의 SSH 접속까지 연속해서 문제가 발생했다.</p>

<p>겉으로는 하나의 장애처럼 보였지만 Windows의 DHCP, VirtualBox의 Bridged Adapter와 Ubuntu VM의 Network 설정에서 서로 다른 문제가 이어진 상황이었다. 이 글에서는 확인된 사실과 당시의 추정을 구분하여 각 Network 계층을 어떤 순서로 확인했는지 정리한다.</p>

<h2 id="1--장애-발생-전-구성">1 ) 장애 발생 전 구성</h2>

<hr />

<p>나는 Windows 랩탑을 홈 네트워크 아래에 두고, VPN을 구성하여 외부에서 접속이 가능하도록 구성해두고 실습 시마다 사용해오고 있었다.
노트북의 VirtualBox에는 Kubernetes와 NFS 실습용 VM 네 개가 실행되고 있었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Windows Laptop
│
├── lab-control-plane  192.168.0.200
├── lab-worker1        192.168.0.201
├── lab-worker2        192.168.0.202
└── lab-nfs            192.168.0.203
</code></pre></div></div>

<p>각 VM의 Network Adapter는 Bridged Adapter로 구성되어 노트북의 Realtek 유선 NIC를 통해 <code class="language-plaintext highlighter-rouge">192.168.0.0/24</code> Network에 연결되어 있었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VM
 │
VirtualBox Bridged Adapter
 │
Realtek Ethernet NIC
 │
ipTIME Router
</code></pre></div></div>

<p>실습 VM을 장기적으로 수용할 별도의 Linux Host를 준비하면서 노트북에 연결되어 있던 LAN Cable을 데스크톱으로 옮겼다. 물리 연결은 다음과 같이 변경되었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>변경 전

Windows Laptop
      │ Ethernet
      ▼
ipTIME Router
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>변경 후

Linux Host ── Ethernet ── ipTIME Router

Windows Laptop ── Wi-Fi ── ipTIME Router
</code></pre></div></div>

<p>이 변경은 Kubernetes 설정 자체를 수정한 작업이 아니다. Host의 물리 Network 경로가 Ethernet에서 Wi-Fi로 바뀐 작업이다.</p>

<h2 id="2--장애를-세-단계로-분리">2 ) 장애를 세 단계로 분리</h2>

<hr />

<p>복구 과정에서 관찰한 증상은 다음 세 단계로 나뉘었다.</p>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>관찰한 증상</th>
      <th>확인할 계층</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Wi-Fi AP에는 연결됐지만 인터넷을 사용할 수 없음</td>
      <td>Windows IP·DHCP</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Windows 인터넷은 복구됐지만 Host에서 VM으로 Ping·SSH 실패</td>
      <td>Routing·VirtualBox Bridge</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Bridge 설정 변경 후 일부 VM의 NIC는 존재하지만 <code class="language-plaintext highlighter-rouge">DOWN</code> 상태</td>
      <td>Guest NIC·Netplan</td>
    </tr>
  </tbody>
</table>

<p>단계별 장애를 구분하지 않으면 Windows DHCP 문제를 해결한 뒤에도 VM 접속 실패를 같은 원인으로 오해하기 쉽다.</p>

<h2 id="3--wi-fi-연결과-ip-할당을-분리해서-확인">3 ) Wi-Fi 연결과 IP 할당을 분리해서 확인</h2>

<hr />

<p>노트북은 Wi-Fi 이름과 신호 세기를 정상적으로 표시했지만 인터넷에 연결되지 않았다. 먼저 Windows에서 사용 중인 Adapter의 IP 설정을 확인했다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ipconfig</span><span class="w"> </span><span class="nx">/all</span><span class="w">
</span></code></pre></div></div>

<p>Wi-Fi Adapter에는 다음과 같은 값이 표시되었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DHCP 사용           : 예
자동 구성 IPv4 주소 : 169.254.100.110
서브넷 마스크       : 255.255.0.0
기본 게이트웨이     : 없음
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">169.254.0.0/16</code>은 Windows가 DHCP Server에서 IPv4 설정을 받지 못했을 때 사용하는 APIPA(Automatic Private IP Addressing) 범위이다. 이 주소와 Gateway가 없다는 결과로 다음 상태를 확인할 수 있었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Wi-Fi Association   성공
        │
        ▼
DHCP 주소 할당      실패
        │
        ▼
169.254.x.x 자동 구성
        │
        ▼
기본 Gateway 없음
</code></pre></div></div>

<p>Wi-Fi의 무선 연결 상태는 별도로 확인했다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">netsh</span><span class="w"> </span><span class="nx">wlan</span><span class="w"> </span><span class="nx">show</span><span class="w"> </span><span class="nx">interfaces</span><span class="w">
</span></code></pre></div></div>

<p>확인 결과 SSID, 신호 세기와 연결 상태는 정상으로 표시되었다. 따라서 이 시점의 문제는 AP와의 Association이 아니라 IPv4 설정을 받지 못한 상태였다.</p>

<h3 id="dhcp-lease-재요청">DHCP Lease 재요청</h3>

<p>처음에는 DHCP 주소를 반납하고 다시 요청했다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ipconfig</span><span class="w"> </span><span class="nx">/release</span><span class="w">
</span><span class="n">ipconfig</span><span class="w"> </span><span class="nx">/renew</span><span class="w">
</span></code></pre></div></div>

<p>유선 LAN Cable이 제거된 Ethernet Adapter에서는 다음 메시지가 함께 출력되었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>미디어의 연결이 끊긴 상태에서는 이더넷에서 작업을 수행할 수 없습니다.
</code></pre></div></div>

<p>이 메시지는 Cable이 연결되지 않은 Ethernet Adapter에 대한 결과이다. Wi-Fi의 DHCP 처리 결과와 구분해야 한다. Wi-Fi만 대상으로 실행하려면 Adapter 이름을 지정한다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ipconfig</span><span class="w"> </span><span class="nx">/release</span><span class="w"> </span><span class="s2">"Wi-Fi"</span><span class="w">
</span><span class="n">ipconfig</span><span class="w"> </span><span class="nx">/renew</span><span class="w"> </span><span class="s2">"Wi-Fi"</span><span class="w">
</span></code></pre></div></div>

<p>Wi-Fi 갱신 과정에서는 다음 오류가 발생했다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>인터페이스 갱신하는 동안 오류 발생:
개체가 이미 있음
</code></pre></div></div>

<p>이 메시지만으로 중복된 IP Address나 Route가 정확한 원인이라고 확정할 수는 없다. 당시에는 Windows TCP/IP 상태 이상을 의심하고 현재 설정을 먼저 확인했다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-NetIPConfiguration</span><span class="w"> </span><span class="nt">-InterfaceAlias</span><span class="w"> </span><span class="s2">"Wi-Fi"</span><span class="w">
</span><span class="n">Get-NetIPAddress</span><span class="w"> </span><span class="nt">-InterfaceAlias</span><span class="w"> </span><span class="s2">"Wi-Fi"</span><span class="w">
</span><span class="n">Get-NetRoute</span><span class="w"> </span><span class="nt">-InterfaceAlias</span><span class="w"> </span><span class="s2">"Wi-Fi"</span><span class="w">
</span></code></pre></div></div>

<h3 id="windows-network-설정-초기화">Windows Network 설정 초기화</h3>

<p>일반적인 DHCP 재요청으로 복구되지 않아 관리자 권한 Terminal에서 Winsock Catalog와 TCP/IP 설정을 초기화했다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">netsh</span><span class="w"> </span><span class="nx">winsock</span><span class="w"> </span><span class="nx">reset</span><span class="w">
</span><span class="n">netsh</span><span class="w"> </span><span class="nx">int</span><span class="w"> </span><span class="nx">ip</span><span class="w"> </span><span class="nx">reset</span><span class="w">
</span></code></pre></div></div>

<p>이 명령은 Network 설정에 영향을 준다. 기존 수동 IP, DNS나 별도 Network 구성이 있다면 먼저 기록하고, 다른 원인 확인 없이 첫 단계에서 실행하지 않는다. 적용을 완료하기 위해 Windows를 재부팅했다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">shutdown</span><span class="w"> </span><span class="nx">/r</span><span class="w"> </span><span class="nx">/t</span><span class="w"> </span><span class="nx">0</span><span class="w">
</span></code></pre></div></div>

<p>재부팅 후 <code class="language-plaintext highlighter-rouge">ipconfig /all</code>에서 다음 상태를 확인했다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IPv4 주소       : 192.168.0.x
서브넷 마스크   : 255.255.255.0
기본 게이트웨이 : 192.168.0.1
DHCP 사용       : 예
</code></pre></div></div>

<p>Wi-Fi 인터넷은 복구되었다. 다만 DHCP 응답을 받지 못한 최초 원인은 Packet Capture나 Windows Event 등으로 확인하지 못했다. 따라서 “Windows Network 초기화와 재부팅 후 복구됨”은 확인된 사실이지만, 특정 IP Object나 Route가 근본 원인이었다고 단정하지 않는다.</p>

<h2 id="4--인터넷-복구와-vm-접속-복구는-별개였다">4 ) 인터넷 복구와 VM 접속 복구는 별개였다</h2>

<hr />

<p>Windows의 Wi-Fi 인터넷이 복구된 뒤에도 외부 SSH 에서 VirtualBox VM으로 접속할 수 없었다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ping</span><span class="w"> </span><span class="nx">192.168.0.200</span><span class="w">
</span><span class="n">ping</span><span class="w"> </span><span class="nx">192.168.0.201</span><span class="w">
</span><span class="n">ssh</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">user</span><span class="err">&gt;@</span><span class="nx">192.168.0.200</span><span class="w">
</span></code></pre></div></div>

<p>반면 VM 사이의 통신은 유지되고 있었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lab-control-plane ↔ lab-worker1   정상
lab-control-plane ↔ lab-worker2   정상
worker            ↔ lab-nfs       정상
Windows Host       → VM            실패
</code></pre></div></div>

<p>이 결과는 Kubernetes Pod Network 전체가 중단됐다는 의미가 아니다. VM 내부 경로는 동작하고 있었고 Windows Host에서 VM으로 가는 경로만 실패했다. 따라서 Kubernetes Resource보다 먼저 Host Routing과 VirtualBox Network 연결을 확인했다.</p>

<p>Windows Routing Table을 조회했다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route</span><span class="w"> </span><span class="nx">print</span><span class="w">
</span></code></pre></div></div>

<p>주요 결과는 다음과 같았다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0.0.0.0/0       → 192.168.0.1   Interface 192.168.0.99
192.168.0.0/24  → 연결됨        Interface 192.168.0.99

VirtualBox Host-Only Adapter
169.254.70.191/16
</code></pre></div></div>

<p>Windows는 VM의 <code class="language-plaintext highlighter-rouge">192.168.0.20x</code> 주소를 Wi-Fi와 같은 Local Network의 주소로 판단하고 있었다. Host-Only Adapter는 해당 대역을 담당하지 않았다. 이 결과만으로 VirtualBox 설정을 확정할 수는 없지만, VM이 어떤 Host Interface에 Bridge되어 있는지 확인해야 한다는 범위를 정할 수 있었다.</p>

<h2 id="5--확정-원인은-bridged-adapter의-대상-nic였다">5 ) 확정 원인은 Bridged Adapter의 대상 NIC였다</h2>

<hr />

<p>VM을 종료한 뒤 VirtualBox의 다음 설정을 확인했다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VM 설정
└── Network
    └── Adapter 1
        ├── Attached to : Bridged Adapter
        └── Name        : Realtek PCIe GbE Family Controller
</code></pre></div></div>

<p>Bridged Adapter의 대상이 LAN Cable을 제거한 Realtek Ethernet NIC로 남아 있었다. 장애 전에는 이 경로가 유효했다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VM
 │
VirtualBox Bridge
 │
Realtek Ethernet
 │
LAN Cable
 │
Router
</code></pre></div></div>

<p>Cable을 데스크톱으로 옮긴 뒤에는 같은 설정이 물리적으로 끊긴 Interface를 가리켰다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VM
 │
VirtualBox Bridge
 │
Realtek Ethernet
 │
Cable 없음
 ╳
</code></pre></div></div>

<p>VirtualBox의 Bridged Adapter는 사용자가 선택한 Host Network Interface를 통해 Guest Traffic을 전달한다. Windows가 Wi-Fi로 인터넷에 연결되었다고 해서 기존 VM의 Bridge 대상도 자동으로 Wi-Fi NIC로 변경되는 것은 아니었다.</p>

<p>각 VM의 Bridge 대상 <code class="language-plaintext highlighter-rouge">Name</code>을 현재 연결된 Wi-Fi Adapter로 변경했다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>기존: Realtek PCIe GbE Family Controller
변경: Intel(R) Wi-Fi 6 AX201 160MHz
</code></pre></div></div>

<p>이 단계에서는 동시에 여러 설정을 바꾸지 않는 것이 중요하다. 우선 다음 값은 유지하고 Bridge 대상만 변경해야 원인과 결과를 분리할 수 있다.</p>

<ul>
  <li>
    <p>Adapter Type</p>
  </li>
  <li>
    <p>MAC Address</p>
  </li>
  <li>
    <p>Promiscuous Mode</p>
  </li>
  <li>
    <p>Cable Connected 상태</p>
  </li>
</ul>

<p>Wi-Fi Bridging은 Host OS, Wi-Fi Driver와 Access Point 구성에 따라 Ethernet Bridging과 다르게 동작하거나 제한될 수 있다. 따라서 Bridge 대상을 Wi-Fi NIC로 변경했다는 사실만으로 모든 환경에서 같은 결과가 보장되지는 않는다.</p>

<h2 id="6--가상-nic-변경-후-발생한-두-번째-문제">6 ) 가상 NIC 변경 후 발생한 두 번째 문제</h2>

<hr />

<p>Bridge 대상을 변경하는 과정에서 Guest에 보여줄 Adapter Type도 <code class="language-plaintext highlighter-rouge">virtio-net</code>으로 변경했다. 이후 <code class="language-plaintext highlighter-rouge">lab-nfs</code>를 제외한 일부 Ubuntu VM에서 Network가 올라오지 않았다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip addr
</code></pre></div></div>

<p>다음 상태가 확인되었다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2: enp0s3: &lt;BROADCAST,MULTICAST&gt;
    state DOWN
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">enp0s3</code> Interface는 존재했다. 처음엔 “NIC가 유실됐네” 라고 생각했는데, 다음 상태로 표현하는 것이 정확하다고 한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NIC 존재
   │
   ▼
Link DOWN
   │
   ▼
IPv4 Address 없음
</code></pre></div></div>

<p>Adapter Type 변경이 직접 원인이었는지, Netplan 설정이 적용되지 않은 이유가 무엇인지는 당시 기록해두었던 자료만으로 확정할 수 없었다. 초반 이슈 이후로는 Interface 이름이 계속 <code class="language-plaintext highlighter-rouge">enp0s3</code>로 확인됐고 변경 전후 Netplan과 System Log를 모두 보존하지 못했기 때문이다.</p>

<h3 id="interface와-netplan-확인">Interface와 Netplan 확인</h3>

<p>먼저 한 VM에서 Interface를 임시로 활성화했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ip <span class="nb">link set </span>enp0s3 up
ip <span class="nb">link </span>show enp0s3
ip addr show enp0s3
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ip link set</code>으로 변경한 상태는 재부팅 후에도 유지되는 영구 Network 설정이 아니다. NIC를 올릴 수 있는지 확인한 뒤 Netplan을 점검했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> <span class="nt">-l</span> /etc/netplan/
<span class="nb">sudo cat</span> /etc/netplan/<span class="k">*</span>.yaml
</code></pre></div></div>

<p>Netplan의 Interface 이름과 <code class="language-plaintext highlighter-rouge">ip addr</code>에서 확인한 실제 이름이 같은지 확인했다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">network</span><span class="pi">:</span>
  <span class="na">version</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">ethernets</span><span class="pi">:</span>
    <span class="na">enp0s3</span><span class="pi">:</span>
      <span class="na">dhcp4</span><span class="pi">:</span> <span class="no">false</span>
      <span class="na">addresses</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">192.168.0.200/24</span>
      <span class="na">routes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">to</span><span class="pi">:</span> <span class="s">default</span>
          <span class="na">via</span><span class="pi">:</span> <span class="s">192.168.0.1</span>
      <span class="na">nameservers</span><span class="pi">:</span>
        <span class="na">addresses</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">8.8.8.8</span>
          <span class="pi">-</span> <span class="s">1.1.1.1</span>
</code></pre></div></div>

<p>각 VM에는 해당 Node에 할당한 IP Address를 사용한다.</p>

<table>
  <thead>
    <tr>
      <th>VM</th>
      <th>Address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lab-control-plane</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.0.200/24</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lab-worker1</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.0.201/24</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lab-worker2</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.0.202/24</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lab-nfs</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.0.203/24</code></td>
    </tr>
  </tbody>
</table>

<p>여러 Netplan YAML File이 존재하면 설정이 함께 처리되므로 같은 Interface를 중복 정의하거나 서로 다른 값을 적용하는 File이 있는지도 확인해야 한다.</p>

<p>설정의 YAML 문법과 생성 결과를 먼저 검사했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>netplan generate
</code></pre></div></div>

<p>Network를 변경할 때는 가능하면 다음 명령으로 임시 적용하여 <strong>연결이 끊기면 이전 설정으로 돌아갈 수 있게 한다</strong>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>netplan try
</code></pre></div></div>

<p>확인 후 설정을 적용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>netplan apply
</code></pre></div></div>

<p>SSH로 접속한 상태에서 Network를 변경하면 연결을 잃을 수 있다. 이번과 같이 Guest Network 자체가 동작하지 않는 상황에서는 VirtualBox Console에서 작업하는 것이 안전하다.</p>

<p>적용 후 Interface, Route와 통신을 순서대로 확인했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip addr show enp0s3
ip route
ping <span class="nt">-c</span> 3 192.168.0.1
ping <span class="nt">-c</span> 3 192.168.0.99
</code></pre></div></div>

<p>Netplan을 다시 적용한 뒤 각 VM의 고정 IP가 복구되었고 외부 네트워크에서 VPN에 접속된 Mac의 SSH와 Windows Host에서이 VM으로 Ping과 SSH가 가능해졌다.</p>

<h2 id="7--확인된-사실과-추정을-구분">7 ) 확인된 사실과 추정을 구분</h2>

<hr />

<p>복구됐다는 사실만으로 사용한 명령이 곧 근본 원인을 입증하는 것은 아니지만, 네트워크 구성이 변경될 때마다 비슷한 장애가 발생해왔었고, 다시 기억할 수 있도록 이번 장애의 판단 수준을 구분하면 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>판단</th>
      <th>근거</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Windows가 정상 DHCP 주소를 받지 못함</td>
      <td>확인됨</td>
      <td><code class="language-plaintext highlighter-rouge">169.254.x.x</code>, Gateway 없음</td>
    </tr>
    <tr>
      <td>Wi-Fi Association 자체는 정상</td>
      <td>확인됨</td>
      <td><code class="language-plaintext highlighter-rouge">netsh wlan show interfaces</code>의 연결 상태</td>
    </tr>
    <tr>
      <td>Windows TCP/IP 내부 Object 충돌이 근본 원인</td>
      <td>미확정</td>
      <td>초기화 후 복구됐지만 직접 원인을 확인하지 못함</td>
    </tr>
    <tr>
      <td>VM Bridge가 연결이 끊긴 Ethernet NIC를 참조함</td>
      <td>확인됨</td>
      <td>VirtualBox VM Network 설정에서 Realtek NIC 확인</td>
    </tr>
    <tr>
      <td>해당 Bridge 설정이 Host에서 VM으로 접근하지 못한 원인</td>
      <td>확인됨</td>
      <td>물리 연결 상태와 Bridge 대상이 불일치했고 변경 후 경로 복구</td>
    </tr>
    <tr>
      <td>Ubuntu VM의 <code class="language-plaintext highlighter-rouge">enp0s3</code>가 존재하지만 <code class="language-plaintext highlighter-rouge">DOWN</code> 상태</td>
      <td>확인됨</td>
      <td>VM의 <code class="language-plaintext highlighter-rouge">ip addr</code> 결과</td>
    </tr>
    <tr>
      <td>Adapter Type 변경 후 Netplan 장애 발생</td>
      <td>확인됨</td>
      <td>Adapter에 따라 netplan에서 인터페이스를 재할당하여 복구</td>
    </tr>
    <tr>
      <td>Guest Network 설정을 확인하고 다시 적용한 뒤 복구</td>
      <td>확인됨</td>
      <td>고정 IP, Ping과 SSH 복구</td>
    </tr>
  </tbody>
</table>

<p>가장 명확했던 원인은 VirtualBox Bridge가 물리적으로 연결되지 않은 Host NIC를 계속 참조했다는 점이다.</p>

<h2 id="8--계층별-진단-순서">8 ) 계층별 진단 순서</h2>

<hr />

<p>“통신이 안 된다”는 증상을 하나의 문제로 다루지 않고 아래에서 위로 확인했다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Wi-Fi Association
        │
        ▼
IP Address와 DHCP
        │
        ▼
Default Gateway와 Host Route
        │
        ▼
VirtualBox Attachment Mode
        │
        ▼
Bridge 대상 Host NIC
        │
        ▼
Guest NIC Link 상태
        │
        ▼
Netplan Address와 Route
        │
        ▼
Ping과 SSH
</code></pre></div></div>

<p>각 단계의 판단 기준은 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>확인 결과</th>
      <th>다음으로 볼 대상</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Wi-Fi가 연결되지 않음</td>
      <td>Wi-Fi Profile, 인증과 Adapter 상태</td>
    </tr>
    <tr>
      <td>Wi-Fi 연결, <code class="language-plaintext highlighter-rouge">169.254.x.x</code>, Gateway 없음</td>
      <td>DHCP와 Windows IP 설정</td>
    </tr>
    <tr>
      <td>ipTIME 의 DHCP 충돌은 아니었음</td>
      <td>ipTIME DHCP 서버 관리 및 내부 네트워크 설정</td>
    </tr>
    <tr>
      <td>Host 인터넷 정상, Host에서 모든 VM 접근 실패</td>
      <td>VirtualBox Attachment와 Bridge 대상</td>
    </tr>
    <tr>
      <td>VM끼리 통신 가능, Host에서 VM만 실패</td>
      <td>Host와 VM 사이의 VirtualBox 경로</td>
    </tr>
    <tr>
      <td>Guest NIC가 보이지 않음</td>
      <td>가상 NIC 연결 여부와 Guest Driver</td>
    </tr>
    <tr>
      <td>Guest NIC가 존재하지만 <code class="language-plaintext highlighter-rouge">DOWN</code></td>
      <td>Link 상태와 Guest Network 설정</td>
    </tr>
    <tr>
      <td>IP는 있지만 Gateway Ping 실패</td>
      <td>Subnet, Route, Bridge와 물리 Network</td>
    </tr>
    <tr>
      <td>Ping 성공, SSH만 실패</td>
      <td>SSH Service, Port와 Firewall</td>
    </tr>
  </tbody>
</table>

<p>이 순서를 사용하면 DHCP 장애 상태에서 SSH 설정을 바꾸거나, Guest NIC가 <code class="language-plaintext highlighter-rouge">DOWN</code>인 상태에서 Kubernetes 설정을 먼저 수정하는 일을 피할 수 있을 것 같다.</p>

<h2 id="9--재발-방지-기준">9 ) 재발 방지 기준</h2>

<hr />

<h3 id="host의-물리-interface가-바뀌면-bridge-대상을-확인한다">Host의 물리 Interface가 바뀌면 Bridge 대상을 확인한다</h3>

<p>Bridged Adapter는 특정 Host Interface에 연결된다. Ethernet에서 Wi-Fi로 전환하거나 USB Ethernet Adapter를 교체했다면 모든 VM의 Bridge 대상을 확인한다.</p>

<h3 id="한-번에-하나의-설정만-변경한다">한 번에 하나의 설정만 변경한다</h3>

<p>Bridge 대상과 Adapter Type을 동시에 변경하면 어떤 변경이 장애를 만들었는지 분리하기 어렵다. 다음 순서로 하나씩 변경하고 매 단계에서 통신을 확인한다.</p>

<ol>
  <li>
    <p>Bridge 대상 Host NIC 변경</p>
  </li>
  <li>
    <p>Guest 부팅 후 NIC와 IP 확인</p>
  </li>
  <li>
    <p>Gateway와 Host 통신 확인</p>
  </li>
  <li>
    <p>필요한 경우에만 Adapter Type 변경</p>
  </li>
  <li>
    <p>다시 Guest NIC와 Netplan 확인</p>
  </li>
</ol>

<h3 id="mac-address를-불필요하게-변경하지-않는다">MAC Address를 불필요하게 변경하지 않는다</h3>

<p>DHCP Reservation이나 Network 접근 정책에서 MAC Address를 사용할 수 있다. Bridge 대상만 변경하는 작업에서는 Guest MAC Address를 유지한다.</p>

<h3 id="network-변경-전-현재-상태를-기록한다">Network 변경 전 현재 상태를 기록한다</h3>

<p>Windows에서는 다음 결과를 보존한다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ipconfig</span><span class="w"> </span><span class="nx">/all</span><span class="w">
</span><span class="n">route</span><span class="w"> </span><span class="nx">print</span><span class="w">
</span><span class="n">Get-NetIPConfiguration</span><span class="w">
</span></code></pre></div></div>

<p>Ubuntu VM에서는 다음 결과를 보존한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip addr
ip route
<span class="nb">sudo cat</span> /etc/netplan/<span class="k">*</span>.yaml
</code></pre></div></div>

<p>변경 전후 결과가 있으면 복구 여부뿐 아니라 정확한 원인까지 확인할 수 있다.</p>

<h3 id="netplan은-console과-rollback을-준비한다">Netplan은 Console과 Rollback을 준비한다</h3>

<p>Remote 접속 중 Network를 변경해야 한다면 <code class="language-plaintext highlighter-rouge">netplan try</code>를 우선 사용한다. Bridge, IP와 Route를 변경하는 작업은 연결이 끊길 수 있으므로 가능한 경우 Local Console이나 VirtualBox Console을 확보한다.</p>

<h2 id="10--최종-확인">10 ) 최종 확인</h2>

<hr />

<p>각 계층의 복구 여부를 다음 순서로 확인했다.</p>

<h3 id="windows-host">Windows Host</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ipconfig</span><span class="w"> </span><span class="nx">/all</span><span class="w">
</span><span class="n">ping</span><span class="w"> </span><span class="nx">192.168.0.1</span><span class="w">
</span><span class="n">ping</span><span class="w"> </span><span class="nx">192.168.0.200</span><span class="w">
</span><span class="n">ssh</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">user</span><span class="err">&gt;@</span><span class="nx">192.168.0.200</span><span class="w">
</span></code></pre></div></div>

<h3 id="ubuntu-vm">Ubuntu VM</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip addr show enp0s3
ip route
ping <span class="nt">-c</span> 3 192.168.0.1
ping <span class="nt">-c</span> 3 192.168.0.99
</code></pre></div></div>

<p>확인 결과는 다음과 같았다.</p>

<table>
  <thead>
    <tr>
      <th>대상</th>
      <th>결과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Windows Wi-Fi의 DHCP Address와 Default Gateway</td>
      <td>복구</td>
    </tr>
    <tr>
      <td>Windows 인터넷 연결</td>
      <td>복구</td>
    </tr>
    <tr>
      <td>Windows Host에서 VM Ping</td>
      <td>복구</td>
    </tr>
    <tr>
      <td>Windows Host에서 VM SSH</td>
      <td>복구</td>
    </tr>
    <tr>
      <td>VM 사이 통신</td>
      <td>정상</td>
    </tr>
    <tr>
      <td>Ubuntu VM의 고정 IP와 Default Route</td>
      <td>복구</td>
    </tr>
  </tbody>
</table>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p><code class="language-plaintext highlighter-rouge">169.254.x.x</code> 주소와 Gateway 부재는 Wi-Fi 연결 여부가 아니라 DHCP 주소 할당부터 확인해야 한다는 단서였다.</p>
    </li>
    <li>
      <p>Windows 인터넷이 복구되어도 VirtualBox VM의 Network 경로가 자동으로 복구되는 것은 아니다.</p>
    </li>
    <li>
      <p>VirtualBox VM은 연결이 끊긴 Realtek Ethernet NIC에 계속 Bridge되어 있었으며, 이것이 Host에서 VM으로 접근할 수 없었던 확정 원인이었다.</p>
    </li>
    <li>
      <p>Guest의 <code class="language-plaintext highlighter-rouge">enp0s3</code>는 유실된 것이 아니라 존재하지만 <code class="language-plaintext highlighter-rouge">DOWN</code> 상태였고, Interface와 Netplan을 확인하고 다시 적용한 뒤 고정 IP가 복구되었다.</p>
    </li>
    <li>
      <p>Network 장애는 Association, DHCP, Route, VirtualBox Bridge, Guest NIC, Netplan과 Application 순서로 계층을 나누어 확인한다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="TroubleShooting" /><category term="Network" /><summary type="html"><![CDATA[노트북의 연결을 유선에서 Wi-Fi로 전환한 뒤 발생한 Windows DHCP, VirtualBox Bridged Adapter와 Ubuntu Netplan 문제를 계층별로 추적한 기록]]></summary></entry><entry><title type="html">Kubernetes Health Check와 restartPolicy</title><link href="https://hyn128.site/cloud-native-37-kubernetes-health-check-restart-policy/" rel="alternate" type="text/html" title="Kubernetes Health Check와 restartPolicy" /><published>2026-09-04T00:00:00+09:00</published><updated>2026-09-04T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-37-kubernetes-health-check-restart-policy</id><content type="html" xml:base="https://hyn128.site/cloud-native-37-kubernetes-health-check-restart-policy/"><![CDATA[<p>Kubernetes는 Process가 실행 중이라는 사실만으로 Application이 정상이라고 판단하지 않는다. Worker의 kubelet은 Probe를 실행하여 Container의 생존 여부, 요청 처리 가능 여부와 시작 완료 여부를 확인한다. Probe 결과와 <code class="language-plaintext highlighter-rouge">restartPolicy</code>를 함께 이해하면 Container가 재시작되는 경우와 Service Traffic에서만 제외되는 경우를 구분할 수 있다.</p>

<h2 id="1--health-check와-pod-상태">1 ) Health Check와 Pod 상태</h2>

<hr />

<p>Probe는 Pod 전체가 아니라 Pod 안의 각 Container에 설정한다. 같은 Container에도 목적이 다른 Probe를 함께 설정할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>Probe</th>
      <th>확인 대상</th>
      <th>실패가 계속될 때의 결과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Liveness Probe</td>
      <td>Container가 계속 정상 동작할 수 있는가</td>
      <td>kubelet이 해당 Container를 재시작함</td>
    </tr>
    <tr>
      <td>Readiness Probe</td>
      <td>현재 요청을 처리할 준비가 되었는가</td>
      <td>Container는 유지하고 Pod를 Service Endpoint에서 제외함</td>
    </tr>
    <tr>
      <td>Startup Probe</td>
      <td>Application의 최초 시작이 끝났는가</td>
      <td>성공할 때까지 Liveness·Readiness Probe를 보류하고, 계속 실패하면 Container를 재시작함</td>
    </tr>
  </tbody>
</table>

<p>Liveness Probe는 Process가 살아 있지만 Deadlock이나 Memory Leak 등의 문제로 정상 응답하지 못하고, 재시작 없이는 회복하기 어려운 상태를 감지할 때 사용한다.</p>

<p>Readiness Probe는 Database 연결, Cache Load, 초기 Data 준비처럼 요청 처리에 필요한 조건을 확인한다. 일시적으로 실패해도 Container를 재시작하지 않으며, 다시 성공하면 Service Endpoint에 포함될 수 있다.</p>

<p>Startup Probe는 시작 시간이 긴 Application을 보호한다. Startup Probe가 성공하기 전에는 Liveness·Readiness Probe가 실행되지 않으므로, 시작 중인 Container가 Liveness Probe 실패로 반복 재시작되는 상황을 방지할 수 있다.</p>

<p>외부 Load Balancer의 Health Check와 kubelet의 Probe는 별개의 검사이다. Load Balancer의 검사 방식과 대상은 Cloud Provider와 구성에 따라 달라지며, 외부 검사만으로 Container 내부의 생존 상태와 준비 상태를 모두 판단할 수는 없다.</p>

<h2 id="2--control-plane과-worker의-상태-처리">2 ) Control Plane과 Worker의 상태 처리</h2>

<hr />

<p>Probe 실행과 상태 반영 과정은 다음과 같다.</p>

<ol>
  <li>
    <p>사용자가 Pod Spec에 Container별 Probe를 선언하여 API Server에 저장한다.</p>
  </li>
  <li>
    <p>Scheduler가 Pod를 실행할 Worker를 선택한다.</p>
  </li>
  <li>
    <p>Worker의 kubelet이 Pod Spec에 따라 Probe를 주기적으로 실행한다.</p>
  </li>
  <li>
    <p>Liveness·Startup Probe가 실패 기준에 도달하면 kubelet이 해당 Container를 종료하고 <code class="language-plaintext highlighter-rouge">restartPolicy</code>에 따라 다시 시작한다.</p>
  </li>
  <li>
    <p>Readiness Probe 결과는 Pod의 <code class="language-plaintext highlighter-rouge">Ready</code>와 <code class="language-plaintext highlighter-rouge">ContainersReady</code> Condition에 반영되어 API Server로 보고된다.</p>
  </li>
  <li>
    <p>EndpointSlice Controller는 Ready 상태를 기준으로 Service가 요청을 전달할 Endpoint를 갱신한다.</p>
  </li>
</ol>

<p>따라서 Liveness 실패는 주로 Worker 안의 Container Lifecycle 변화로 나타나고, Readiness 실패는 Control Plane에 보고된 Pod Condition과 Service Endpoint 변화로 나타난다.</p>

<p><a href="/cloud-native-24-pod-lifecycle-kubectl-wait/">Kubernetes Pod Lifecycle과 kubectl wait</a>에서 정리한 <code class="language-plaintext highlighter-rouge">Running</code> Phase는 Container가 실행 중임을 나타낸다. Application이 Traffic을 받을 준비가 되었는지는 <code class="language-plaintext highlighter-rouge">Ready</code> Condition과 Readiness Probe를 함께 확인해야 한다.</p>

<h2 id="3--probe-실행-방식">3 ) Probe 실행 방식</h2>

<hr />

<h3 id="exec">exec</h3>

<p>Container 안에서 명령을 실행한다. 종료 코드가 <code class="language-plaintext highlighter-rouge">0</code>이면 성공이고, 그 외의 값이면 실패이다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livenessProbe</span><span class="pi">:</span>
  <span class="na">exec</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">test</span>
      <span class="pi">-</span> <span class="s">-e</span>
      <span class="pi">-</span> <span class="s">/ok.txt</span>
</code></pre></div></div>

<p>Application 상태를 검증하는 명령이 Container Image 안에 존재해야 한다.</p>

<h3 id="httpget">httpGet</h3>

<p>kubelet이 지정한 Path와 Port로 HTTP GET 요청을 보낸다. 응답 Status Code가 <code class="language-plaintext highlighter-rouge">200</code> 이상 <code class="language-plaintext highlighter-rouge">400</code> 미만이면 성공으로 판단한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livenessProbe</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/health</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
    <span class="na">scheme</span><span class="pi">:</span> <span class="s">HTTP</span>
    <span class="na">httpHeaders</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Host</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s">web.example.com</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Authorization</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s">Bearer REPLACE_WITH_TOKEN</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">host</code> Field는 접속할 대상 Host를 지정한다. HTTP <code class="language-plaintext highlighter-rouge">Host</code> Header가 필요하면 <code class="language-plaintext highlighter-rouge">httpHeaders</code>에 지정한다. 인증 정보는 Manifest에 직접 저장하지 않고 실제 환경의 Secret 관리 방식을 사용해야 한다.</p>

<h3 id="tcpsocket">tcpSocket</h3>

<p>kubelet이 지정한 Port에 TCP Connection을 열 수 있는지 확인한다. Connection이 성립하면 성공이고 열 수 없으면 실패이다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livenessProbe</span><span class="pi">:</span>
  <span class="na">tcpSocket</span><span class="pi">:</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>TCP Probe는 Port가 열려 있는지는 확인할 수 있지만 Application이 요청을 올바르게 처리하는지까지 검증하지는 않는다.</p>

<h2 id="4--probe-주기와-실패-판정">4 ) Probe 주기와 실패 판정</h2>

<hr />

<p>세 Probe는 공통적으로 다음 시간 관련 Field를 사용한다.</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">initialDelaySeconds</code></td>
      <td>Container 시작 후 첫 Probe까지 기다리는 시간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">periodSeconds</code></td>
      <td>Probe를 반복하는 간격</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">timeoutSeconds</code></td>
      <td>한 번의 Probe 응답을 기다리는 제한 시간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">successThreshold</code></td>
      <td>실패 상태에서 성공으로 판단하기 위해 필요한 연속 성공 횟수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">failureThreshold</code></td>
      <td>실패 동작을 수행하기 위해 필요한 연속 실패 횟수</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">successThreshold</code>는 1 이상이어야 한다. Liveness·Startup Probe에서는 반드시 <code class="language-plaintext highlighter-rouge">1</code>이어야 하며 Readiness Probe에는 더 큰 값을 사용할 수 있다.</p>

<p>Liveness Probe의 <code class="language-plaintext highlighter-rouge">failureThreshold</code>가 너무 작으면 일시적인 지연에도 Container가 재시작될 수 있다. 반대로 너무 크면 실제 장애 감지가 늦어진다. 시작 시간 때문에 Liveness Probe의 <code class="language-plaintext highlighter-rouge">initialDelaySeconds</code>를 과도하게 늘리는 대신 Startup Probe로 최초 시작 구간을 분리할 수 있다.</p>

<h2 id="5--liveness와-readiness-probe-적용">5 ) Liveness와 Readiness Probe 적용</h2>

<hr />

<p>다음 예제는 nginx의 <code class="language-plaintext highlighter-rouge">/index.html</code>을 Liveness Probe로 확인하고, <code class="language-plaintext highlighter-rouge">/usr/share/nginx/html/50x.html</code>의 존재 여부를 Readiness Probe로 확인한다. <code class="language-plaintext highlighter-rouge">sample-healthcheck.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-healthcheck</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">sample-healthcheck</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">livenessProbe</span><span class="pi">:</span>
        <span class="na">httpGet</span><span class="pi">:</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s">/index.html</span>
          <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
          <span class="na">scheme</span><span class="pi">:</span> <span class="s">HTTP</span>
        <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">3</span>
        <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">successThreshold</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">2</span>
      <span class="na">readinessProbe</span><span class="pi">:</span>
        <span class="na">exec</span><span class="pi">:</span>
          <span class="na">command</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">test</span>
            <span class="pi">-</span> <span class="s">-e</span>
            <span class="pi">-</span> <span class="s">/usr/share/nginx/html/50x.html</span>
        <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">3</span>
        <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">successThreshold</span><span class="pi">:</span> <span class="m">2</span>
        <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">1</span>
</code></pre></div></div>

<p>Pod를 생성하고 두 Probe의 결과를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-healthcheck.yaml
kubectl <span class="nb">wait</span> <span class="se">\</span>
  <span class="nt">--for</span><span class="o">=</span><span class="nv">condition</span><span class="o">=</span>Ready <span class="se">\</span>
  pod/sample-healthcheck <span class="se">\</span>
  <span class="nt">--timeout</span><span class="o">=</span>60s
kubectl describe pod sample-healthcheck
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">kubectl describe</code>의 <code class="language-plaintext highlighter-rouge">Containers</code> 영역에서 Liveness·Readiness 설정을 확인하고 <code class="language-plaintext highlighter-rouge">Conditions</code> 영역에서 <code class="language-plaintext highlighter-rouge">Ready</code>와 <code class="language-plaintext highlighter-rouge">ContainersReady</code> 값을 확인한다.</p>

<h2 id="6--liveness-probe-실패">6 ) Liveness Probe 실패</h2>

<hr />

<p>Liveness Probe 실패가 Container 재시작으로 이어지는지 확인한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">sample-liveness.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-liveness</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">livenessProbe</span><span class="pi">:</span>
        <span class="na">httpGet</span><span class="pi">:</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s">/index.html</span>
          <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
          <span class="na">scheme</span><span class="pi">:</span> <span class="s">HTTP</span>
        <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">3</span>
        <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">successThreshold</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">2</span>
</code></pre></div></div>

<p>Pod를 생성하고 변화 과정을 감시한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-liveness.yaml
kubectl get pod sample-liveness <span class="nt">--watch</span>
</code></pre></div></div>

<p>다른 터미널에서 Liveness Probe가 확인하는 File을 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec </span>sample-liveness <span class="nt">--</span> <span class="se">\</span>
  <span class="nb">rm</span> <span class="nt">-f</span> /usr/share/nginx/html/index.html
</code></pre></div></div>

<p>연속 실패 횟수가 <code class="language-plaintext highlighter-rouge">failureThreshold</code>에 도달하면 kubelet이 nginx Container를 재시작한다. Pod Object가 새로 만들어지는 것은 아니므로 Pod 이름과 UID는 유지되고 <code class="language-plaintext highlighter-rouge">RESTARTS</code>가 증가한다. Container가 재시작되면 Image의 File System으로 다시 시작하므로 삭제했던 기본 <code class="language-plaintext highlighter-rouge">index.html</code>도 복구된다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod sample-liveness <span class="se">\</span>
  <span class="nt">-o</span> custom-columns<span class="o">=</span>NAME:.metadata.name,UID:.metadata.uid,RESTARTS:.status.containerStatuses[0].restartCount
kubectl describe pod sample-liveness
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">kubectl describe</code>의 Event에서 Liveness Probe 실패와 Container 재시작 기록을 확인한다.</p>

<h2 id="7--readiness-probe-실패">7 ) Readiness Probe 실패</h2>

<hr />

<p>Readiness 실패가 Service Endpoint에 미치는 영향을 확인한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">sample-readiness.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-readiness</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">sample-readiness</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">readinessProbe</span><span class="pi">:</span>
        <span class="na">exec</span><span class="pi">:</span>
          <span class="na">command</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">test</span>
            <span class="pi">-</span> <span class="s">-e</span>
            <span class="pi">-</span> <span class="s">/usr/share/nginx/html/50x.html</span>
        <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">3</span>
        <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">successThreshold</span><span class="pi">:</span> <span class="m">2</span>
        <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">1</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-readiness-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">sample-readiness</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>Pod와 Service를 생성하고 Ready 상태와 EndpointSlice를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-readiness.yaml
kubectl <span class="nb">wait</span> <span class="se">\</span>
  <span class="nt">--for</span><span class="o">=</span><span class="nv">condition</span><span class="o">=</span>Ready <span class="se">\</span>
  pod/sample-readiness <span class="se">\</span>
  <span class="nt">--timeout</span><span class="o">=</span>60s
kubectl get pod sample-readiness
kubectl get endpointslice <span class="se">\</span>
  <span class="nt">-l</span> kubernetes.io/service-name<span class="o">=</span>sample-readiness-service
</code></pre></div></div>

<p>Readiness Probe가 확인하는 File을 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec </span>sample-readiness <span class="nt">--</span> <span class="se">\</span>
  <span class="nb">rm</span> <span class="nt">-f</span> /usr/share/nginx/html/50x.html
</code></pre></div></div>

<p>Probe 실패 후 Pod의 <code class="language-plaintext highlighter-rouge">READY</code> 값과 EndpointSlice 상태를 다시 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod sample-readiness <span class="nt">--watch</span>
kubectl get endpointslice <span class="se">\</span>
  <span class="nt">-l</span> kubernetes.io/service-name<span class="o">=</span>sample-readiness-service <span class="se">\</span>
  <span class="nt">-o</span> yaml
</code></pre></div></div>

<p>Container는 재시작되지 않지만 Pod는 Ready 상태에서 벗어나고 Service의 정상 Endpoint에서 제외된다. File을 다시 생성하면 연속 두 번 성공한 뒤 Ready 상태로 돌아간다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec </span>sample-readiness <span class="nt">--</span> <span class="se">\</span>
  <span class="nb">touch</span> /usr/share/nginx/html/50x.html
kubectl <span class="nb">wait</span> <span class="se">\</span>
  <span class="nt">--for</span><span class="o">=</span><span class="nv">condition</span><span class="o">=</span>Ready <span class="se">\</span>
  pod/sample-readiness <span class="se">\</span>
  <span class="nt">--timeout</span><span class="o">=</span>60s
</code></pre></div></div>

<h2 id="8--startup-probe로-시작-구간-보호">8 ) Startup Probe로 시작 구간 보호</h2>

<hr />

<p>다음 예제는 Container가 시작된 지 20초 후 <code class="language-plaintext highlighter-rouge">/tmp/started</code> File을 생성한다. Startup Probe가 이 File을 발견하기 전에는 Liveness·Readiness Probe가 실행되지 않는다. <code class="language-plaintext highlighter-rouge">sample-startup.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-startup</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">command</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">/bin/sh</span>
        <span class="pi">-</span> <span class="s">-c</span>
        <span class="pi">-</span> <span class="pi">|</span>
          <span class="s">rm -f /tmp/started /tmp/probe.log</span>
          <span class="s">(sleep 20; touch /tmp/started) &amp;</span>
          <span class="s">exec nginx -g 'daemon off;'</span>
      <span class="na">startupProbe</span><span class="pi">:</span>
        <span class="na">exec</span><span class="pi">:</span>
          <span class="na">command</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">/bin/sh</span>
            <span class="pi">-</span> <span class="s">-c</span>
            <span class="pi">-</span> <span class="pi">|</span>
              <span class="s">echo "[$(date)] startup" &gt;&gt; /tmp/probe.log</span>
              <span class="s">test -e /tmp/started</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">3</span>
        <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">successThreshold</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">10</span>
      <span class="na">livenessProbe</span><span class="pi">:</span>
        <span class="na">exec</span><span class="pi">:</span>
          <span class="na">command</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">/bin/sh</span>
            <span class="pi">-</span> <span class="s">-c</span>
            <span class="pi">-</span> <span class="pi">|</span>
              <span class="s">echo "[$(date)] liveness" &gt;&gt; /tmp/probe.log</span>
              <span class="s">test ! -e /tmp/liveness-fail</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">3</span>
        <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">successThreshold</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">3</span>
      <span class="na">readinessProbe</span><span class="pi">:</span>
        <span class="na">exec</span><span class="pi">:</span>
          <span class="na">command</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">/bin/sh</span>
            <span class="pi">-</span> <span class="s">-c</span>
            <span class="pi">-</span> <span class="pi">|</span>
              <span class="s">echo "[$(date)] readiness" &gt;&gt; /tmp/probe.log</span>
              <span class="s">test ! -e /tmp/readiness-fail</span>
        <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">3</span>
        <span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">successThreshold</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">1</span>
</code></pre></div></div>

<p>Pod를 생성하고 Probe 실행 순서를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-startup.yaml
kubectl get pod sample-startup <span class="nt">--watch</span>
</code></pre></div></div>

<p>다른 터미널에서 Probe 기록을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec </span>sample-startup <span class="nt">--</span> <span class="se">\</span>
  <span class="nb">cat</span> /tmp/probe.log
</code></pre></div></div>

<p>처음에는 <code class="language-plaintext highlighter-rouge">startup</code>만 기록된다. <code class="language-plaintext highlighter-rouge">/tmp/started</code>가 생성되어 Startup Probe가 성공하면 <code class="language-plaintext highlighter-rouge">startup</code> 검사는 끝나고 <code class="language-plaintext highlighter-rouge">liveness</code>와 <code class="language-plaintext highlighter-rouge">readiness</code>가 기록되기 시작한다.</p>

<p>이 예제에서 Startup Probe는 최대 약 30초 동안 시작 완료를 기다릴 수 있다. 그 안에 성공하지 못하면 kubelet은 해당 Container를 재시작한다. Pod Object 자체를 다시 생성하는 동작은 아니다.</p>

<h2 id="9--restartpolicy">9 ) restartPolicy</h2>

<hr />

<p><code class="language-plaintext highlighter-rouge">spec.restartPolicy</code>는 Pod 안의 Container가 종료되었을 때 kubelet이 다시 시작할지를 결정한다. Pod를 새로 생성하는 정책이 아니다.</p>

<table>
  <thead>
    <tr>
      <th>값</th>
      <th>종료 코드 <code class="language-plaintext highlighter-rouge">0</code></th>
      <th>종료 코드 <code class="language-plaintext highlighter-rouge">0</code> 이외</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Always</code></td>
      <td>재시작</td>
      <td>재시작</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OnFailure</code></td>
      <td>재시작하지 않음</td>
      <td>재시작</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Never</code></td>
      <td>재시작하지 않음</td>
      <td>재시작하지 않음</td>
    </tr>
  </tbody>
</table>

<p>일반 Pod의 기본값은 <code class="language-plaintext highlighter-rouge">Always</code>이다. Deployment와 StatefulSet 같은 일반적인 장기 실행 Workload의 Pod Template도 <code class="language-plaintext highlighter-rouge">Always</code>를 사용한다. 완료형 Workload인 Job은 성공한 Container가 다시 시작되면 작업을 완료할 수 없으므로 <code class="language-plaintext highlighter-rouge">OnFailure</code> 또는 <code class="language-plaintext highlighter-rouge">Never</code>만 사용할 수 있다. Job의 재시도 방식은 <a href="/cloud-native-30-kubernetes-job-cronjob/">Kubernetes Job과 CronJob</a>에서 다룬다.</p>

<p>정상 종료한 Container도 <code class="language-plaintext highlighter-rouge">Always</code>에서 재시작되는지 확인한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">sample-restart-always.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-restart-always</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">tools-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">busybox:1.36</span>
      <span class="na">command</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">/bin/sh</span>
        <span class="pi">-</span> <span class="s">-c</span>
        <span class="pi">-</span> <span class="s">exit </span><span class="m">0</span>
</code></pre></div></div>

<p>Pod를 생성하면 Process가 종료 코드 <code class="language-plaintext highlighter-rouge">0</code>으로 끝나지만 kubelet이 계속 Container를 재시작한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-restart-always.yaml
kubectl get pod sample-restart-always <span class="nt">--watch</span>
kubectl describe pod sample-restart-always
</code></pre></div></div>

<p>반복 종료가 발생하면 재시작 사이의 지연이 증가하며 kubectl의 <code class="language-plaintext highlighter-rouge">STATUS</code>에 <code class="language-plaintext highlighter-rouge">CrashLoopBackOff</code>가 표시될 수 있다. <code class="language-plaintext highlighter-rouge">CrashLoopBackOff</code>는 Pod Phase가 아니라 반복 실패에 대한 kubectl 상태 표시이다.</p>

<h2 id="10--probe와-restartpolicy의-연결">10 ) Probe와 restartPolicy의 연결</h2>

<hr />

<p>Liveness·Startup Probe 실패가 기준 횟수에 도달하면 kubelet은 실패한 Container를 종료한다. 이후 Container 수준의 <code class="language-plaintext highlighter-rouge">restartPolicy</code> 적용 결과에 따라 재시작 여부가 결정된다. Readiness Probe는 Container를 종료하지 않으므로 <code class="language-plaintext highlighter-rouge">restartPolicy</code>를 작동시키지 않는다.</p>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>Container 종료</th>
      <th><code class="language-plaintext highlighter-rouge">restartPolicy</code> 적용</th>
      <th>Service Traffic</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Liveness 실패 기준 도달</td>
      <td>종료함</td>
      <td>적용함</td>
      <td>재시작과 Ready 상태에 따라 제외될 수 있음</td>
    </tr>
    <tr>
      <td>Readiness 실패</td>
      <td>종료하지 않음</td>
      <td>적용하지 않음</td>
      <td>정상 Endpoint에서 제외됨</td>
    </tr>
    <tr>
      <td>Startup 실패 기준 도달</td>
      <td>종료함</td>
      <td>적용함</td>
      <td>시작 완료 전에는 Ready가 아님</td>
    </tr>
    <tr>
      <td>Container Process 자체 종료</td>
      <td>이미 종료됨</td>
      <td>적용함</td>
      <td>Ready가 아니므로 정상 Endpoint에서 제외됨</td>
    </tr>
  </tbody>
</table>

<p>여러 Container가 있는 Pod에서는 Liveness 실패가 발생한 Container만 재시작한다. 다만 Container 중 하나라도 Ready 상태가 아니면 <code class="language-plaintext highlighter-rouge">ContainersReady</code>와 일반적인 <code class="language-plaintext highlighter-rouge">Ready</code> Condition에 영향을 줄 수 있다.</p>

<blockquote>
  <p><strong>중간 정리</strong></p>

  <ul>
    <li>
      <p>kubelet은 Worker에서 Container별 Probe를 실행한다.</p>
    </li>
    <li>
      <p>Liveness와 Startup 실패는 Container 재시작으로 이어질 수 있고 Readiness 실패는 Traffic 전달을 중단한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">restartPolicy</code>는 종료된 Container의 재시작 여부를 결정하며 Pod Object를 다시 생성하지 않는다.</p>
    </li>
  </ul>
</blockquote>

<h2 id="11--init-container">11 ) Init Container</h2>

<hr />

<blockquote>
  <p><strong>Init Container</strong></p>

  <p>Main Container가 시작되기 전에 초기 설정이나 준비 작업을 완료하는 Container이다.</p>
</blockquote>

<p>Init Container는 <code class="language-plaintext highlighter-rouge">spec.initContainers</code>에 여러 개를 선언할 수 있다. 목록 위에서부터 하나씩 실행되며 각 Init Container가 성공해야 다음 Init Container가 시작된다. 모든 초기화가 끝나야 <code class="language-plaintext highlighter-rouge">spec.containers</code>의 Main Container가 시작된다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Init Container</th>
      <th>Main Container</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시작 시점</td>
      <td>Main Container보다 먼저 시작</td>
      <td>모든 Init Container 성공 후 시작</td>
    </tr>
    <tr>
      <td>여러 Container의 순서</td>
      <td>선언 순서대로 하나씩 실행</td>
      <td>기본적으로 서로 병렬로 시작</td>
    </tr>
    <tr>
      <td>실행 형태</td>
      <td>준비 작업을 마치고 정상 종료</td>
      <td>일반적으로 Application Process를 계속 실행</td>
    </tr>
    <tr>
      <td>주요 용도</td>
      <td>설정 생성, 의존 대상 대기, 초기 Data 준비</td>
      <td>실제 요청 처리와 Background 작업</td>
    </tr>
  </tbody>
</table>

<p>초기화에만 필요한 Script나 Utility를 별도 Image에 둘 수 있으므로 Main Image의 구성과 권한을 줄이는 데 도움이 된다. Init Container와 Main Container가 결과를 공유하려면 공통 Volume을 Mount해야 한다. 서로의 Container File System은 자동으로 공유되지 않는다.</p>

<p>Control Plane과 Worker 관점의 실행 순서는 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>API Server에 저장된 Pod Spec
            │
            ▼
Scheduler가 Worker 선택
            │
            ▼
Worker의 kubelet
  ├── 첫 번째 Init Container 실행·성공 확인
  ├── 두 번째 Init Container 실행·성공 확인
  └── Main Container 실행
            │
            ▼
       Probe와 Ready 판정
</code></pre></div></div>

<p>Init Container가 실패하면 kubelet은 Pod의 <code class="language-plaintext highlighter-rouge">restartPolicy</code>에 따라 다시 시도한다. 초기화가 성공하지 않는 동안 Main Container는 시작되지 않는다.</p>

<h2 id="12--init-container-실습">12 ) Init Container 실습</h2>

<hr />

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-initcontainer.yaml</code>로 저장한다. 두 Init Container와 nginx Container는 <code class="language-plaintext highlighter-rouge">emptyDir</code> Volume을 공유한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-initcontainer</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">initContainers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">output-1</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">alpine:3.20</span>
      <span class="na">command</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">sh</span>
        <span class="pi">-</span> <span class="s">-c</span>
        <span class="pi">-</span> <span class="s">sleep 20; echo 1st &gt; /usr/share/nginx/html/index.html</span>
      <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html-volume</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/usr/share/nginx/html</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">output-2</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">alpine:3.20</span>
      <span class="na">command</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">sh</span>
        <span class="pi">-</span> <span class="s">-c</span>
        <span class="pi">-</span> <span class="s">sleep 10; echo 2nd &gt; /usr/share/nginx/html/index.html</span>
      <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html-volume</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/usr/share/nginx/html</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html-volume</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/usr/share/nginx/html</span>
  <span class="na">volumes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">html-volume</span>
      <span class="na">emptyDir</span><span class="pi">:</span> <span class="pi">{}</span>
</code></pre></div></div>

<p>Pod를 적용하면서 <code class="language-plaintext highlighter-rouge">STATUS</code>와 Init Container 진행 번호를 관찰한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">--watch</span>
</code></pre></div></div>

<p>다른 Terminal에서 다음 명령을 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-initcontainer.yaml
</code></pre></div></div>

<p>초기화가 끝나면 각 Init Container의 Log와 Main Container가 읽는 File을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl logs pod/sample-initcontainer <span class="nt">-c</span> output-1
kubectl logs pod/sample-initcontainer <span class="nt">-c</span> output-2
kubectl <span class="nb">exec </span>pod/sample-initcontainer <span class="nt">--</span> <span class="se">\</span>
  <span class="nb">cat</span> /usr/share/nginx/html/index.html
</code></pre></div></div>

<p>두 번째 Init Container가 첫 번째 Container의 File을 덮어쓰므로 최종 출력은 <code class="language-plaintext highlighter-rouge">2nd</code>이다. 이 결과는 두 작업이 병렬로 경쟁한 것이 아니라 선언 순서대로 완료되었음을 보여준다.</p>

<h2 id="13--container-lifecycle-hook">13 ) Container Lifecycle Hook</h2>

<hr />

<p>Container Lifecycle Hook은 Container 시작 직후나 종료 직전에 한 번 수행할 작업을 정의한다.</p>

<table>
  <thead>
    <tr>
      <th>Hook</th>
      <th>실행 시점</th>
      <th>주요 용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">postStart</code></td>
      <td>Container가 생성된 직후</td>
      <td>초기화 신호 전달, Cache Warm-up, 내부 API 호출</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">preStop</code></td>
      <td>Container가 종료되기 전</td>
      <td>새 작업 수신 중단, Connection 정리, Application의 안전한 종료 요청</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">postStart</code>와 Container의 <code class="language-plaintext highlighter-rouge">ENTRYPOINT</code>는 비동기적으로 시작되므로 어느 쪽이 먼저 실행된다고 보장되지 않는다. 다만 Kubernetes는 <code class="language-plaintext highlighter-rouge">postStart</code>가 완료될 때까지 Container를 완전히 Running 상태로 관리하지 않는다. 시작 전에 반드시 끝나야 하는 순차 작업은 <code class="language-plaintext highlighter-rouge">postStart</code>보다 Init Container가 적합하다.</p>

<p>Hook은 한 번의 Lifecycle Event에 대해 실행되며 Probe처럼 주기적으로 상태를 검사하지 않는다. Hook Handler가 실패하면 Kubernetes는 Container를 종료하고 Pod의 <code class="language-plaintext highlighter-rouge">restartPolicy</code>에 따라 처리한다. Hook 실행에는 별도의 Timeout Field가 없으므로 외부 호출이나 장시간 작업에는 명령 자체의 제한 시간과 실패 처리를 구성해야 한다.</p>

<p><code class="language-plaintext highlighter-rouge">exec</code> Handler는 Container 안에서 명령을 실행한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">postStart</span><span class="pi">:</span>
  <span class="na">exec</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/bin/sh</span>
      <span class="pi">-</span> <span class="s">-c</span>
      <span class="pi">-</span> <span class="s">sleep 10; touch /tmp/poststart</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">httpGet</code> Handler는 지정한 Endpoint에 HTTP 요청을 보낸다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">postStart</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/warmup</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
    <span class="na">host</span><span class="pi">:</span> <span class="s">application.example.com</span>
    <span class="na">scheme</span><span class="pi">:</span> <span class="s">HTTP</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">postStart</code>는 Application의 지속적인 정상 여부를 판정하지 않는다. 실행 중 상태 점검에는 Liveness·Readiness·Startup Probe를 사용한다.</p>

<h2 id="14--lifecycle-hook-실습">14 ) Lifecycle Hook 실습</h2>

<hr />

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-lifecycle-exec.yaml</code>로 저장한다. <code class="language-plaintext highlighter-rouge">postStart</code>는 시작 표시 File을 만들고, <code class="language-plaintext highlighter-rouge">preStop</code>은 nginx에 안전한 종료를 요청한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-lifecycle-exec</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">terminationGracePeriodSeconds</span><span class="pi">:</span> <span class="m">30</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">lifecycle</span><span class="pi">:</span>
        <span class="na">postStart</span><span class="pi">:</span>
          <span class="na">exec</span><span class="pi">:</span>
            <span class="na">command</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="s">/bin/sh</span>
              <span class="pi">-</span> <span class="s">-c</span>
              <span class="pi">-</span> <span class="s">sleep 10; touch /tmp/poststart</span>
        <span class="na">preStop</span><span class="pi">:</span>
          <span class="na">exec</span><span class="pi">:</span>
            <span class="na">command</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="s">/bin/sh</span>
              <span class="pi">-</span> <span class="s">-c</span>
              <span class="pi">-</span> <span class="pi">|</span>
                <span class="s">echo "preStop started" &gt; /proc/1/fd/1</span>
                <span class="s">nginx -s quit</span>
                <span class="s">sleep 5</span>
</code></pre></div></div>

<p>Pod를 적용하고 시작 상태를 관찰한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-lifecycle-exec.yaml
kubectl get pod sample-lifecycle-exec <span class="nt">--watch</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">postStart</code>가 끝난 뒤 생성된 File을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec </span>pod/sample-lifecycle-exec <span class="nt">--</span> <span class="nb">ls</span> <span class="nt">-l</span> /tmp/poststart
</code></pre></div></div>

<p>한 Terminal에서 Log를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl logs <span class="nt">-f</span> pod/sample-lifecycle-exec
</code></pre></div></div>

<p>다른 Terminal에서 Pod를 삭제하면 <code class="language-plaintext highlighter-rouge">preStop started</code>가 기록되고 Grace Period 안에서 nginx가 종료된다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-f</span> sample-lifecycle-exec.yaml
</code></pre></div></div>

<p>Pod가 완전히 삭제된 뒤에는 <code class="language-plaintext highlighter-rouge">kubectl exec</code>로 <code class="language-plaintext highlighter-rouge">/tmp/prestop</code> 같은 Container 내부 File을 확인할 수 없다. 종료 과정은 Hook Log, Application Log와 Event를 통해 관찰해야 한다.</p>

<h2 id="15--graceful-shutdown">15 ) Graceful Shutdown</h2>

<hr />

<blockquote>
  <p><strong>Graceful Shutdown</strong></p>

  <p>처리 중인 요청과 Data를 가능한 한 안전하게 마무리한 뒤 Application Process를 종료하는 절차이다.</p>
</blockquote>

<p>Pod 삭제나 Rolling Update로 Container가 종료될 때의 주요 흐름은 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pod 종료 요청
    │ terminationGracePeriodSeconds Countdown 시작
    ▼
preStop Hook 실행
    ▼
Container의 주 Process에 SIGTERM 전달
    ▼
Application이 신규 요청을 중단하고 기존 작업 정리
    ├── Grace Period 안에 종료 ──▶ 정상 종료
    └── 종료하지 못함 ──────────▶ SIGKILL로 강제 종료
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">terminationGracePeriodSeconds</code>의 기본값은 30초이다. <code class="language-plaintext highlighter-rouge">preStop</code> 실행 시간과 Application이 <code class="language-plaintext highlighter-rouge">SIGTERM</code>을 처리하는 시간은 같은 Grace Period 안에서 사용된다. 단순히 값을 늘리기 전에 실제 요청 처리 시간, Connection 종료, Data Flush와 종료 Log를 측정해야 한다.</p>

<p>nginx는 <code class="language-plaintext highlighter-rouge">SIGTERM</code>을 받으면 빠르게 종료할 수 있다. Worker Process가 처리 중인 요청을 마치도록 하려면 <code class="language-plaintext highlighter-rouge">preStop</code>에서 <code class="language-plaintext highlighter-rouge">nginx -s quit</code>으로 Graceful Shutdown을 요청할 수 있다. Application마다 종료 Signal 처리 방식이 다르므로 Container의 PID 1 Process가 Signal을 전달받고 올바르게 처리하는지도 확인해야 한다.</p>

<p>Rolling Update에서 Readiness Probe, <code class="language-plaintext highlighter-rouge">preStop</code>, Grace Period는 서로 다른 역할을 한다.</p>

<table>
  <thead>
    <tr>
      <th>구성</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Readiness Probe</td>
      <td>새 요청을 받을 수 있는 Pod인지 판단</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">preStop</code></td>
      <td>Container 종료 직전에 Application별 정리 작업 수행</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">terminationGracePeriodSeconds</code></td>
      <td>종료 작업을 마칠 수 있는 전체 유예 시간 제공</td>
    </tr>
  </tbody>
</table>

<h2 id="16--실습-resource-정리">16 ) 실습 Resource 정리</h2>

<hr />

<p>실습 중 생성한 Pod와 Service를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods
kubectl get service sample-readiness-service
kubectl get endpointslice <span class="se">\</span>
  <span class="nt">-l</span> kubernetes.io/service-name<span class="o">=</span>sample-readiness-service
</code></pre></div></div>

<p>Manifest로 생성한 Resource를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-f</span> sample-healthcheck.yaml
kubectl delete <span class="nt">-f</span> sample-liveness.yaml
kubectl delete <span class="nt">-f</span> sample-readiness.yaml
kubectl delete <span class="nt">-f</span> sample-startup.yaml
kubectl delete <span class="nt">-f</span> sample-restart-always.yaml
kubectl delete <span class="nt">-f</span> sample-initcontainer.yaml
kubectl delete <span class="nt">-f</span> sample-lifecycle-exec.yaml <span class="nt">--ignore-not-found</span>
</code></pre></div></div>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>Liveness Probe는 Container가 계속 동작할 수 있는지 확인하고, 실패 기준에 도달하면 kubelet이 해당 Container를 재시작한다.</p>
    </li>
    <li>
      <p>Readiness Probe는 현재 요청을 처리할 수 있는지 확인하며, 실패한 Pod는 재시작하지 않고 Service의 정상 Endpoint에서 제외한다.</p>
    </li>
    <li>
      <p>Startup Probe가 성공하기 전에는 Liveness·Readiness Probe를 실행하지 않아 시작 시간이 긴 Application의 반복 재시작을 방지한다.</p>
    </li>
    <li>
      <p>Probe는 <code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">httpGet</code>, <code class="language-plaintext highlighter-rouge">tcpSocket</code> 방식으로 실행할 수 있으며 주기, Timeout과 성공·실패 횟수를 Application 특성에 맞게 설정한다.</p>
    </li>
    <li>
      <p>Worker의 kubelet이 Probe를 실행하고 결과를 API Server에 보고하면 Control Plane의 Controller가 Ready 상태를 Service Endpoint에 반영한다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">restartPolicy</code>는 Pod가 아니라 종료된 Container의 재시작 여부를 결정한다.</p>
    </li>
    <li>
      <p>Init Container는 Main Container보다 먼저 선언 순서대로 실행되며 공통 Volume으로 초기화 결과를 전달할 수 있다.</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">postStart</code>와 <code class="language-plaintext highlighter-rouge">preStop</code>은 Lifecycle Event에 한 번 실행되고 Probe는 Container 상태를 주기적으로 검사한다.</p>
    </li>
    <li>
      <p>Graceful Shutdown에서는 <code class="language-plaintext highlighter-rouge">preStop</code>과 Application의 종료 처리가 같은 <code class="language-plaintext highlighter-rouge">terminationGracePeriodSeconds</code> 안에 완료되어야 한다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Kubernetes" /><summary type="html"><![CDATA[Probe와 restartPolicy, Init Container, Lifecycle Hook 및 Graceful Shutdown의 동작과 실습 정리]]></summary></entry><entry><title type="html">Kubernetes Resource 관리와 Autoscaling</title><link href="https://hyn128.site/cloud-native-36-kubernetes-resource-management-autoscaling/" rel="alternate" type="text/html" title="Kubernetes Resource 관리와 Autoscaling" /><published>2026-09-03T00:00:00+09:00</published><updated>2026-09-03T00:00:00+09:00</updated><id>https://hyn128.site/cloud-native-36-kubernetes-resource-management-autoscaling</id><content type="html" xml:base="https://hyn128.site/cloud-native-36-kubernetes-resource-management-autoscaling/"><![CDATA[<p>Kubernetes는 Container가 요청하는 Resource를 기준으로 Pod를 Worker에 배치하고, kubelet과 Container Runtime을 통해 실행 중인 Resource 사용을 제한한다. Namespace에는 LimitRange와 ResourceQuota를 적용할 수 있으며, Resource Request는 Cluster와 Pod Autoscaling 판단에도 사용된다.</p>

<h2 id="1--cluster-api-resource와-관리-범위">1 ) Cluster API Resource와 관리 범위</h2>

<hr />

<p>Cluster를 운영할 때 자주 확인하는 Resource는 Cluster 범위와 Namespace 범위로 나뉜다.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>범위</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Node</td>
      <td>Cluster</td>
      <td>Worker의 상태와 Capacity·Allocatable 표시</td>
    </tr>
    <tr>
      <td>Namespace</td>
      <td>Cluster</td>
      <td>Namespace 범위 Resource의 논리적 구분</td>
    </tr>
    <tr>
      <td>PersistentVolume</td>
      <td>Cluster</td>
      <td>Cluster에 제공되는 Storage 표현</td>
    </tr>
    <tr>
      <td>ResourceQuota</td>
      <td>Namespace</td>
      <td>Resource 사용량과 Object 개수 제한</td>
    </tr>
    <tr>
      <td>ServiceAccount</td>
      <td>Namespace</td>
      <td>Pod와 Process의 API Identity 제공</td>
    </tr>
    <tr>
      <td>Role</td>
      <td>Namespace</td>
      <td>Namespace 안의 API 권한 정의</td>
    </tr>
    <tr>
      <td>ClusterRole</td>
      <td>Cluster</td>
      <td>Cluster 범위 또는 재사용 가능한 API 권한 정의</td>
    </tr>
    <tr>
      <td>RoleBinding</td>
      <td>Namespace</td>
      <td>Namespace 안의 주체에 Role·ClusterRole 연결</td>
    </tr>
    <tr>
      <td>ClusterRoleBinding</td>
      <td>Cluster</td>
      <td>Cluster 범위에서 주체에 ClusterRole 연결</td>
    </tr>
    <tr>
      <td>NetworkPolicy</td>
      <td>Namespace</td>
      <td>선택한 Pod의 Ingress·Egress Traffic 제어</td>
    </tr>
  </tbody>
</table>

<p>Node와 Namespace의 기본 조회는 다음 명령으로 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Master 또는 관리 Client에서 실행</span>
kubectl get nodes <span class="nt">-o</span> wide
kubectl get node worker1 <span class="nt">-o</span> yaml
kubectl get namespaces
kubectl get pods <span class="nt">-n</span> kube-system
kubectl get pods <span class="nt">--all-namespaces</span>
</code></pre></div></div>

<p>Node Object는 일반 Workload처럼 사용자가 반복해서 생성·삭제하는 Resource가 아니다. kubelet 등록과 Node Bootstrap 과정에서 Cluster에 추가되며 운영 중에는 상태, Capacity, Condition과 배치된 Pod를 자주 확인한다.</p>

<p><code class="language-plaintext highlighter-rouge">-A</code>는 <code class="language-plaintext highlighter-rouge">--all-namespaces</code>의 축약 Option이다. <code class="language-plaintext highlighter-rouge">-a</code>는 모든 Namespace를 조회하는 Option이 아니다.</p>

<p>Namespace의 구조와 생성 방법은 <a href="/cloud-native-23-kubernetes-namespace-kubectl/">Kubernetes Namespace와 kubectl 기본 사용</a>에서, PV는 <a href="/cloud-native-35-kubernetes-volume-persistent-storage/">Kubernetes Volume과 Persistent Storage</a>에서 설명한다.</p>

<h2 id="2--resource-request와-limit">2 ) Resource Request와 Limit</h2>

<hr />

<p>Container의 <code class="language-plaintext highlighter-rouge">resources.requests</code>와 <code class="language-plaintext highlighter-rouge">resources.limits</code>에는 CPU, Memory와 Ephemeral Storage 등을 지정할 수 있다. Device Plugin을 사용하면 GPU 같은 Extended Resource도 요청할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>설정</th>
      <th>Control Plane과 Worker에서의 역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">requests</code></td>
      <td>Scheduler가 Pod를 배치할 수 있는 Node를 판단할 때 사용</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">limits</code></td>
      <td>kubelet과 Container Runtime이 실행 중인 Container의 사용 상한을 적용</td>
    </tr>
  </tbody>
</table>

<p>CPU는 Clock Frequency가 아니라 CPU Unit으로 지정한다. <code class="language-plaintext highlighter-rouge">1</code> CPU는 물리 Core 또는 Virtual Core 하나에 해당하며 <code class="language-plaintext highlighter-rouge">1000m</code>과 같다.</p>

<table>
  <thead>
    <tr>
      <th>값</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1000m</code></td>
      <td>1 CPU</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">500m</code></td>
      <td>0.5 CPU</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">100m</code></td>
      <td>0.1 CPU</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">512Mi</code></td>
      <td>512 Mebibyte</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1Gi</code></td>
      <td>1 Gibibyte</td>
    </tr>
  </tbody>
</table>

<p>Request는 Container가 항상 실제로 소비하는 최솟값이 아니다. Scheduler의 배치 판단 기준이며, CPU 경합 시에는 CPU Share의 가중치에도 사용된다. Node에 여유가 있으면 Container는 Request보다 많은 CPU와 Memory를 사용할 수 있다.</p>

<p>Limit 적용 방식은 Resource마다 다르다.</p>

<ul>
  <li>
    <p>CPU Limit을 초과하려 하면 Linux Kernel이 CPU 시간을 Throttling한다.</p>
  </li>
  <li>
    <p>Memory Limit은 반응적으로 적용되며, Memory 부족이 감지되면 OOM Killer가 Container Process를 종료할 수 있다.</p>
  </li>
</ul>

<h2 id="3--cpu와-memory-requestlimit-실습">3 ) CPU와 Memory Request·Limit 실습</h2>

<hr />

<p>실습 Resource가 기존 Workload에 미치는 영향을 줄이기 위해 <code class="language-plaintext highlighter-rouge">resource-lab</code> Namespace를 사용한다. Namespace가 없다면 Master 또는 관리 Client에서 생성한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create namespace resource-lab
kubectl get namespace resource-lab
</code></pre></div></div>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-resource.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-resource</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">sample-resource</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">sample-resource</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">512Mi</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">1Gi</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
</code></pre></div></div>

<p>Master 또는 관리 Client에서 Deployment를 생성하고 Rollout 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-resource.yaml
kubectl rollout status deployment/sample-resource <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
kubectl get pods <span class="nt">-n</span> resource-lab <span class="se">\</span>
  <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>sample-resource <span class="nt">-o</span> wide
</code></pre></div></div>

<p>Control Plane의 Scheduler는 Container별 Request를 합산하여 배치 가능한 Worker를 선택한다. Worker의 kubelet은 Pod Spec의 Limit을 Container Runtime에 전달하고 Runtime은 Linux cgroup을 통해 CPU와 Memory 제한을 적용한다.</p>

<p>Pod에 저장된 Resource 설정을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> resource-lab <span class="se">\</span>
  <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>sample-resource <span class="nt">-o</span> json <span class="se">\</span>
  | jq <span class="s1">'.items[].spec.containers[].resources'</span>
</code></pre></div></div>

<h2 id="4--request만-지정한-경우">4 ) Request만 지정한 경우</h2>

<hr />

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-resource-only-requests.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-resource-only-requests</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">sample-resource-only-requests</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">sample-resource-only-requests</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">256Mi</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s">200m</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-resource-only-requests.yaml
kubectl get pods <span class="nt">-n</span> resource-lab <span class="se">\</span>
  <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>sample-resource-only-requests
kubectl get pods <span class="nt">-n</span> resource-lab <span class="se">\</span>
  <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>sample-resource-only-requests <span class="nt">-o</span> json <span class="se">\</span>
  | jq <span class="s1">'.items[].spec.containers[].resources'</span>
</code></pre></div></div>

<p>Request만 있고 Limit이 없으면 해당 Resource에 Container별 상한이 설정되지 않는다. CPU는 Node의 여유 Resource를 더 사용할 수 있고, Memory 사용이 증가하여 Node 전체에 Memory Pressure가 발생하면 OOM이나 Pod Eviction으로 이어질 수 있다.</p>

<h2 id="5--limit만-지정한-경우">5 ) Limit만 지정한 경우</h2>

<hr />

<p>Admission 단계에서 다른 기본값이 적용되지 않았다면 특정 Resource의 Limit만 지정했을 때 Kubernetes는 같은 값을 Request로 복사한다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-resource-only-limits.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-resource-only-limits</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">resources</span><span class="pi">:</span>
        <span class="na">limits</span><span class="pi">:</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="s">256Mi</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s">200m</span>
</code></pre></div></div>

<p>Pod를 생성한 뒤 API Server에 저장된 Request와 Limit을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-resource-only-limits.yaml
kubectl get pod sample-resource-only-limits <span class="se">\</span>
  <span class="nt">-n</span> resource-lab <span class="nt">-o</span> json <span class="se">\</span>
  | jq <span class="s1">'.spec.containers[].resources'</span>
</code></pre></div></div>

<p>Namespace에 LimitRange 등 Admission 단계의 기본값이 있으면 결과가 달라질 수 있다. Scheduling Resource를 명확히 관리하려면 Request와 Limit을 모두 명시한다.</p>

<h2 id="6--ephemeral-storage-제한">6 ) Ephemeral Storage 제한</h2>

<hr />

<p>Local Ephemeral Storage는 Pod가 실행되는 Node의 임시 저장 공간이다. kubelet이 측정하는 주요 대상은 다음과 같다.</p>

<ul>
  <li>
    <p>Container Log</p>
  </li>
  <li>
    <p>Container의 Writable Layer</p>
  </li>
  <li>
    <p>Disk 기반 <code class="language-plaintext highlighter-rouge">emptyDir</code> Data</p>
  </li>
</ul>

<p>Memory 기반 <code class="language-plaintext highlighter-rouge">emptyDir</code>은 Ephemeral Storage가 아니라 Container Memory 사용량으로 계산된다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-ephemeral-storage.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-ephemeral-storage</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">resources</span><span class="pi">:</span>
        <span class="na">requests</span><span class="pi">:</span>
          <span class="na">ephemeral-storage</span><span class="pi">:</span> <span class="s">1Gi</span>
        <span class="na">limits</span><span class="pi">:</span>
          <span class="na">ephemeral-storage</span><span class="pi">:</span> <span class="s">2Gi</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-ephemeral-storage.yaml
kubectl <span class="nb">wait</span> <span class="nt">--for</span><span class="o">=</span><span class="nv">condition</span><span class="o">=</span>Ready <span class="se">\</span>
  pod/sample-ephemeral-storage <span class="se">\</span>
  <span class="nt">-n</span> resource-lab <span class="nt">--timeout</span><span class="o">=</span>120s
</code></pre></div></div>

<p>Container의 Writable Layer에 2GiB File 생성을 시도한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec</span> <span class="nt">-n</span> resource-lab sample-ephemeral-storage <span class="nt">--</span> <span class="se">\</span>
  <span class="nb">dd </span><span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>/dummy <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">count</span><span class="o">=</span>2049
</code></pre></div></div>

<p>사용량이 Limit을 초과하면 kubelet이 Pod를 Eviction 대상으로 표시할 수 있다. File System 구성과 kubelet의 Local Storage 측정 조건에 따라 결과 시점이 달라질 수 있으므로 Pod 상태와 Event를 함께 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pod sample-ephemeral-storage <span class="se">\</span>
  <span class="nt">-n</span> resource-lab <span class="nt">--watch</span>
kubectl describe pod sample-ephemeral-storage <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
kubectl get events <span class="nt">-n</span> resource-lab <span class="se">\</span>
  <span class="nt">--sort-by</span><span class="o">=</span>.lastTimestamp
</code></pre></div></div>

<p>여러 Container가 있는 Pod의 Ephemeral Storage Limit은 Container별 Limit의 합으로 계산한다. Pod 사용량에는 Container의 Writable Layer와 Log 및 Disk 기반 <code class="language-plaintext highlighter-rouge">emptyDir</code> 사용량이 포함된다.</p>

<h2 id="7--node-capacity와-allocatable">7 ) Node Capacity와 Allocatable</h2>

<hr />

<p>Node의 전체 Capacity가 모두 Pod에 할당되는 것은 아니다. kubelet은 OS와 Kubernetes Component가 사용할 Resource를 고려하여 <code class="language-plaintext highlighter-rouge">status.allocatable</code>에 Pod가 사용할 수 있는 양을 표시한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get node worker1 <span class="se">\</span>
  <span class="nt">-o</span> custom-columns<span class="o">=</span><span class="s1">'NAME:.metadata.name,CPU:.status.capacity.cpu,ALLOCATABLE_CPU:.status.allocatable.cpu,MEMORY:.status.capacity.memory,ALLOCATABLE_MEMORY:.status.allocatable.memory'</span>
kubectl describe node worker1
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>설정</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">kubeReserved</code></td>
      <td>kubelet과 Container Runtime 등 Kubernetes 관련 Daemon Resource 예약</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">systemReserved</code></td>
      <td>OS와 System Daemon Resource 예약</td>
    </tr>
    <tr>
      <td>Eviction Threshold</td>
      <td>Node Resource 고갈 전에 kubelet이 Pod를 Eviction할 기준</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">kubeReserved</code>와 <code class="language-plaintext highlighter-rouge">systemReserved</code>는 kubelet에서 구성하는 값이며 모든 Cluster에 같은 값이 자동으로 적용되는 것은 아니다. 실제 Pod 배치 가능량은 Node의 <code class="language-plaintext highlighter-rouge">status.allocatable</code>에서 확인한다.</p>

<h2 id="8--node-pressure-eviction">8 ) Node-pressure Eviction</h2>

<hr />

<p>Worker의 kubelet에 있는 Eviction Manager는 Memory, Disk 공간과 File System Inode 등의 상태를 주기적으로 확인한다. Threshold가 충족되면 Node 전체의 Resource 고갈을 막기 위해 Pod를 종료하고 Resource를 회수한다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Soft Threshold</td>
      <td>조건이 <code class="language-plaintext highlighter-rouge">evictionSoftGracePeriod</code> 동안 지속된 뒤 Eviction 시작</td>
    </tr>
    <tr>
      <td>Hard Threshold</td>
      <td>Grace Period <code class="language-plaintext highlighter-rouge">0s</code>로 즉시 Eviction 시작</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">evictionMaxPodGracePeriod</code></td>
      <td>Soft Eviction에서 허용할 Pod 종료 Grace Period 상한</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">evictionSoftGracePeriod</code>는 Threshold가 얼마나 지속돼야 하는지를 나타낸다. Pod 종료에 허용할 시간과 같은 설정이 아니다. Node-pressure Eviction은 Pod의 <code class="language-plaintext highlighter-rouge">terminationGracePeriodSeconds</code>나 PodDisruptionBudget을 그대로 따르지 않는다.</p>

<p>kubelet은 다음 순서로 Eviction 대상을 평가한다.</p>

<ol>
  <li>
    <p>고갈된 Resource의 실제 사용량이 Request를 초과했는지 확인한다.</p>
  </li>
  <li>
    <p>Pod Priority가 낮은 Pod를 우선한다.</p>
  </li>
  <li>
    <p>실제 사용량이 Request를 얼마나 초과했는지 비교한다.</p>
  </li>
</ol>

<p><code class="language-plaintext highlighter-rouge">kubectl describe node</code>에서 <code class="language-plaintext highlighter-rouge">MemoryPressure</code>, <code class="language-plaintext highlighter-rouge">DiskPressure</code>, <code class="language-plaintext highlighter-rouge">PIDPressure</code> Condition과 Event를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe node worker1

<span class="c"># worker1에서 실행</span>
<span class="nb">sudo </span>journalctl <span class="nt">-u</span> kubelet <span class="nt">--since</span> <span class="s2">"10 minutes ago"</span>
</code></pre></div></div>

<h2 id="9--scheduling-불가와-overcommit">9 ) Scheduling 불가와 Overcommit</h2>

<hr />

<p>Resource Request를 수용할 Worker가 없으면 Pod는 <code class="language-plaintext highlighter-rouge">Pending</code> 상태에 머문다. 이는 실제 CPU·Memory 사용률이 100%라는 의미가 아니라 Scheduler가 계산한 Allocatable 대비 Request가 부족하다는 의미이다.</p>

<p>Resource Overcommit은 일반적으로 Node Capacity보다 큰 Limit 총량을 허용하여 Workload가 동시에 최대치를 사용하지 않는다는 전제로 Resource를 배치하는 방식이다. Request 부족으로 Pod를 배치하지 못하는 상태와 구분해야 한다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-resource-scale.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-resource-scale</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">sample-resource-scale</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">sample-resource-scale</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">512Mi</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">1Gi</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-resource-scale.yaml
kubectl scale deployment/sample-resource-scale <span class="se">\</span>
  <span class="nt">-n</span> resource-lab <span class="nt">--replicas</span><span class="o">=</span>6
kubectl get pods <span class="nt">-n</span> resource-lab <span class="se">\</span>
  <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>sample-resource-scale <span class="nt">-o</span> wide
</code></pre></div></div>

<p>실제 Cluster에 충분한 Resource가 있으면 여섯 Pod가 모두 실행될 수 있다. <code class="language-plaintext highlighter-rouge">Pending</code> Pod가 발생한 경우 Pod Event와 Node의 할당 현황을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe pod &lt;pending-pod-name&gt; <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
kubectl describe node worker1
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">kubectl describe node</code>의 <code class="language-plaintext highlighter-rouge">Allocated resources</code>는 실제 사용량이 아니라 Pod Spec에 선언된 Request와 Limit의 합계이다. 실제 사용량은 Metrics Server가 제공하는 <code class="language-plaintext highlighter-rouge">kubectl top</code>으로 구분해 확인한다.</p>

<h2 id="10--여러-container의-resource-계산">10 ) 여러 Container의 Resource 계산</h2>

<hr />

<p>Scheduler는 Pod 단위로 Worker를 선택하므로 Pod에 포함된 Container의 Resource를 합산한다.</p>

<ul>
  <li>
    <p>일반 Application Container의 같은 Resource Request와 Limit은 모두 합산한다.</p>
  </li>
  <li>
    <p>일반 Init Container는 순차 실행되므로 같은 Resource에서 가장 큰 Init Container 값을 사용한다.</p>
  </li>
  <li>
    <p>Application Container 합계와 Init Container의 최댓값 중 큰 값을 Pod의 Scheduling 기준으로 사용한다.</p>
  </li>
</ul>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pod의 유효 Request
  = max(
      모든 Application Container Request의 합,
      Init Container Request 중 최댓값
    )
</code></pre></div></div>

<p>이 계산은 Init Container가 Application Container보다 큰 초기화 Resource를 요구할 때 해당 Pod가 실행 가능한 Worker를 확보하는 데 사용된다.</p>

<h2 id="11--cluster-autoscaler">11 ) Cluster Autoscaler</h2>

<hr />

<p>Cluster Autoscaler는 실행할 Node가 부족한 Pod를 감지하고 연동된 Infrastructure의 Node Group 크기를 조정한다. 단순한 Node CPU·Memory 평균 사용률이 아니라 Scheduler가 배치하지 못한 Pod와 해당 Pod의 Request를 주요 판단 기준으로 사용한다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pod 생성
  → Scheduler가 모든 Node의 Allocatable과 Request 비교
  → 적합한 Node가 없어 Pod가 Pending
  → Cluster Autoscaler가 확장 가능 여부 확인
  → Infrastructure Node Group 확장
  → 새 Worker 준비와 Cluster 등록
  → Scheduler가 Pending Pod 배치
</code></pre></div></div>

<p>Request를 지나치게 크게 설정하면 실제 사용량이 낮아도 Pod가 배치되지 않아 Scale-out이 발생할 수 있다. 반대로 Request를 지나치게 낮게 설정하면 실제 부하가 높아도 Scheduler는 여유가 있다고 판단할 수 있다.</p>

<p>Cluster Autoscaler는 실제 Node를 생성할 수 있는 Infrastructure와 연결되어야 한다.</p>

<table>
  <thead>
    <tr>
      <th>환경</th>
      <th>연동 구조 예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AWS</td>
      <td>Auto Scaling Group의 Desired Capacity 조정 및 Tag 기반 Node Group 탐색</td>
    </tr>
    <tr>
      <td>직접 구축한 VM 환경</td>
      <td>VM 생성, OS 준비와 <code class="language-plaintext highlighter-rouge">kubeadm join</code>까지 자동화 필요</td>
    </tr>
    <tr>
      <td>Cluster API 사용 환경</td>
      <td>Machine Resource와 Infrastructure Provider로 Node Lifecycle 관리</td>
    </tr>
  </tbody>
</table>

<p>Cluster API는 On-premise 환경에서 사용할 수 있는 선택지 중 하나이며, kubeadm Cluster에 Cluster Autoscaling을 자동으로 제공하는 기능은 아니다.</p>

<p>Request와 Limit은 임의의 고정 비율로 결정하지 않고 Application의 부하 Test와 관측 결과를 기준으로 조정한다. Memory Limit이 지나치게 낮으면 부하 Test 중 OOM Kill이 발생할 수 있고 Request가 실제 요구량보다 크면 Scheduling과 Node 확장 효율이 낮아질 수 있다.</p>

<h2 id="12--limitrange">12 ) LimitRange</h2>

<hr />

<p>LimitRange는 Namespace 안에서 Container, Pod 또는 PVC에 허용할 Resource 범위를 정의한다. 새 Object가 API Server의 Admission 단계를 통과할 때 적용되며 이미 실행 중인 Pod의 Resource 설정은 변경하지 않는다.</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">default</code></td>
      <td>Container에 Limit이 없을 때 적용할 기본 Limit</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">defaultRequest</code></td>
      <td>Container에 Request가 없을 때 적용할 기본 Request</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">max</code></td>
      <td>허용할 최대 Resource</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">min</code></td>
      <td>허용할 최소 Resource</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">maxLimitRequestRatio</code></td>
      <td>Limit과 Request 사이의 최대 비율</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">type: Container</code>에서는 위 항목을 모두 사용할 수 있다. Pod 범위에서는 기본값을 주입하는 <code class="language-plaintext highlighter-rouge">default</code>, <code class="language-plaintext highlighter-rouge">defaultRequest</code>를 사용하지 않으며, PVC에는 Storage Request의 <code class="language-plaintext highlighter-rouge">min</code>과 <code class="language-plaintext highlighter-rouge">max</code>를 적용한다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-limitrange-container.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">LimitRange</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-limitrange-container</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">limits</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Container</span>
      <span class="na">default</span><span class="pi">:</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s">512Mi</span>
        <span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
      <span class="na">defaultRequest</span><span class="pi">:</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s">256Mi</span>
        <span class="na">cpu</span><span class="pi">:</span> <span class="s">250m</span>
      <span class="na">max</span><span class="pi">:</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s">1025Mi</span>
        <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
      <span class="na">min</span><span class="pi">:</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s">128Mi</span>
        <span class="na">cpu</span><span class="pi">:</span> <span class="s">125m</span>
      <span class="na">maxLimitRequestRatio</span><span class="pi">:</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
        <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-limitrange-container.yaml
kubectl describe limitrange sample-limitrange-container <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
</code></pre></div></div>

<h2 id="13--limitrange-위반-확인">13 ) LimitRange 위반 확인</h2>

<hr />

<p>CPU Request와 Limit이 최소값 <code class="language-plaintext highlighter-rouge">125m</code>보다 작은 Pod를 생성한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">sample-pod-below-min.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-pod-below-min</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">resources</span><span class="pi">:</span>
        <span class="na">requests</span><span class="pi">:</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s">100m</span>
        <span class="na">limits</span><span class="pi">:</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s">100m</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-pod-below-min.yaml
</code></pre></div></div>

<p>API Server는 LimitRange의 최소 CPU보다 작다는 <code class="language-plaintext highlighter-rouge">Forbidden</code> 응답으로 생성을 거부한다.</p>

<p>다음에는 Limit과 Request 비율이 4인 Pod를 확인한다. 내용을 <code class="language-plaintext highlighter-rouge">sample-pod-over-ratio.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-pod-over-ratio</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-container</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
      <span class="na">resources</span><span class="pi">:</span>
        <span class="na">requests</span><span class="pi">:</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s">125m</span>
        <span class="na">limits</span><span class="pi">:</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-pod-over-ratio.yaml
</code></pre></div></div>

<p>설정된 <code class="language-plaintext highlighter-rouge">maxLimitRequestRatio.cpu</code>가 <code class="language-plaintext highlighter-rouge">2</code>이므로 비율 <code class="language-plaintext highlighter-rouge">4</code>인 요청은 <code class="language-plaintext highlighter-rouge">Forbidden</code> 응답으로 거부된다. 실패한 요청은 Pod Resource를 생성하지 않는다.</p>

<h2 id="14--resourcequota-object-개수-제한">14 ) ResourceQuota Object 개수 제한</h2>

<hr />

<p>ResourceQuota는 Namespace 전체에서 사용할 수 있는 Resource 양과 생성 가능한 Object 수를 제한한다. 기존 Object를 삭제하거나 설정을 변경하지는 않지만, Quota가 생성되면 Namespace의 기존 Object도 현재 사용량에 포함된다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-resourcequota-count.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ResourceQuota</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-resourcequota-count</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">hard</span><span class="pi">:</span>
    <span class="na">count/persistentvolumeclaims</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/services</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/secrets</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/configmaps</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/replicationcontrollers</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/deployments.apps</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/replicasets.apps</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/statefulsets.apps</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/jobs.batch</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">count/cronjobs.batch</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">count/deployments.extensions</code>는 제거된 <code class="language-plaintext highlighter-rouge">extensions</code> API Group을 대상으로 하므로 현재 Manifest에 사용하지 않는다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-resourcequota-count.yaml
kubectl describe resourcequota sample-resourcequota-count <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
</code></pre></div></div>

<p>ConfigMap 열한 개 생성을 시도한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>i <span class="k">in</span> <span class="si">$(</span><span class="nb">seq </span>1 11<span class="si">)</span><span class="p">;</span> <span class="k">do
  </span>kubectl create configmap <span class="s2">"conf-</span><span class="k">${</span><span class="nv">i</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--from-literal</span><span class="o">=</span><span class="nv">key1</span><span class="o">=</span>val1 <span class="se">\</span>
    <span class="nt">-n</span> resource-lab
<span class="k">done</span>
</code></pre></div></div>

<p>Namespace에 이미 존재하는 ConfigMap을 포함하여 총수가 <code class="language-plaintext highlighter-rouge">10</code>에 도달하면 다음 생성 요청이 거부된다. Kubernetes가 자동으로 만든 <code class="language-plaintext highlighter-rouge">kube-root-ca.crt</code> ConfigMap 등이 있을 수 있으므로 실패 순서를 고정하지 않고 ResourceQuota의 현재 사용량을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe resourcequota sample-resourcequota-count <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
kubectl get configmaps <span class="nt">-n</span> resource-lab
</code></pre></div></div>

<h2 id="15--resourcequota-key-종류">15 ) ResourceQuota Key 종류</h2>

<hr />

<p><code class="language-plaintext highlighter-rouge">count/&lt;resource&gt;[.&lt;group&gt;]</code> 형식 외에도 Kubernetes가 정의한 Object별 Quota Key와 StorageClass별 Key를 사용할 수 있다. 두 형식은 단순히 옛날 방식과 최근 방식으로 나뉘지 않는다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-resourcequota-special.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ResourceQuota</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-resourcequota-special</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">hard</span><span class="pi">:</span>
    <span class="na">sample-storageclass.storageclass.storage.k8s.io/persistentvolumeclaims</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">services.loadbalancers</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">services.nodeports</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">pods</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">persistentvolumeclaims</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">replicationcontrollers</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">secrets</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">configmaps</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">services</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
    <span class="na">resourcequotas</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10"</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Key</th>
      <th>제한 대상</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">services.loadbalancers</code></td>
      <td><code class="language-plaintext highlighter-rouge">type: LoadBalancer</code> Service 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">services.nodeports</code></td>
      <td>Service가 사용하는 NodePort 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pods</code></td>
      <td>Terminal 상태가 아닌 Pod 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;storage-class&gt;.storageclass.storage.k8s.io/persistentvolumeclaims</code></td>
      <td>해당 StorageClass를 요청하는 PVC 수</td>
    </tr>
  </tbody>
</table>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-resourcequota-special.yaml
kubectl describe resourcequota sample-resourcequota-special <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
</code></pre></div></div>

<h2 id="16--resourcequota-사용량-제한">16 ) ResourceQuota 사용량 제한</h2>

<hr />

<p>ResourceQuota는 CPU, Memory, Storage와 Extended Resource의 Namespace 전체 합계도 제한할 수 있다.</p>

<p>다음 내용을 <code class="language-plaintext highlighter-rouge">sample-resourcequota-usable.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ResourceQuota</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sample-resourcequota-usable</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">resource-lab</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">hard</span><span class="pi">:</span>
    <span class="na">requests.memory</span><span class="pi">:</span> <span class="s">2Gi</span>
    <span class="na">requests.storage</span><span class="pi">:</span> <span class="s">5Gi</span>
    <span class="na">sample-storageclass.storageclass.storage.k8s.io/requests.storage</span><span class="pi">:</span> <span class="s">5Gi</span>
    <span class="na">requests.ephemeral-storage</span><span class="pi">:</span> <span class="s">5Gi</span>
    <span class="na">requests.nvidia.com/gpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
    <span class="na">limits.cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">4"</span>
    <span class="na">limits.ephemeral-storage</span><span class="pi">:</span> <span class="s">10Gi</span>
</code></pre></div></div>

<p>GPU 같은 Extended Resource는 Overcommit을 지원하지 않으므로 Quota에는 <code class="language-plaintext highlighter-rouge">requests.&lt;extended-resource&gt;</code> 형식만 사용한다. <code class="language-plaintext highlighter-rouge">limits.nvidia.com/gpu</code>는 유효한 Extended Resource Quota Key가 아니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> sample-resourcequota-usable.yaml
kubectl describe resourcequota sample-resourcequota-usable <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
</code></pre></div></div>

<p>같은 Namespace에 ResourceQuota가 여러 개 있으면 새 Object는 모든 Quota 조건을 만족해야 한다.</p>

<h2 id="17--horizontal-pod-autoscaler">17 ) Horizontal Pod Autoscaler</h2>

<hr />

<blockquote>
  <p><strong>Horizontal Pod Autoscaler(HPA)</strong></p>

  <p>Metric 값을 기준으로 Deployment나 StatefulSet 같은 확장 가능한 Workload의 Replica 수를 자동 조정하는 API Resource와 Controller이다.</p>
</blockquote>

<p>HPA는 Deployment, StatefulSet, ReplicaSet과 ReplicationController처럼 Scale Subresource를 제공하는 대상의 Replica를 조정할 수 있다. 부하가 높으면 Replica를 늘리고 낮으면 줄인다. CPU 사용률처럼 Request 대비 비율을 사용하는 Resource Metric은 대상 Pod의 Container에 해당 Resource Request가 정의돼 있어야 계산할 수 있다.</p>

<p>HPA Controller의 기본 동기화 주기는 <code class="language-plaintext highlighter-rouge">30초</code>가 아니라 <code class="language-plaintext highlighter-rouge">15초</code>이다. 실제 값은 kube-controller-manager의 <code class="language-plaintext highlighter-rouge">horizontalPodAutoscalerSyncPeriod</code> 설정에 따라 달라질 수 있다.</p>

<p>기본 Replica 계산식은 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>desiredReplicas
  = ceil(
      currentReplicas
      × currentMetricValue
      ÷ desiredMetricValue
    )
</code></pre></div></div>

<p>두 Pod의 CPU 사용률이 각각 <code class="language-plaintext highlighter-rouge">100%</code>, <code class="language-plaintext highlighter-rouge">90%</code>라면 평균은 <code class="language-plaintext highlighter-rouge">95%</code>이다. 목표 평균 사용률이 <code class="language-plaintext highlighter-rouge">50%</code>일 때 계산 결과는 다음과 같다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ceil(2 × 95 ÷ 50)
= ceil(3.8)
= 4
</code></pre></div></div>

<p>따라서 허용 범위와 Stabilization 동작 등 다른 조건을 제외한 기본 계산 결과는 Replica 네 개이다. Metric 수집 상태는 <a href="/cloud-native-26-resource-inspection-debugging/">Kubernetes Resource 조회와 Pod Debugging</a>의 Metrics Server와 <code class="language-plaintext highlighter-rouge">kubectl top</code> 절에서 확인할 수 있다.</p>

<h2 id="18--hpa-동작-구조와-준비-상태-확인">18 ) HPA 동작 구조와 준비 상태 확인</h2>

<hr />

<p>HPA는 Control Plane에서 실행되는 Controller와 Worker에서 수집되는 Metric을 연결하여 동작한다.</p>

<ol>
  <li>
    <p>Worker의 kubelet이 Container의 CPU·Memory 사용량을 수집한다.</p>
  </li>
  <li>
    <p>Metrics Server가 각 Node의 kubelet에서 Metric을 수집하여 Metrics API로 제공한다.</p>
  </li>
  <li>
    <p>Control Plane의 HPA Controller가 Metrics API에서 대상 Pod의 Metric을 조회한다.</p>
  </li>
  <li>
    <p>HPA Controller가 계산한 Replica 수를 Deployment의 Scale Subresource에 반영한다.</p>
  </li>
  <li>
    <p>Deployment Controller가 변경된 Replica 수에 맞춰 Pod를 생성하거나 제거한다.</p>
  </li>
  <li>
    <p>새 Pod가 필요하면 Scheduler가 Worker를 선택하고, 해당 Worker의 kubelet과 Container Runtime이 Container를 실행한다.</p>
  </li>
</ol>

<p>HPA를 사용하기 전에 <a href="/cloud-native-26-resource-inspection-debugging/">Kubernetes Resource 조회와 Pod Debugging</a>에서 설치한 Metrics Server와 Metric 수집 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-n</span> kube-system <span class="se">\</span>
  <span class="nt">-l</span> k8s-app<span class="o">=</span>metrics-server
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl top nodes
kubectl top pods <span class="nt">-A</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">kubectl top</code> 결과가 나오지 않는 상태에서는 Resource Metric 기반 HPA 실습을 진행할 수 없다. 먼저 Metrics Server Pod, APIService와 kubelet 통신 상태를 확인해야 한다.</p>

<h2 id="19--hpa-실습">19 ) HPA 실습</h2>

<hr />

<p>HPA가 CPU 사용률을 계산할 수 있도록 CPU Request를 지정한 Deployment를 생성한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">nginx-hpa.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s">100m</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">128Mi</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">256Mi</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>Cluster 안에서 부하 발생 Pod가 nginx에 접근할 수 있도록 다음 내용을 <code class="language-plaintext highlighter-rouge">nginx-hpa-service.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-hpa-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
</code></pre></div></div>

<p>CPU 평균 사용률이 Request의 <code class="language-plaintext highlighter-rouge">50%</code>를 넘으면 Replica를 늘리도록 다음 내용을 <code class="language-plaintext highlighter-rouge">nginx-hpa-autoscaler.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling/v2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HorizontalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
  <span class="na">minReplicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">maxReplicas</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">metrics</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
      <span class="na">resource</span><span class="pi">:</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">cpu</span>
        <span class="na">target</span><span class="pi">:</span>
          <span class="na">type</span><span class="pi">:</span> <span class="s">Utilization</span>
          <span class="na">averageUtilization</span><span class="pi">:</span> <span class="m">50</span>
</code></pre></div></div>

<p>세 Resource를 생성하고 연결 상태를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> nginx-hpa.yaml
kubectl apply <span class="nt">-f</span> nginx-hpa-service.yaml
kubectl apply <span class="nt">-f</span> nginx-hpa-autoscaler.yaml

kubectl get deployment,service,hpa
kubectl describe hpa nginx-hpa
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TARGETS</code>가 <code class="language-plaintext highlighter-rouge">&lt;unknown&gt;/50%</code>로 계속 표시되면 CPU Request와 Metrics Server 상태를 함께 확인한다.</p>

<h3 id="부하에-따른-replica-변화-확인">부하에 따른 Replica 변화 확인</h3>

<p>두 터미널에서 HPA와 Pod의 변화를 각각 관찰한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get hpa nginx-hpa <span class="nt">--watch</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>nginx-hpa <span class="nt">--watch</span>
</code></pre></div></div>

<p>다른 터미널에서 Service로 요청을 반복 전송하는 Pod 세 개를 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl run load-generator <span class="se">\</span>
  <span class="nt">--image</span><span class="o">=</span>busybox:1.36 <span class="se">\</span>
  <span class="nt">--restart</span><span class="o">=</span>Never <span class="se">\</span>
  <span class="nt">--</span> /bin/sh <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"while true; do wget -q -O- http://nginx-hpa-service; done"</span>

kubectl run load-generator1 <span class="se">\</span>
  <span class="nt">--image</span><span class="o">=</span>busybox:1.36 <span class="se">\</span>
  <span class="nt">--restart</span><span class="o">=</span>Never <span class="se">\</span>
  <span class="nt">--</span> /bin/sh <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"while true; do wget -q -O- http://nginx-hpa-service; done"</span>

kubectl run load-generator2 <span class="se">\</span>
  <span class="nt">--image</span><span class="o">=</span>busybox:1.36 <span class="se">\</span>
  <span class="nt">--restart</span><span class="o">=</span>Never <span class="se">\</span>
  <span class="nt">--</span> /bin/sh <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"while true; do wget -q -O- http://nginx-hpa-service; done"</span>
</code></pre></div></div>

<p>HPA가 즉시 반응하지 않을 수 있다. Metric 수집과 HPA 동기화가 진행된 뒤 CPU 사용률, <code class="language-plaintext highlighter-rouge">DESIRED</code> Replica와 Pod 수가 변하는지 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl top pods <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>nginx-hpa
kubectl get hpa nginx-hpa
kubectl get deployment nginx-hpa
</code></pre></div></div>

<p>부하 발생 Pod를 삭제하면 CPU 사용률이 낮아지고 Stabilization 조건에 따라 Replica가 다시 줄어든다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete pod <span class="se">\</span>
  load-generator load-generator1 load-generator2
kubectl get hpa nginx-hpa <span class="nt">--watch</span>
</code></pre></div></div>

<h2 id="20--hpa-metric과-target">20 ) HPA Metric과 Target</h2>

<hr />

<p><code class="language-plaintext highlighter-rouge">autoscaling/v2</code> HPA는 다음 Metric Source를 사용할 수 있다.</p>

<table>
  <thead>
    <tr>
      <th>Metric Type</th>
      <th>판단 대상</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Resource</code></td>
      <td>Pod의 CPU·Memory 같은 Resource 사용량</td>
      <td>CPU Request 대비 평균 사용률</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ContainerResource</code></td>
      <td>Pod 안의 특정 Container Resource 사용량</td>
      <td>Application Container의 CPU 사용률</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Pods</code></td>
      <td>각 Pod에서 수집한 Custom Metric</td>
      <td>Pod별 Connection 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Object</code></td>
      <td>하나의 Kubernetes Object와 연결된 Metric</td>
      <td>Ingress의 초당 요청 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">External</code></td>
      <td>Kubernetes Object와 직접 연결되지 않은 외부 Metric</td>
      <td>Load Balancer QPS, Queue 길이</td>
    </tr>
  </tbody>
</table>

<p>Resource Metric의 Target은 다음과 같이 구분한다.</p>

<table>
  <thead>
    <tr>
      <th>Target Type</th>
      <th>의미</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Utilization</code></td>
      <td>Resource Request 대비 평균 사용률</td>
      <td>CPU Request의 <code class="language-plaintext highlighter-rouge">50%</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AverageValue</code></td>
      <td>Pod 하나당 Metric의 평균값</td>
      <td>Pod당 Memory <code class="language-plaintext highlighter-rouge">500Mi</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Value</code></td>
      <td>Object·External Metric의 전체 목표값</td>
      <td>Queue 전체 길이</td>
    </tr>
  </tbody>
</table>

<p>여러 Metric을 지정하면 HPA는 각 Metric으로 필요한 Replica 수를 계산한 뒤 가장 큰 값을 사용한다. 따라서 CPU와 Memory 중 하나만 목표치를 초과해도 Scale Out이 발생할 수 있다.</p>

<h2 id="21--다중-metric과-scaling-behavior">21 ) 다중 Metric과 Scaling Behavior</h2>

<hr />

<p>앞서 만든 <code class="language-plaintext highlighter-rouge">nginx-hpa-autoscaler.yaml</code>을 다음 내용으로 변경한다. CPU 사용률과 평균 Memory 사용량을 함께 확인하고, Replica 증감 속도를 제한하는 설정이다. <code class="language-plaintext highlighter-rouge">autoscaling/v2beta2</code>는 Kubernetes 1.26부터 제공되지 않으므로 현재 API인 <code class="language-plaintext highlighter-rouge">autoscaling/v2</code>를 사용한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling/v2</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">HorizontalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">scaleTargetRef</span><span class="pi">:</span>
    <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-hpa</span>
  <span class="na">minReplicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">maxReplicas</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">metrics</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
      <span class="na">resource</span><span class="pi">:</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">cpu</span>
        <span class="na">target</span><span class="pi">:</span>
          <span class="na">type</span><span class="pi">:</span> <span class="s">Utilization</span>
          <span class="na">averageUtilization</span><span class="pi">:</span> <span class="m">50</span>
    <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Resource</span>
      <span class="na">resource</span><span class="pi">:</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">memory</span>
        <span class="na">target</span><span class="pi">:</span>
          <span class="na">type</span><span class="pi">:</span> <span class="s">AverageValue</span>
          <span class="na">averageValue</span><span class="pi">:</span> <span class="s">500Mi</span>
  <span class="na">behavior</span><span class="pi">:</span>
    <span class="na">scaleDown</span><span class="pi">:</span>
      <span class="na">stabilizationWindowSeconds</span><span class="pi">:</span> <span class="m">300</span>
      <span class="na">policies</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Percent</span>
          <span class="na">value</span><span class="pi">:</span> <span class="m">100</span>
          <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">15</span>
    <span class="na">scaleUp</span><span class="pi">:</span>
      <span class="na">stabilizationWindowSeconds</span><span class="pi">:</span> <span class="m">0</span>
      <span class="na">policies</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Percent</span>
          <span class="na">value</span><span class="pi">:</span> <span class="m">100</span>
          <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">15</span>
        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Pods</span>
          <span class="na">value</span><span class="pi">:</span> <span class="m">4</span>
          <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">15</span>
      <span class="na">selectPolicy</span><span class="pi">:</span> <span class="s">Max</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">behavior</code>의 주요 항목은 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scaleUp</code></td>
      <td>Replica를 늘릴 때 적용할 정책</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scaleDown</code></td>
      <td>Replica를 줄일 때 적용할 정책</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">policies.type: Percent</code></td>
      <td>현재 Replica 수를 기준으로 허용할 변화 비율</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">policies.type: Pods</code></td>
      <td>일정 시간 동안 변경할 수 있는 Pod 개수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">periodSeconds</code></td>
      <td>정책이 허용하는 변화량을 계산할 기간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">selectPolicy</code></td>
      <td>여러 Policy 중 사용할 Policy 선택</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">stabilizationWindowSeconds</code></td>
      <td>이전 권장값을 고려하여 급격한 변동을 줄이는 시간</td>
    </tr>
  </tbody>
</table>

<p>Scale Up의 <code class="language-plaintext highlighter-rouge">selectPolicy: Max</code>는 <code class="language-plaintext highlighter-rouge">100%</code> 증가와 Pod 네 개 증가 중 더 큰 변화량을 허용한다. <code class="language-plaintext highlighter-rouge">selectPolicy: Disabled</code>를 해당 방향에 지정하면 그 방향의 Scaling을 비활성화할 수 있다.</p>

<p>변경된 HPA를 적용하고 실제 설정을 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> nginx-hpa-autoscaler.yaml
kubectl get hpa nginx-hpa <span class="nt">-o</span> yaml
</code></pre></div></div>

<h2 id="22--vertical-pod-autoscaler">22 ) Vertical Pod Autoscaler</h2>

<hr />

<blockquote>
  <p><strong>Vertical Pod Autoscaler(VPA)</strong></p>

  <p>Container의 실제 Resource 사용량을 분석하여 CPU·Memory Request 권장값을 계산하고, Update Mode에 따라 이를 Pod에 적용하는 Autoscaler이다.</p>
</blockquote>

<p>HPA는 주로 Replica 수를 늘리거나 줄이는 Horizontal Scaling을 담당한다. VPA는 Container 하나에 필요한 CPU·Memory Request를 조정하는 Vertical Scaling을 담당한다. Request가 실제 사용량보다 지나치게 작으면 성능과 Scheduling이 불안정해질 수 있고, 지나치게 크면 Worker의 Resource가 낭비될 수 있다.</p>

<p>VPA는 Kubernetes 기본 구성 요소가 아니므로 별도로 설치해야 한다. 설치 후에는 다음 구성 요소가 협력한다.</p>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Recommender</td>
      <td>Resource 사용 이력을 바탕으로 Request 권장값 계산</td>
    </tr>
    <tr>
      <td>Updater</td>
      <td>Update Mode에 따라 기존 Pod를 교체할지 판단</td>
    </tr>
    <tr>
      <td>Admission Controller</td>
      <td>새 Pod 생성 시 VPA 권장값을 Pod Request에 반영</td>
    </tr>
  </tbody>
</table>

<p>VPA 설치가 필요한 환경에서는 공식 Autoscaler 저장소를 내려받아 설치 스크립트를 실행한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/kubernetes/autoscaler.git
<span class="nb">cd </span>autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh
</code></pre></div></div>

<p>TLS 인증서 오류가 발생하면 원인을 해결해야 한다. 전역 Git 설정에서 TLS 검증을 비활성화하면 이후 모든 Repository 연결의 검증이 생략되므로 사용하지 않는다.</p>

<p>CRD와 VPA 구성 요소를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get crd verticalpodautoscalers.autoscaling.k8s.io
kubectl get pods <span class="nt">-n</span> kube-system | <span class="nb">grep </span>vpa
kubectl api-resources | <span class="nb">grep</span> <span class="nt">-i</span> verticalpodautoscaler
</code></pre></div></div>

<p>VPA 설치 방식과 지원하는 Update Mode는 사용하는 VPA Release와 Kubernetes Version에 따라 확인해야 한다.</p>

<h2 id="23--vpa-recommendation-실습">23 ) VPA Recommendation 실습</h2>

<hr />

<p>작은 Request를 지정한 Deployment를 생성한다. 다음 내용을 <code class="language-plaintext highlighter-rouge">vpa-deployment.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-vpa</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">nginx-vpa</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">nginx-vpa</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:stable</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s">50m</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">64Mi</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s">512Mi</span>
          <span class="na">ports</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>기존 Pod를 변경하지 않고 Recommendation만 확인하도록 다음 내용을 <code class="language-plaintext highlighter-rouge">nginx-vpa-autoscaler.yaml</code>로 저장한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">autoscaling.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">VerticalPodAutoscaler</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-vpa</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">targetRef</span><span class="pi">:</span>
    <span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-vpa</span>
  <span class="na">updatePolicy</span><span class="pi">:</span>
    <span class="na">updateMode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Off"</span>
</code></pre></div></div>

<p>두 파일을 적용한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> vpa-deployment.yaml
kubectl apply <span class="nt">-f</span> nginx-vpa-autoscaler.yaml
kubectl get deployment,pods,vpa
</code></pre></div></div>

<p>VPA가 사용량을 관찰하고 Recommendation을 계산할 시간이 필요하다. 일정 시간 뒤 결과를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe vpa nginx-vpa
kubectl get vpa nginx-vpa <span class="se">\</span>
  <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.status.recommendation}'</span>
</code></pre></div></div>

<p>Recommendation의 주요 값은 다음과 같다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Lower Bound</code></td>
      <td>안정적인 동작을 위해 권장되는 Resource 범위의 하한</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Upper Bound</code></td>
      <td>권장되는 Resource 범위의 상한</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Target</code></td>
      <td>VPA의 Container Resource Policy를 반영한 Request 권장값</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Uncapped Target</code></td>
      <td>Container Resource Policy를 적용하기 전 사용량 기반 권장값</td>
    </tr>
  </tbody>
</table>

<p>실습의 <code class="language-plaintext highlighter-rouge">Off</code> Mode에서는 Recommendation이 표시되더라도 기존 Pod의 Request는 자동으로 바뀌지 않는다.</p>

<h2 id="24--vpa-update-mode와-hpa-조합">24 ) VPA Update Mode와 HPA 조합</h2>

<hr />

<p>VPA의 Update Mode는 Resource 권장값을 언제 적용할지 결정한다.</p>

<table>
  <thead>
    <tr>
      <th>Update Mode</th>
      <th>동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Off</code></td>
      <td>권장값만 계산하고 Pod Resource를 변경하지 않음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Initial</code></td>
      <td>새 Pod가 생성될 때만 권장값 적용</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Recreate</code></td>
      <td>권장값 적용이 필요하면 기존 Pod를 제거하고 새 Pod에 반영</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InPlaceOrRecreate</code></td>
      <td>가능한 경우 실행 중인 Pod를 변경하고, 불가능하면 Pod를 교체</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InPlace</code></td>
      <td>지원되는 Resource를 실행 중인 Pod에 직접 변경</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">Auto</code>는 Deprecated 상태이며 현재는 <code class="language-plaintext highlighter-rouge">Recreate</code>와 같은 방식으로 동작한다. 새 설정에는 의도를 명확히 나타내는 Mode를 사용한다. In-place Mode는 Kubernetes와 VPA가 해당 기능을 지원하는지 먼저 확인해야 한다.</p>

<p>VPA와 HPA가 같은 CPU Resource를 동시에 조정하면 서로의 판단에 영향을 줄 수 있다. CPU 사용률 기반 HPA는 현재 사용량을 CPU Request와 비교하는데, VPA가 그 Request를 변경하면 HPA의 계산 기준도 변하기 때문이다.</p>

<table>
  <thead>
    <tr>
      <th>HPA 기준</th>
      <th>VPA 대상</th>
      <th>판단</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CPU 사용률</td>
      <td>CPU Request</td>
      <td>계산 기준이 변하므로 충돌 가능성이 있음</td>
    </tr>
    <tr>
      <td>CPU 사용률</td>
      <td>Memory Request</td>
      <td>서로 다른 Resource를 조정하므로 역할 분리가 가능함</td>
    </tr>
    <tr>
      <td>Request 수·Queue 길이 같은 외부 Metric</td>
      <td>CPU·Memory Request</td>
      <td>Replica 수와 개별 Pod Resource의 판단 기준을 분리할 수 있음</td>
    </tr>
    <tr>
      <td>CPU 사용률</td>
      <td><code class="language-plaintext highlighter-rouge">Off</code> Mode의 CPU·Memory Recommendation</td>
      <td>자동 변경 없이 권장값을 검토할 수 있음</td>
    </tr>
  </tbody>
</table>

<p>운영 환경에서는 Application 특성, Pod 교체 영향과 HPA Metric을 확인한 뒤 Mode와 조정 대상을 결정한다.</p>

<blockquote>
  <p><strong>중간 정리</strong></p>

  <ul>
    <li>
      <p>HPA Controller는 Metric을 기준으로 Workload Replica 수를 조정한다.</p>
    </li>
    <li>
      <p>VPA는 Container의 Resource 사용량을 분석하여 CPU·Memory Request 권장값을 계산한다.</p>
    </li>
    <li>
      <p>같은 Resource를 기준으로 HPA와 VPA를 함께 사용하면 계산 기준이 서로 영향을 줄 수 있다.</p>
    </li>
  </ul>
</blockquote>

<h2 id="25--실습-resource-정리">25 ) 실습 Resource 정리</h2>

<hr />

<p>HPA와 VPA 실습 Resource를 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get deployment,service,hpa,vpa
kubectl get pods
</code></pre></div></div>

<p>HPA 실습 Resource를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-f</span> nginx-hpa-autoscaler.yaml
kubectl delete <span class="nt">-f</span> nginx-hpa-service.yaml
kubectl delete <span class="nt">-f</span> nginx-hpa.yaml
kubectl delete pod <span class="se">\</span>
  load-generator load-generator1 load-generator2 <span class="se">\</span>
  <span class="nt">--ignore-not-found</span>
</code></pre></div></div>

<p>VPA 실습 Resource를 삭제한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete <span class="nt">-f</span> nginx-vpa-autoscaler.yaml
kubectl delete <span class="nt">-f</span> vpa-deployment.yaml
</code></pre></div></div>

<p>VPA 자체는 다른 Workload에서도 사용할 수 있으므로 이 실습만을 이유로 설치 구성 요소까지 제거하지 않는다.</p>

<p>Resource 관리 실습용 Namespace 안의 Resource도 확인한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get all <span class="nt">-n</span> resource-lab
kubectl get limitranges,resourcequotas <span class="se">\</span>
  <span class="nt">-n</span> resource-lab
kubectl get configmaps <span class="nt">-n</span> resource-lab
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">resource-lab</code>이 이 실습에만 사용됐는지 확인한 뒤 Namespace를 삭제한다. Namespace를 삭제하면 그 안의 Resource도 함께 삭제된다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete namespace resource-lab
</code></pre></div></div>

<h2 id="전체-정리">전체 정리</h2>

<hr />

<blockquote>
  <p><strong>최종 정리</strong></p>

  <ul>
    <li>
      <p>Request는 Scheduler의 Pod 배치 기준이고 Limit은 실행 중인 Container에 적용할 Resource 상한이다.</p>
    </li>
    <li>
      <p>CPU Limit은 Throttling으로 적용되고 Memory Limit은 OOM Kill로 이어질 수 있다.</p>
    </li>
    <li>
      <p>kubelet은 Memory와 Disk 등의 Node-pressure를 감지하여 Threshold와 Pod 우선순위에 따라 Eviction한다.</p>
    </li>
    <li>
      <p>LimitRange는 개별 Object의 기본값과 허용 범위를 제어하고 ResourceQuota는 Namespace 전체 사용량과 Object 수를 제한한다.</p>
    </li>
    <li>
      <p>Cluster Autoscaler는 배치되지 못한 Pod의 Request를 기준으로 Node 확장을 판단하고 HPA는 Metric을 기준으로 Workload Replica를 조정한다.</p>
    </li>
    <li>
      <p>HPA는 Metric을 기준으로 Replica 수를 조정하고 VPA는 CPU·Memory Request 권장값을 계산하거나 적용한다.</p>
    </li>
    <li>
      <p>HPA와 VPA를 함께 사용할 때는 같은 Resource가 양쪽의 판단 기준이 되지 않도록 Metric과 조정 대상을 구분한다.</p>
    </li>
    <li>
      <p>다음 글인 <a href="/cloud-native-37-kubernetes-health-check-restart-policy/">Kubernetes Health Check와 restartPolicy</a>에서는 kubelet이 Container 상태를 점검하고 실패에 대응하는 과정을 다룬다.</p>
    </li>
  </ul>
</blockquote>]]></content><author><name></name></author><category term="CloudNative" /><category term="AutoEverSW" /><category term="Kubernetes" /><summary type="html"><![CDATA[Container의 Resource 제한과 Namespace별 할당량, HPA·VPA의 Metric 기반 Autoscaling 동작과 실습 정리]]></summary></entry></feed>