<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>반달곰의 개발이야기</title>
    <link>https://halfmoonbearlog.tistory.com/</link>
    <description>꾸준히 성실하게 걷고 싶습니다.
지속 가능한 열정을 추구합니다.</description>
    <language>ko</language>
    <pubDate>Sat, 9 May 2026 05:09:14 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>반달bear</managingEditor>
    <image>
      <title>반달곰의 개발이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/5500623/attach/157136cfd3fb499eb6bbae73cf85db21</url>
      <link>https://halfmoonbearlog.tistory.com</link>
    </image>
    <item>
      <title>쿠버네티스 컨테이너 런타임 비교 정리 &amp;mdash; 도커 지원 중단과 CRI, OCI, shim</title>
      <link>https://halfmoonbearlog.tistory.com/114</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 1.24부터 kubelet이 Docker를 직접 지원하지 않습니다 (정 docker를 사용하겠다면 cri-dockerd로 도커를 계속 사용할 수는 있습니다). 그래서 찾다보면 도커의 대체제로 다양한 컨테이너 런타임이 존재합니다. containerd, CRI-O, 부터 runC, Podman, nerdctl, crictl, ctr, shim, OCI, CRI와 같은 어떤 역할인지 알지 못하는 개념들이 많이 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 쿠버네티스와 컨테이너 런타임 사이에 왜 표준(OCI, CRI)이 필요했는지부터 시작해서, 그 표준 위에 올라간 구현체들이 각자 어떻게 서로를 호출하는지, 마지막에는 그래서 뭘 골라야 하는가까지 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 쿠버네티스는 OCI와 CRI 위에서 컨테이너를 띄운다&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U4KSe/dJMcaciyqxs/upHm266Bei7B6CBKfQS33k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U4KSe/dJMcaciyqxs/upHm266Bei7B6CBKfQS33k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U4KSe/dJMcaciyqxs/upHm266Bei7B6CBKfQS33k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU4KSe%2FdJMcaciyqxs%2FupHm266Bei7B6CBKfQS33k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;873&quot; height=&quot;429&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;429&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CRI &amp;mdash; kubelet과 컨테이너 런타임 사이의 규격 (일종의 인터페이스)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet은 컨테이너를 직접 띄우지 않습니다. 컨테이너 런타임에게 &lt;b&gt;gRPC로 요청을 던집니다&lt;/b&gt;. &quot;이 이미지로 컨테이너 만들어줘&quot;, &quot;이거 로그 보여줘&quot;, &quot;이거 멈춰&quot;. 이 gRPC 규격이 CRI(Container Runtime Interface)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRI는 2016년쯤 쿠버네티스 프로젝트가 직접 만들었습니다. 그 전에는 kubelet이 특정 런타임(도커)에 직접 묶여 있어서, 다른 런타임을 지원하려면 kubelet 코드를 수정해야 했습니다. CRI가 생긴 뒤로는 &lt;b&gt;kubelet을 다시 컴파일하지 않아도 CRI를 구현한 런타임이면 뭐든 붙일 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRI가 정의하는 RPC는 대략 이런 것들입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 pull&lt;/li&gt;
&lt;li&gt;Pod 생성 / 제거&lt;/li&gt;
&lt;li&gt;컨테이너 생성 / 시작 / 중지&lt;/li&gt;
&lt;li&gt;컨테이너&amp;middot;Pod 상태 조회&lt;/li&gt;
&lt;li&gt;로그 스트림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OCI &amp;mdash; CRI와 커널(OS)를 연결하는 규격 (일종의 인터페이스)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRI 요청을 받은 런타임은 커널(OS)에서 namespace를 만들고, cgroup을 걸고, 파일 시스템을 마운트한 뒤 프로세스를 fork/exec 합니다. 이 저수준 작업의 규격이 OCI(Open Container Initiative)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI는 리눅스 재단 산하 단체로, 두 가지를 표준화했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;런타임 스펙&lt;/b&gt; &amp;mdash; 특정 설정 파일(bundle)을 주면 컨테이너를 이렇게 띄워야 한다는 규격&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 스펙&lt;/b&gt; &amp;mdash; 컨테이너 이미지의 레이어 구조, 메타데이터 포맷&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;OCI 덕분에&amp;nbsp;도커로 빌드한 이미지를 containerd나 CRI-O에서 그대로 사용할 수 있습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 표준의 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 쿠버네티스와 커널 사이에 표준이 두 개 겹쳐 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[ kubelet ]
     │
     │  &amp;larr; CRI (kubelet &amp;harr; 런타임)
     ▼
[ 컨테이너 런타임 ]
     │
     │  &amp;larr; OCI (런타임 &amp;harr; 커널, 이미지 포맷)
     ▼
[ namespace + cgroup = 컨테이너(프로세스) ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 표준 덕분에 kubelet은 어떤 런타임이 밑에 있는지 몰라도 되고 런타임은 어느 이미지 빌더가 만든 이미지인지 몰라도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. shim이란?&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker-shim, containerd-shim 과 같이 컨테이너 런타임 조사를 하다보면 shim이란 단어가 많이 나옵니다. shim이란 단어를 범용적으로 사용하는데이름이 비슷해서 혼동하기 쉽지만 기술별로 가지는 의미가 다릅니다. 대표적으로 containerd-shim docker-shim을 다뤄보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;containerd-shim - 고수준(containerd)과 저수준(runC) 사이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 컨테이너를 띄우는 저수준 도구는 보통 한 번 실행되고 종료되도록 설계돼 있습니다(뒤에서 자세히 설명할 runC가 그렇습니다). 컨테이너를 띄운 도구는 곧바로 죽고, 띄워진 컨테이너 프로세스만 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 질문이 생깁니다. &lt;b&gt;컨테이너의 부모는 누구인가?&lt;/b&gt; 리눅스에서 부모 프로세스가 죽으면 자식은 PID 1(init)에 입양됩니다. 그런데 컨테이너 입장에서는 로그는 누가 모으고, 종료 코드는 누가 받아주고, 고수준 런타임 데몬(containerd 같은 것)이 재시작됐을 때 &quot;이 컨테이너는 내 거야&quot;라고 누가 주장해줄지 문제가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 빈자리를 메우는 게 &lt;b&gt;containerd-shim&lt;/b&gt;입니다. containerd-shim은 컨테이너마다 하나씩 붙어서 구동이 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저수준 런타임은 컨테이너를 띄우고 바로 종료&lt;/li&gt;
&lt;li&gt;shim이 컨테이너 프로세스의 부모 역할을 이어받음&lt;/li&gt;
&lt;li&gt;고수준 런타임 데몬(containerd 프로세스라고 이해해도 됩니다)이 재시작돼도 shim은 독립적으로 살아 있어서 컨테이너가 고아가 되지 않음&lt;/li&gt;
&lt;li&gt;컨테이너의 stdout/stderr을 받아두고, 종료 코드를 기다렸다가 데몬에 보고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;containerd-shim은 컨테이너의 라이프사이클을 데몬과 분리시키기 위해 끼어 있는 얇은 프로세스입니다. 초기 모델에서는 컨테이너마다 shim이 하나씩 떴고, 현재의 shim v2에서는 보통 Pod(샌드박스)당 하나의 shim이 떠서 같은 Pod 안의 컨테이너들을 함께 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dockershim &amp;mdash; kubelet과 고수준(Docker) 사이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shim이라는 이름이 쿠버네티스 맥락에서 또 등장하는데 여기선 다른 의미를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스가 CRI를 만들었을 때 containerd와 CRI-O는 이걸 구현했습니다. 그런데 도커는 CRI를 구현하지 않은 상태였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 쿠버네티스 프로젝트가 직접 어댑터를 만들어서 kubelet 안에 넣었습니다. 이것이 &lt;b&gt;docker-shim&lt;/b&gt;입니다. kubelet에서 CRI 호출이 나가면 docker-shim이 받아서 도커 API로 번역해 dockerd에 전달합니다. (1.24에서 제거됐습니다, 이유는 뒤에서)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 shim의 공통점과 차이점&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;containerd-shim&lt;/th&gt;
&lt;th&gt;dockershim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;위치&lt;/td&gt;
&lt;td&gt;런타임 &amp;harr; 컨테이너 프로세스&lt;/td&gt;
&lt;td&gt;kubelet &amp;harr; 도커&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;컨테이너 라이프사이클을 데몬과 분리&lt;/td&gt;
&lt;td&gt;CRI를 도커 API로 번역&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;현재 상태&lt;/td&gt;
&lt;td&gt;컨테이너 하나당 하나씩 상시 동작&lt;/td&gt;
&lt;td&gt;1.24에서 제거됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 고수준 컨테이너 런타임 &amp;mdash; Docker, containerd, CRI-O&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 런타임은 실제로는 두 층으로 나뉘어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;저수준 런타임&lt;/b&gt; &amp;mdash; 실제로 namespace/cgroup을 설정하고 프로세스를 fork/exec합니다. 대표적으로 &lt;b&gt;runC&lt;/b&gt;. OCI 런타임 스펙을 구현합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고수준 런타임&lt;/b&gt; &amp;mdash; 그 위에서 이미지 pull, 컨테이너 라이프사이클 관리, API 제공 같은 &lt;b&gt;저수준 런타임 운영에 필요한 일&lt;/b&gt;을 합니다. 대표적으로 Docker, containerd, CRI-O. 데몬 프로세스로 돌면서 내부에서 저수준 런타임(runC)을 호출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;1110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tuLmC/dJMcaa57cHm/0XdVqC9UiT5Qn2oBwp3JY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tuLmC/dJMcaa57cHm/0XdVqC9UiT5Qn2oBwp3JY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tuLmC/dJMcaa57cHm/0XdVqC9UiT5Qn2oBwp3JY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtuLmC%2FdJMcaa57cHm%2F0XdVqC9UiT5Qn2oBwp3JY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1378&quot; height=&quot;1110&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;1110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker&lt;img src=&quot;Files/image%202.png&quot; alt=&quot;&quot; /&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커는 원래 CLI, 데몬, 이미지 빌드, 레지스트리 통신, 컨테이너 실행, 저수준 커널 조작까지 다 하는 하나의 덩어리였습니다. 그러다 규모가 커지면서 내부가 쪼개지기 시작했습니다. 저수준 부분은 &lt;b&gt;runC&lt;/b&gt;로 분리되어 나갔고, 고수준의 상당 부분은 &lt;b&gt;containerd&lt;/b&gt;로 분리되어 나갔습니다. 지금의 도커는 내부적으로 이렇게 생겼습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0xhhQ/dJMcajhDcOu/axIQtkSF03Djo2VZOfDzqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0xhhQ/dJMcajhDcOu/axIQtkSF03Djo2VZOfDzqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0xhhQ/dJMcajhDcOu/axIQtkSF03Djo2VZOfDzqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0xhhQ%2FdJMcajhDcOu%2FaxIQtkSF03Djo2VZOfDzqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;388&quot; height=&quot;564&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;docker&lt;/code&gt; &amp;mdash; 사용자가 치는 CLI&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dockerd&lt;/code&gt; &amp;mdash; Docker 데몬. CLI 요청을 받아서 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-containerd&lt;/code&gt; &amp;mdash; containerd를 얇게 래핑한 것&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-runc&lt;/code&gt; &amp;mdash; runC를 얇게 래핑한 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker run nginx&lt;/code&gt;를 치면 네 개 계층을 거칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커는 왜 쿠버네티스에서 빠졌나&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;1183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dunurh/dJMcahYsb0o/WsVZMvpLXXu2GInNFtFLm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dunurh/dJMcahYsb0o/WsVZMvpLXXu2GInNFtFLm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dunurh/dJMcahYsb0o/WsVZMvpLXXu2GInNFtFLm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdunurh%2FdJMcahYsb0o%2FWsVZMvpLXXu2GInNFtFLm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;1183&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;1183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커의 문제는 하나였습니다. &lt;b&gt;CRI를 직접 구현하지 않습니다.&lt;/b&gt; 그래서 쿠버네티스는 docker-shim이라는 어댑터를 별도로 유지해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 도커가 이미 내부적으로 containerd를 쓰고 있다면, kubelet &amp;rarr; docker-shim &amp;rarr; dockerd &amp;rarr; docker-containerd를 거쳐서 간접적으로 containerd를 부르는 셈입니다. docker-shim(kubelet과 도커 프로세스/데몬을 연결), dockerd(docker-shim과 docker-containerd를 연결)이 중복인 셈입니다. kubelet이 containerd를 직접 부르면 깔끔합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 쿠버네티스는 1.24에서 docker-shim을 제거했습니다. (도커로 빌드한 이미지는 OCI 표준이라 containerd에서 그대로 돌아갑니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 도커는 래핑 바이너리가 아니라 표준인 containerd/runc를 그대로 호출합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;containerd &amp;mdash; 도커에서 독립한 범용 런타임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;containerd&lt;/b&gt;는 원래 도커의 일부였다가 분리되어 나와 CNCF 프로젝트가 됐고, 지금은 졸업 프로젝트입니다. 범용적으로 설계돼 있어서 쿠버네티스 외의 용도로도 쓸 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;CRI 표준이 없었을 때&lt;/b&gt; &amp;mdash; containerd 자체는 CRI를 몰랐고, 별도의 &lt;code&gt;cri-containerd&lt;/code&gt; 데몬이 kubelet과 containerd 사이에 다리를 놓음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CRI 표준이 생기고 난 후&lt;/b&gt; &amp;mdash; containerd에 CRI 플러그인이 내장되어 kubelet이 직접 containerd를 호출&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CRI-O &amp;mdash; 처음부터 쿠버네티스 전용으로 설계된 경량 런타임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CRI-O&lt;/b&gt;는 레드햇에서 만든 런타임인데, 이름에서 드러나듯 &lt;b&gt;처음부터 CRI 구현만을 목적으로&lt;/b&gt; 만들어졌습니다. 쿠버네티스 전용이라고 보면 됩니다. containerd와 달리 범용 용도를 버린 대신 경량화돼 있고, kubelet &amp;rarr; CRI-O &amp;rarr; runC &amp;rarr; 컨테이너의 호출 경로가 가장 짧습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet이 컨테이너를 띄울 때 거치는 경로는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biwPbA/dJMcaib1wU3/1YOMQdLNXEFc7nrsfg50sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biwPbA/dJMcaib1wU3/1YOMQdLNXEFc7nrsfg50sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biwPbA/dJMcaib1wU3/1YOMQdLNXEFc7nrsfg50sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiwPbA%2FdJMcaib1wU3%2F1YOMQdLNXEFc7nrsfg50sK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;830&quot; height=&quot;688&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;img src=&quot;Files/image%205.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker-shim 경유&lt;/b&gt; &amp;mdash; kubelet &amp;rarr; docker-shim &amp;rarr; Dockerd(도커 엔진) &amp;rarr; containerd &amp;rarr; runC &amp;rarr; 컨테이너. 1.24에서 제거. 중간이 너무 많음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Containerd + CRI Plugin&lt;/b&gt; &amp;mdash; kubelet &amp;rarr; containerd(CRI Plugin 내장) &amp;rarr; containerd-shim &amp;rarr; runC &amp;rarr; 컨테이너. 현재 표준.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CRI-O&lt;/b&gt; &amp;mdash; kubelet &amp;rarr; CRI-O &amp;rarr; runC &amp;rarr; 컨테이너.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Podman은 뭘까?&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료를 찾아보면 RedHat에서 공식적으로 지원하는 툴은 Podman이라는 말이 자주 보이는데 Podman은 어디에 들어가는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론부터 말하면 Podman은 쿠버네티스 노드 런타임 후보가 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Files/image%206.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d9PX0p/dJMcaduVnch/AlaDkNaqR6ayn5SDkGiYu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d9PX0p/dJMcaduVnch/AlaDkNaqR6ayn5SDkGiYu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d9PX0p/dJMcaduVnch/AlaDkNaqR6ayn5SDkGiYu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9PX0p%2FdJMcaduVnch%2FAlaDkNaqR6ayn5SDkGiYu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;514&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Podman이 푸는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Podman은 레드햇이 만든 OCI 호환 컨테이너 엔진입니다. 가장 큰 특징은 &lt;b&gt;데몬리스&lt;/b&gt;라는 점입니다. 도커처럼 dockerd 같은 중앙 데몬을 두지 않고, &lt;code&gt;podman run&lt;/code&gt; 명령이 현재 사용자 프로세스로 직접 runC를 호출해서 컨테이너를 만듭니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중앙 데몬 없음 &amp;rarr; rootless 실행이 자연스러움&lt;/li&gt;
&lt;li&gt;CLI가 도커와 호환됨 &amp;rarr; &lt;code&gt;alias docker=podman&lt;/code&gt;으로 거의 그대로 대체 가능&lt;/li&gt;
&lt;li&gt;별도의 고수준 런타임 레이어 없이 Podman 자체가 그 역할을 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데몬이 없다는 건 &quot;항상 떠 있는 컨테이너&quot;를 관리할 주체가 없다는 뜻이기도 합니다. Podman은 이 문제를 systemd에 서비스로 등록하는 방식으로 풉니다. &lt;code&gt;podman generate systemd&lt;/code&gt;로 서비스 파일을 뽑아 systemd에 맡기면, 부팅 시 자동 시작이나 실패 시 재시작을 systemd가 대신 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Podman 생태계는 한 덩어리가 아니라 작은 도구들이 역할을 나눠 가집니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Files/image%207.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;1028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tt0ZS/dJMcacbMpYK/ePcUjqZLZUQ2CYK613zAr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tt0ZS/dJMcacbMpYK/ePcUjqZLZUQ2CYK613zAr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tt0ZS/dJMcacbMpYK/ePcUjqZLZUQ2CYK613zAr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftt0ZS%2FdJMcacbMpYK%2FePcUjqZLZUQ2CYK613zAr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;878&quot; height=&quot;1028&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;1028&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;buildah&lt;/b&gt; &amp;mdash; 이미지 빌드 담당. &lt;code&gt;docker build&lt;/code&gt;를 대체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;skopeo&lt;/b&gt; &amp;mdash; 이미지 레지스트리 간 전송 담당. &lt;code&gt;docker push&lt;/code&gt;/&lt;code&gt;docker pull&lt;/code&gt; 영역을 대체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설계는 전부 &lt;b&gt;단일 호스트 사용 사례&lt;/b&gt;에 맞춰져 있습니다. 개발자 로컬 머신, CI 러너, 단일 서버에서 도커를 대체하는 용도입니다. RHEL 계열에 기본 탑재된 이유도 이것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Podman은 왜 쿠버네티스 노드 런타임이 될 수 없는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet이 노드에서 컨테이너를 띄울 때는 CRI gRPC 규격으로 런타임을 호출합니다. &lt;b&gt;Podman은 CRI를 구현하지 않습니다.&lt;/b&gt; 그래서 kubelet이 호출할 수가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Podman이 쿠버네티스와 엮이는 경로가 없진 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;podman generate kube&lt;/code&gt; &amp;mdash; 로컬에서 띄운 컨테이너를 쿠버네티스 YAML로 뽑아줌&lt;/li&gt;
&lt;li&gt;&lt;code&gt;podman play kube&lt;/code&gt; &amp;mdash; 쿠버네티스 YAML을 Podman이 단일 호스트에서 실행 (mini-k8s 흉내)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 개발 편의 도구지, 실제 클러스터 노드 런타임이 아닙니다. &lt;b&gt;같은 회사(레드햇)에서 쿠버네티스 노드 런타임으로 미는 건 Podman이 아니라 CRI-O입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Podman과 containerd/CRI-O가 나란히 비교되는 표가 종종 있는데 사실 층위가 다릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;containerd, CRI-O&lt;/b&gt;: 쿠버네티스 노드 런타임 (CRI 구현)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Podman&lt;/b&gt;: 단일 호스트용 컨테이너 엔진 (CRI 미구현, 도커 CLI 대체재)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CLI는 뭘 사용해야 할까?&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임을 containerd로 바꾸기로 했다고 하면 다음 질문이 나옵니다. 그럼 &lt;code&gt;docker ps&lt;/code&gt;, &lt;code&gt;docker logs&lt;/code&gt; 대신 뭘 쓰지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커에서는 &lt;code&gt;docker&lt;/code&gt;가 CLI이고 &lt;code&gt;dockerd&lt;/code&gt;가 데몬이었습니다. 그런데 쿠버네티스 노드에서 실제로 컨테이너를 띄우는 주체는 사람이 아니라 kubelet입니다. containerd만 깔려 있으면 쿠버네티스는 정상 동작합니다. CLI는 사실 필수가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 운영자가 노드에 들어가서 확인할 일이 있기 때문에 CLI는 필요합니다. containerd 환경에서 쓸 수 있는 CLI는 세 가지입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;crictl &amp;mdash; 쿠버네티스 표준 디버깅 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRI gRPC를 직접 호출하는 CLI입니다. &lt;b&gt;kubelet이 보는 것과 같은 관점&lt;/b&gt;으로 컨테이너를 봅니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;crictl ps              # kubelet이 관리하는 컨테이너 목록
crictl pods            # Pod 목록
crictl logs &amp;lt;id&amp;gt;       # 컨테이너 로그
crictl exec -it &amp;lt;id&amp;gt; sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kubelet이 만든 것만 보이고&lt;/b&gt; 이미지 빌드는 못 합니다. 쿠버네티스 관점에서 사용할 수 있는 CLI입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ctr &amp;mdash; containerd 네이티브 CLI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;containerd에 기본으로 딸려오는 CLI입니다. 굉장히 저수준이고 containerd의 모든 네임스페이스를 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;ctr -n k8s.io containers list
ctr images pull ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;-n k8s.io&lt;/code&gt;를 안 붙이면 쿠버네티스 컨테이너가 안 보이고&lt;/b&gt; 도커와 명령어 체계가 달라서 추가 학습이 필요합니다. 일상적으로 쓰기는 불편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nerdctl &amp;mdash; Docker 호환 CLI for containerd&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;containerd 공식 서브프로젝트입니다. &lt;b&gt;도커 CLI 문법을 그대로 containerd에 사용할 수 있게&lt;/b&gt; 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;nerdctl run -d nginx
nerdctl ps
nerdctl images
nerdctl build -t myapp .
nerdctl login my-registry.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker&lt;/code&gt;를 &lt;code&gt;nerdctl&lt;/code&gt;로 바꾸면 거의 다 동작합니다. 도커에 익숙한 운영자가 containerd 환경으로 가장 저항감 없이 넘어갈 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 뭘 골라야 할까?&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;노드 런타임: containerd&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker는 공식 탈락.&lt;/b&gt; 1.24에서 dockershim이 제거됨. 쓰려면 별도의 &lt;code&gt;cri-dockerd&lt;/code&gt;를 운영해야 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Podman은 애초에 후보가 아님.&lt;/b&gt; CRI를 구현하지 않아 kubelet이 호출 불가. 단일 호스트용 도구.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CRI-O는&lt;/b&gt; 기능은 containerd와 거의 동등하지만 생태계가 좁음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;containerd는 CNCF 졸업 프로젝트&lt;/b&gt;로 EKS, GKE, AKS의 기본 런타임이고, Kubespray의 기본값이기도 함. 범용성과 문서, 트러블슈팅 자료가 가장 두터움.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubespray로 에어갭 환경에서 클러스터를 올리는 상황이면, 기본값을 바꾸지 않고 그대로 쓰는 게 맞습니다. 프라이빗 레지스트리 구성(&lt;code&gt;library/&lt;/code&gt; 프리픽스 문제, insecure registry 설정)도 containerd 기준으로 맞춰두면 혹시라도 추후에 컨테이너 런타임을 변경하거나 런타임을 직접 개발하더라도 고도화 하기에 편리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CLI: nerdctl, 필요하면 ctr&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;nerdctl&lt;/code&gt;&lt;/b&gt; &amp;mdash; 도커처럼 쓰고 싶을 때 (이미지 pull/push, 레지스트리 로그인, 수동 실행)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;ctr&lt;/code&gt;&lt;/b&gt; &amp;mdash; 가끔 저수준 확인 (네임스페이스별 조회 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 스택&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubelet
  │ CRI
  ▼
containerd (CRI Plugin 내장)
  │
  ▼
containerd-shim (컨테이너마다 하나씩)
  │ OCI
  ▼
runC
  │
  ▼
namespace + cgroup = 컨테이너
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조자료&lt;/p&gt;
&lt;div&gt;&lt;a href=&quot;https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/9/html/building_running_and_managing_containers/selecting-a-container-runtime_building-running-and-managing-containers&quot;&gt;https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/9/html/building_running_and_managing_containers/selecting-a-container-runtime_building-running-and-managing-containers&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://mkdev.me/posts/the-tool-that-really-runs-your-containers-deep-dive-into-runc-and-oci-specifications&quot;&gt;https://mkdev.me/posts/the-tool-that-really-runs-your-containers-deep-dive-into-runc-and-oci-specifications&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://mkdev.me/posts/dockerless-part-3-moving-development-environment-to-containers-with-podman&quot;&gt;https://mkdev.me/posts/dockerless-part-3-moving-development-environment-to-containers-with-podman&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=sK5i-N34im8&quot;&gt;https://www.youtube.com/watch?v=sK5i-N34im8&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://velog.io/@yange/Kubernetes-container-runtime%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC&quot;&gt;https://velog.io/@yange/Kubernetes-container-runtime%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/9/html/building_running_and_managing_containers/selecting-a-container-runtime_building-running-and-managing-containers&quot;&gt;https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/9/html/building_running_and_managing_containers/selecting-a-container-runtime_building-running-and-managing-containers&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://atl.kr/dokuwiki/doku.php/podman_%EB%A1%9C%EC%BB%AC_%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88_%EB%9F%B0%ED%83%80%EC%9E%84%EC%97%90%EC%84%9C_%ED%8F%AC%EB%93%9C_%EB%B0%8F_%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88_%EA%B4%80%EB%A6%AC&quot;&gt;https://atl.kr/dokuwiki/doku.php/podman_%EB%A1%9C%EC%BB%AC_%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88_%EB%9F%B0%ED%83%80%EC%9E%84%EC%97%90%EC%84%9C_%ED%8F%AC%EB%93%9C_%EB%B0%8F_%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88_%EA%B4%80%EB%A6%AC&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://blog.naver.com/pjt3591oo/222992244712&quot;&gt;https://blog.naver.com/pjt3591oo/222992244712&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://www.cncf.co.kr/blog/k8s-crio-runc/&quot;&gt;https://www.cncf.co.kr/blog/k8s-crio-runc/&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://labex.io/ko/tutorials/linux-how-to-check-if-a-container-runtime-is-installed-in-linux-558703&quot;&gt;https://labex.io/ko/tutorials/linux-how-to-check-if-a-container-runtime-is-installed-in-linux-558703&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://kimsanghyeon.tistory.com/237&quot;&gt;https://kimsanghyeon.tistory.com/237&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do?bbsId=InstructionGuide_main&amp;amp;nttId=18590&amp;amp;pageIndex=1&quot;&gt;https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do?bbsId=InstructionGuide_main&amp;amp;nttId=18590&amp;amp;pageIndex=1&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://insujang.github.io/2019-10-31/container-runtime/&quot;&gt;https://insujang.github.io/2019-10-31/container-runtime/&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&lt;a href=&quot;https://www.tutorialworks.com/difference-docker-containerd-runc-crio-oci/&quot;&gt;https://www.tutorialworks.com/difference-docker-containerd-runc-crio-oci/&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>개발지식/Ops</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/114</guid>
      <comments>https://halfmoonbearlog.tistory.com/114#entry114comment</comments>
      <pubDate>Wed, 22 Apr 2026 18:24:43 +0900</pubDate>
    </item>
    <item>
      <title>트랜스포머 쉽게 이해하기 (2) - 디코더</title>
      <link>https://halfmoonbearlog.tistory.com/113</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://halfmoonbearlog.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[이전 글]&lt;/a&gt;에서는 영어 문장 &quot;I am studying&quot;이 인코더를 거치면서 어떻게 문맥이 녹아든 벡터로 변환되는지 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 그 벡터를 받아 불어(혹은 독일어) 문장을 한 토큰씩 만들어내는 디코더를 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 문장은 다음과 같이 가정하겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원문(영어): &lt;b&gt;I am studying Transformer&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;번역(독일어): &lt;b&gt;Ich studiere Transformer&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 지금까지 디코더가 &quot;Ich&quot;, &quot;studiere&quot;까지 생성했고, 이제 그 다음 단어 &quot;Transformer&quot;를 예측하려는 상황을 떠올려봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디코더 블록의 전체 구조&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코더 블록이 3단계(멀티헤드 셀프 어텐션 &amp;rarr; FFN &amp;rarr; 잔차 연결/층 정규화)였다면, 디코더 블록은 한 단계가 더 있습니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;입력(독일어 임베딩 + 위치 벡터)
  &amp;darr;
① 마스크드 멀티헤드 셀프 어텐션  (Q=K=V, 모두 독일어)
  &amp;darr;  잔차 연결 + 층 정규화
② 인코더-디코더 어텐션 (크로스 어텐션)
     Q = ①의 출력 (독일어)
     K, V = 인코더 최종 출력 (영어)
  &amp;darr;  잔차 연결 + 층 정규화
③ 앞먹임 신경망 (FFN)
  &amp;darr;  잔차 연결 + 층 정규화
출력(512차원)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 블록을 &lt;b&gt;N=6번&lt;/b&gt; 쌓으면 디코더 하나가 완성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코더와 다른 점은 멀티헤드 어텐션에 &quot;마스크드&quot;가 붙다는 점과 인코더-디코더 어텐션에서 인코더의 출력을 K, V로 받아온다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: 마스크드 멀티헤드 셀프 어텐션&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디코더는 번역을 &lt;b&gt;왼쪽에서 오른쪽으로&lt;/b&gt; 한 단어씩 만들어냅니다. &quot;Ich&quot;를 만들 때는 &quot;studiere&quot;나 &quot;Transformer&quot;가 아직 존재하지 않아야 하고, &quot;studiere&quot;를 만들 때는 &quot;Transformer&quot;를 아직 몰라야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 훈련할 때는 정답 문장 &quot;Ich studiere Transformer&quot;를 한꺼번에 디코더에 집어넣습니다(이래야 학습이 빠릅니다). 이렇게 한꺼번에 넣으면 셀프 어텐션이 &lt;b&gt;미래 단어까지 참고해서&lt;/b&gt; 현재 단어를 예측해 버립니다. 즉 &quot;studiere&quot; 위치에서 &quot;Transformer&quot;를 미리 보고 다음 단어를 맞추는 상황이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마스크는 &quot;어디서&quot; 일어나는가? &amp;mdash; 가장 헷갈리는 부분&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 설명이 &quot;미래 토큰을 마스킹한다&quot;고만 말해서 마치 입력에서 토큰을 빼는 것처럼 느껴지는데 &quot;Ich studiere Transformer&quot; 세 토큰이 디코더에 &lt;b&gt;모두&lt;/b&gt; 들어갑니다. 각 토큰은 자기 몫의 Q, K, V 벡터를 만듭니다. 그 다음 Q와 K를 곱하면 3&amp;times;3 어텐션 스코어 행렬이 나오는데, 바로 이 행렬의 오른쪽 위 삼각형을 `-&amp;infin;`로 덮어버리는 것이 마스킹입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0mfOq/dJMcahqB3Wd/RnrUCYgrhI2WRn6VbyJgNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0mfOq/dJMcahqB3Wd/RnrUCYgrhI2WRn6VbyJgNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0mfOq/dJMcahqB3Wd/RnrUCYgrhI2WRn6VbyJgNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0mfOq%2FdJMcahqB3Wd%2FRnrUCYgrhI2WRn6VbyJgNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;560&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토큰이 사라지는 게 아니라, 어텐션 가중치만 0으로 만듭니다&lt;/b&gt;. 그래서 입력 토큰 수는 항상 3개(또는 문장 길이만큼)이고 출력 벡터도 항상 토큰 수와 동일한 개수만큼 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 왜 &lt;code&gt;-&amp;infin;&lt;/code&gt;로 덮는지가 궁금할 수 있는데, 어텐션 스코어는 마지막에 &lt;b&gt;소프트맥스&lt;/b&gt;를 거칩니다. 소프트맥스는 &lt;code&gt;exp(-&amp;infin;) = 0&lt;/code&gt;이므로, &lt;code&gt;-&amp;infin;&lt;/code&gt;로 마스킹된 자리는 최종 어텐션 가중치에서 정확히 &lt;b&gt;0&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마스킹 이후 각 토큰 벡터에 담기는 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스킹을 거친 후 각 토큰 위치의 출력 벡터가 볼 수 있는 정보는 다음과 같이 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: 인코더-디코더 어텐션 (크로스 어텐션)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스크드 셀프 어텐션이 &lt;b&gt;독일어 문장 내부의 관계&lt;/b&gt;를 본다면, 크로스 어텐션은 &lt;b&gt;독일어 &amp;harr; 영어의 관계&lt;/b&gt;를 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Q&lt;/b&gt;: 마스크드 어텐션의 출력 (독일어에서 옴)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;K, V&lt;/b&gt;: 인코더 최종 출력 (영어에서 옴)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 헷갈릴 수 있는 부분이 인코더 최종 출력 &lt;b&gt;하나를 디코더의 6개 블록이 전부 공유해서&lt;/b&gt; K, V로 쓴다는 것입니다. 인코더는 딱 한 번만 돌고 그 최종 출력을 디코더 6개 블록이 똑같이 참조합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: 앞먹임 신경망(FFN) + 잔차 연결 + 층 정규화&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코더와 동일합니다. 잔차 연결과 층 정규화를 다시 거칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 디코더 블록 하나입니다. 이걸 6번 반복하면 디코더가 완성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계: 선형 층(Linear)과 소프트맥스 &amp;mdash; 다음 단어 예측&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디코더 6개 블록을 다 통과한 결과물은 토큰 개수만큼의 512차원 벡터 (512차원이라는 건 처음 토큰 1개를 몇 차원의 벡터로 정의할지 정하는 과정에서 정해지게 됩니다)입니다. &quot;Ich studiere&quot;를 넣었다면 512차원 벡터 2개가, &quot;Ich studiere Transformer&quot;를 넣었다면 3개가 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제로 다음 단어를 고르려면, 이 512차원 벡터를 &lt;b&gt;어휘 사전 전체(약 3만 개)의 확률 분포&lt;/b&gt;로 변환해야 합니다. 이 역할을 &lt;b&gt;선형 층&lt;/b&gt;이 맡습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;512차원 벡터  ──(512 &amp;times; 30,000 가중치 행렬)──▶  30,000차원 벡터
                                                 │
                                                 ▼
                                            소프트맥스
                                                 │
                                                 ▼
                                      어휘 사전 각 단어의 확률
                                      (가장 큰 값이 다음 단어)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 헷갈리는 질문: 어느 위치의 벡터를 선형 층에 넣어야 하지?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 &quot;Ich&quot;, &quot;studiere&quot;를 만들었고, 그 다음인 &quot;Transformer&quot;를 예측하고 싶다고 합시다. 디코더의 출력 벡터는 2개입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벡터 A: &quot;Ich&quot; 위치의 출력 (512차원)&lt;/li&gt;
&lt;li&gt;벡터 B: &quot;studiere&quot; 위치의 출력 (512차원)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 어떤 걸 선형 층에 넣어야 할까요? &lt;b&gt;정답은 B(= 마지막 위치)의 벡터&lt;/b&gt;입니다. 왜일까요?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벡터 A (&quot;Ich&quot; 위치) = &lt;code&gt;[Ich]&lt;/code&gt; 정보만 담고 있음&lt;/li&gt;
&lt;li&gt;벡터 B (&quot;studiere&quot; 위치) = &lt;code&gt;[Ich, studiere]&lt;/code&gt; 정보를 담고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 A에는 &quot;studiere&quot;가 이미 생성되었다는 사실조차 들어있지 않습니다. 벡터 A를 선형 층에 넣으면 모델은 &quot;Ich 다음에 올 단어는?&quot;이라는 질문에 답하게 되고 그 답은 바로 &quot;studiere&quot;가 될 가능성이 높습니다. 이미 우리가 아는 단어죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 벡터 B는 &quot;Ich studiere까지 봤을 때, 그 다음에 올 단어는?&quot;이라는 질문에 해당합니다. 그 답이 우리가 원하는 &lt;b&gt;Transformer&lt;/b&gt;입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디코더의 각 위치 출력 벡터는 &lt;b&gt;그 위치까지의 문맥을 보고 그 다음 단어를 예측하는 용도&lt;/b&gt;로 만들어집니다. 그래서 &quot;n번째 다음 단어&quot;를 예측하려면 &lt;b&gt;&quot;n번째 위치의 출력 벡터&quot;&lt;/b&gt; 를 선형 층에 넣는 것이고, 현재 시점까지 생성된 마지막 단어의 위치가 곧 &quot;다음 단어를 예측할 자리&quot;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훈련할 때는 벡터 A도 버리지 않고 &quot;studiere&quot;를 맞추는 손실 함수에 사용되지만(모든 위치가 각자 다음 단어를 예측하도록 병렬 학습) &lt;b&gt;추론 시 새 단어를 뽑을 때는&lt;/b&gt; &lt;b&gt;마지막 위치의 벡터&lt;/b&gt;만 사용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름 한 번에 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 본 디코더 블록 구조 &amp;rarr; 마스크드 셀프 어텐션 &amp;rarr; 크로스 어텐션 &amp;rarr; FFN &amp;rarr; 선형 층 &amp;rarr; 소프트맥스 &amp;rarr; 다음 단어 선택의 흐름을, 인코더와 합쳐서 한 장으로 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 뽑힌 다음 단어를 다시 디코더 입력 맨 뒤에 붙여 넣고, 같은 과정을 반복합니다. 문장 종결 토큰(&lt;code&gt;&amp;lt;EOS&amp;gt;&lt;/code&gt;)이 나올 때까지 이 과정이 이어지면서 번역 문장이 완성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;디코더 블록 = 마스크드 셀프 어텐션 + 크로스 어텐션 + FFN&lt;/b&gt;, 각 단계 뒤에 잔차 연결과 층 정규화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마스킹은 토큰을 빼는 게 아니라 어텐션 스코어 행렬의 오른쪽 위 삼각형을 &lt;code&gt;-&amp;infin;&lt;/code&gt;로 만드는 것. &lt;/b&gt;토큰은 전부 들어가고 출력도 전부 나오지만 각 위치가 볼 수 있는 정보가 자기 자신과 그 이전 위치로 제한됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;크로스 어텐션에서는 Q는 디코더, K&amp;middot;V는 인코더에서&lt;/b&gt; 옴. 인코더는 한 번만 돌고 그 최종 출력을 디코더 6개 블록이 모두 공유.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선형 층에 넣는 벡터는 항상 마지막 위치의 벡터&lt;/b&gt;. 마스킹 구조상 n번째 위치는 n번째까지 보고 n+1번째를 예측하는 용도로 만들어졌기 때문.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>개발지식/AI</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/113</guid>
      <comments>https://halfmoonbearlog.tistory.com/113#entry113comment</comments>
      <pubDate>Mon, 20 Apr 2026 19:38:45 +0900</pubDate>
    </item>
    <item>
      <title>GPU는 어떻게 트랜스포머의 행렬 연산을 가속하는가</title>
      <link>https://halfmoonbearlog.tistory.com/112</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝 모델, 특히 트랜스포머(Transformer) 기반 모델을 학습시키거나 추론할 때 GPU가 CPU보다 훨씬 빠르다는 사실은 널리 알려져 있습니다. 하지만 &quot;왜&quot; 빠른지, 그리고 &quot;어떻게&quot; 빠른지를 구체적으로 설명하려고 하면 갑자기 막막해집니다. CUDA 코어, 텐서 코어, FP16, 혼합 정밀도, FLOPS, TFLOPS 같은 용어들이 줄줄이 등장하고, 각 용어의 의미를 찾다 보면 또 다른 낯선 용어를 만나게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 그 용어들을 한 자리에서 하나씩 풀어가며, 최종적으로는 &quot;트랜스포머의 어텐션 연산이 GPU에서 왜 그렇게 빠르게 돌아가는가&quot;라는 질문에 답하는 것을 목표로 합니다. CPU와 GPU의 내부 구조 차이부터 시작해서, 숫자를 표현하는 방식, 연산 유닛의 종류, 그리고 이것들이 트랜스포머의 행렬곱과 어떻게 맞물려 작동하는지까지 이어서 설명하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CPU와 GPU의 구조 차이&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU의 내부 구조&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 Central Processing Unit의 약자로, 컴퓨터의 중앙 처리 장치입니다. CPU 칩 안에는 여러 구성 요소가 들어 있는데, 대표적으로 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;코어(core)&lt;/b&gt; 가 있습니다. 코어는 실제로 명령어를 실행하는 단위입니다. 예전에는 한 칩에 코어가 하나였지만, 지금은 4코어, 8코어, 16코어처럼 한 칩에 여러 개의 코어가 들어 있습니다. 각 코어는 독립적으로 프로그램의 명령어를 읽고 실행할 수 있기 때문에 코어가 많을수록 동시에 처리할 수 있는 작업이 많아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 코어 안에는 &lt;b&gt;ALU(Arithmetic and Logical Unit, 산술 논리 장치)&lt;/b&gt; 가 있습니다. ALU는 덧셈, 뺄셈, 곱셈, 나눗셈 같은 사칙 연산과 AND, OR 같은 논리 연산을 실제로 수행하는 회로입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, &lt;b&gt;캐시 메모리(cache memory)&lt;/b&gt; 가 있습니다. CPU가 매번 RAM(주기억장치)까지 가서 데이터를 가져오면 속도가 느려지기 때문에, 자주 쓰는 데이터는 CPU 바로 옆의 작고 빠른 메모리에 보관해 둡니다. 이것이 캐시 메모리입니다. L1, L2, L3로 계층이 나뉘어 있고, L1이 가장 작고 가장 빠르며, L3가 상대적으로 크고 느립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷째, &lt;b&gt;제어 장치(control unit)&lt;/b&gt; 와 &lt;b&gt;분기 예측기(branch predictor)&lt;/b&gt;, &lt;b&gt;디스패처(dispatcher)&lt;/b&gt; 같은 복잡한 &lt;b&gt;제어용 회로&lt;/b&gt;들이 있습니다. 이들의 역할은 다음에 어떤 명령어를 실행할지, 조건문에서 어느 쪽으로 분기할지, 각 명령어를 어느 연산 유닛에 보낼지를 결정하는 것입니다. 현대 CPU는 한 번에 여러 명령어를 파이프라인 방식으로 처리하면서 분기를 미리 예측해서 실행하기 때문에 이런 제어 회로가 전체 칩 면적의 상당 부분을 차지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CPU 코어 블록 다이어그램 읽는 법&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 코어 내부의 블록 다이어그램을 보면 보통 4~5개의 큰 영역으로 색이 나뉘어 있습니다. 각 영역이 하는 역할을 연결해서 보면 앞에서 설명한 구성 요소들이 어디에 해당하는지 한눈에 보입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1687&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ee9ix/dJMcagSJZGJ/NtqJ7DRmnpd0BkcsNsbCs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ee9ix/dJMcagSJZGJ/NtqJ7DRmnpd0BkcsNsbCs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ee9ix/dJMcagSJZGJ/NtqJ7DRmnpd0BkcsNsbCs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEe9ix%2FdJMcagSJZGJ%2FNtqJ7DRmnpd0BkcsNsbCs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1687&quot; height=&quot;1080&quot; data-origin-width=&quot;1687&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Front End (프론트엔드 영역)&lt;/b&gt;: 다이어그램 위쪽에 크게 자리잡은 영역입니다. 이 안에는 &lt;code&gt;Branch Predictor&lt;/code&gt;(분기 예측기, 때로는 L1 BTB, L2 BTB 같은 세부 표가 함께 표시됨), &lt;code&gt;L1 Instruction Cache&lt;/code&gt;(명령어 캐시), &lt;code&gt;Instruction Queue&lt;/code&gt;, &lt;code&gt;Simple Decoder&lt;/code&gt; &amp;times; 여러 개, &lt;code&gt;&amp;mu;code&lt;/code&gt;, &lt;code&gt;Op Queue&lt;/code&gt;, &lt;code&gt;Rename / Dispatch&lt;/code&gt; 같은 박스가 있습니다. 이 모든 것이 &quot;다음에 어떤 명령어를 어떤 순서로 어디로 보낼지&quot;를 결정하는 제어용 회로입니다. 즉, 넷째 항목에서 말한 복잡한 제어 장치가 다이어그램의 이 넓은 영역 전체에 해당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Execution Engine (실행 엔진 영역)&lt;/b&gt;: 프론트엔드 아래, 중앙에 위치한 큰 회색 영역입니다. &lt;code&gt;ReOrder Buffer&lt;/code&gt;, 여러 개의 &lt;code&gt;Scheduler&lt;/code&gt;(ALU Scheduler, Branch Scheduler, Load Scheduler 등), 그리고 그 아래에 실제 연산을 수행하는 &lt;code&gt;ALU&lt;/code&gt;, &lt;code&gt;ALU Shift&lt;/code&gt;, &lt;code&gt;ALU MUL DIV&lt;/code&gt;, &lt;code&gt;Branch&lt;/code&gt; 박스들이 있습니다. 여기서 실제로 숫자를 더하고 곱하는 &lt;b&gt;ALU는 전체 다이어그램에서 보면 의외로 작은 부분만 차지한다는 것을 볼 수 있습니&lt;/b&gt;다. ALU 몇 개를 움직이기 위해 그 위의 스케줄러와 리오더 버퍼가 훨씬 더 큰 공간을 쓰고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FP / Vector Execution (부동소수점/벡터 실행 영역)&lt;/b&gt;: 왼쪽 아래의 파란색 영역입니다. &lt;code&gt;Vector RF&lt;/code&gt;(벡터 레지스터 파일), 그리고 &lt;code&gt;ALU&lt;/code&gt;, &lt;code&gt;ALU MUL ADD AES SHA DIV&lt;/code&gt; 같은 부동소수점/SIMD 연산 유닛이 들어 있습니다. CPU에서 소수점 연산과 벡터 연산은 정수 ALU와는 별개의 유닛에서 처리됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Load / Store (데이터 입출력 영역)&lt;/b&gt;: 오른쪽 아래의 주황색 영역입니다. &lt;code&gt;Store Queue&lt;/code&gt;, &lt;code&gt;Load Queue&lt;/code&gt;, &lt;code&gt;L1 Data Cache&lt;/code&gt;(보통 32KiB 정도), &lt;code&gt;DTLB&lt;/code&gt; 같은 박스가 있습니다. 연산 유닛이 필요로 하는 데이터를 메모리에서 가져오고, 결과를 다시 메모리로 내보내는 통로입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Memory (메모리 영역)&lt;/b&gt;: 가장 오른쪽에 위치한 분홍색 영역입니다. &lt;code&gt;Shared L2 Cache&lt;/code&gt;(보통 2~4 MiB), &lt;code&gt;L2 TLB&lt;/code&gt;가 있고, 그 위로 &lt;code&gt;L3 캐시&lt;/code&gt;까지 이어지는 화살표가 붙어 있습니다. 셋째 항목에서 말한 캐시 계층이 바로 이 부분입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다이어그램에서 가장 중요하게 볼 점은 &lt;b&gt;&quot;실제 계산을 수행하는 ALU가 전체 면적에서 차지하는 비율&quot;&lt;/b&gt; 입니다. 숫자를 직접 더하고 곱하는 박스는 FP/Vector 영역의 ALU 몇 개와 Execution Engine 영역의 ALU 몇 개뿐이고, 나머지 대부분의 공간은 &quot;그 ALU들을 효율적으로 쓰기 위한 제어 회로&quot;가 차지하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPU의 내부 구조&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU는 Graphics Processing Unit의 약자로, 원래는 그래픽을 렌더링하기 위해 만들어진 칩입니다. 화면에 나타나는 수백만 개의 픽셀에 대해 거의 같은 연산(색 계산, 변환 행렬 곱 등)을 동시에 수행해야 하기 때문에, 처음부터 &quot;같은 &lt;b&gt;연산&lt;/b&gt;을 엄청나게 많은 데이터에 동시에 적용&quot;하는 용도로 설계되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU의 가장 중요한 구조적 단위는 &lt;b&gt;SM(Streaming Multiprocessor)&lt;/b&gt; 입니다. SM은 NVIDIA GPU에서 쓰는 용어이고, CPU의 &quot;코어 하나&quot;에 대응되는 모듈이라고 생각하면 됩니다. 즉, GPU 한 장에는 SM이 여러 개 있고, 각 SM은 독립적으로 동작할 수 있는 하나의 처리 단위입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SM 내부에는 CPU의 코어와는 다른 비율로 회로가 배치되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;ALU가 매우 많습니다.&lt;/b&gt; CPU 한 코어에 ALU가 몇 개 들어 있는 정도라면, GPU의 SM 하나에는 수십 개에서 백여 개 단위의 ALU(NVIDIA 용어로는 CUDA 코어)가 들어 있습니다. 그래서 SM 하나만 봐도 같은 연산을 동시에 수십~백여 개 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;b&gt;제어 회로(디스패처, 분기 예측기 등)는 CPU보다 적습니다.&lt;/b&gt; GPU는 &lt;b&gt;연산&lt;/b&gt;이라는 용도에 특화되어 있어서, 복잡한 분기 예측이나 정교한 명령 스케줄링이 덜 필요합니다. 그래서 제어 회로에 쓸 칩 면적을 줄이고 그만큼을 ALU에 더 할당하여서 연산 능력을 강화하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, SM 내부의 ALU들은 보통 &lt;b&gt;4개의 묶음(처리 블록)&lt;/b&gt; 으로 그룹화되어 있습니다. 각 묶음은 한 번에 하나의 명령어 스트림을 받아서 그 안의 &lt;b&gt;ALU들이 서로 다른 데이터에 대해 동시에 같은 연산을 수행하는 방식으로 동작합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 GPU가 잘하는 일은 &lt;b&gt;같은 계산을 서로 다른 수많은 데이터에 동시에 적용하는 일&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GPU SM 다이어그램 읽는 법&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;995&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y5Y6N/dJMcajojbFf/GBkKm9V5wpQlc6PUc4FCRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y5Y6N/dJMcajojbFf/GBkKm9V5wpQlc6PUc4FCRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y5Y6N/dJMcajojbFf/GBkKm9V5wpQlc6PUc4FCRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY5Y6N%2FdJMcajojbFf%2FGBkKm9V5wpQlc6PUc4FCRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;995&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;995&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU SM 다이어그램을 처음 보면 CPU 다이어그램과 구조가 상당히 다릅니다. 위쪽에 얇게 &lt;code&gt;L1 Instruction Cache&lt;/code&gt;가 하나 있고, 그 아래로 &lt;b&gt;거의 똑같이 생긴 네 개의 블록&lt;/b&gt;이 2 &amp;times; 2 격자 형태로 배치된 구조가 먼저 눈에 들어옵니다. 이 네 블록이 위에서 말한 &lt;b&gt;4개의 처리 블록&lt;/b&gt;입니다. SM 한 개 안에서 네 개의 처리 블록이 독립적인 명령어 스트림을 받아 서로 다른 일을 동시에 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 처리 블록의 안을 위에서 아래로 읽으면 다음과 같은 순서로 구성되어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;L0 Instruction Cache&lt;/code&gt;&lt;/b&gt;: 그 블록에서 실행할 명령어를 잠깐 보관해 두는 가장 작은 캐시입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Warp Scheduler (32 thread/clk)&lt;/code&gt;&lt;/b&gt;: 워프(warp)는 32개의 스레드를 하나로 묶은 GPU의 기본 실행 단위입니다. 이 스케줄러가 &quot;이번 클럭에는 어느 워프를 돌릴지&quot;를 결정합니다. 즉, 32개의 스레드가 동시에 같은 명령어를 서로 다른 데이터에 대해 수행하도록 묶어서 보냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Dispatch Unit (32 thread/clk)&lt;/code&gt;&lt;/b&gt;: 스케줄러가 선택한 명령어를 실제 연산 유닛들에게 분배하는 회로입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Register File (16,384 &amp;times; 32-bit)&lt;/code&gt;&lt;/b&gt;: 블록 안의 모든 연산 유닛이 공유하는 고속 저장 공간입니다. ALU가 계산할 값을 꺼내고, 결과를 저장하는 곳입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;INT32&lt;/code&gt;, &lt;code&gt;FP32&lt;/code&gt;, &lt;code&gt;FP64&lt;/code&gt; 컬럼&lt;/b&gt;: 여기가 바로 ALU, 즉 &lt;b&gt;CUDA 코어가 놓인 곳&lt;/b&gt;입니다. 초록색 점들로 빼곡히 채워진 세 개의 열이 눈에 들어오는데, 각각 &quot;정수 연산용&quot;, &quot;단정밀도 부동소수점용&quot;, &quot;배정밀도 부동소수점용&quot; CUDA 코어를 뜻합니다. 한 블록 안에 이런 유닛이 수십 개씩 줄지어 있고, 이것이 네 블록에 걸쳐 있으니 SM 하나 안의 CUDA 코어 총합이 백 단위로 금방 커집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;TENSOR CORE 4th GENERATION&lt;/code&gt;&lt;/b&gt;: 각 블록에서 INT32/FP32/FP64 컬럼 오른쪽에 크게 자리잡은 초록 박스입니다. 이것이 &lt;b&gt;행렬곱 전용 연산 유닛인 텐서 코어&lt;/b&gt;이고, 세대별로 크기와 성능이 다릅니다. 다이어그램을 보면 CUDA 코어들과 거의 맞먹는 면적을 차지하고 있다는 점이 중요합니다. 최신 GPU로 올수록 텐서 코어가 칩 면적에서 차지하는 비중이 점점 커지고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;LD/ST&lt;/code&gt; (Load/Store) 유닛과 &lt;code&gt;SFU&lt;/code&gt;&lt;/b&gt;: 블록의 맨 아래쪽에 줄지어 있는 빨간색 박스들입니다. LD/ST 유닛은 메모리에서 데이터를 읽어오거나 쓰고, SFU(Special Function Unit)는 사인, 코사인, 제곱근, 역수 같은 특수 함수 연산을 담당합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 네 블록 아래 공통으로 자리잡은 영역이 또 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Tensor Memory Accelerator&lt;/code&gt;&lt;/b&gt; (최신 세대에서 등장): 텐서 코어가 사용할 큰 행렬 데이터를 메모리에서 고속으로 옮겨주는 전용 회로입니다. 텐서 코어가 아무리 빨라도 데이터를 제때 공급하지 못하면 성능이 나오지 않기 때문에, 메모리 전송 자체를 담당하는 회로를 별도로 둔 것입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;256 KB L1 Data Cache / Shared Memory&lt;/code&gt;&lt;/b&gt;: SM 안의 네 블록이 &lt;b&gt;함께 쓰는 데이터 캐시이자 공유 메모리&lt;/b&gt;입니다. 같은 SM 안의 스레드들은 이 공유 메모리를 통해 빠르게 데이터를 주고받을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다이어그램에서 가장 중요하게 볼 점은 CPU 다이어그램과의 &lt;b&gt;비례 차이&lt;/b&gt;입니다. CPU 다이어그램에서는 실제 연산 유닛(ALU)이 전체에서 차지하는 비중이 매우 작았던 반면 GPU SM 다이어그램에서는 &lt;b&gt;면적의 대부분을 연산 유닛(INT32/FP32/FP64 컬럼 + 텐서 코어)이 차지&lt;/b&gt;합니다. 제어 회로에 해당하는 워프 스케줄러와 디스패치 유닛은 각 블록의 맨 위에 얇은 띠처럼 있을 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 한 가지 짚어야 할 점은, 이 전체 다이어그램이 &lt;b&gt;SM 하나&lt;/b&gt;에 불과하다는 사실입니다. 실제 GPU에는 이런 SM이 수십에서 백 개 이상 들어 있습니다. 예를 들어 H100에는 SM이 132개 있고, RTX 3060에는 SM이 28개 있습니다. 다이어그램 한 장 분량의 구조가 수십 번 복제되어 있는 셈이라, GPU 전체로 보면 연산 유닛의 수가 수천~수만 개 단위가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 GPU가 딥러닝에 유리한가&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝 모델의 핵심 연산은 대부분 &lt;b&gt;행렬과 벡터의 곱셈&lt;/b&gt;, 그리고 그에 수반되는 &lt;b&gt;덧셈&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 행렬(matrix)은 숫자를 직사각형으로 배열한 표이고, 벡터(vector)는 한 줄짜리 행렬이라고 생각하면 됩니다. 예를 들어 &lt;code&gt;[1, 2, 3]&lt;/code&gt;은 길이 3짜리 벡터이고, 아래 같은 것은 2행 3열의 행렬입니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[1, 2, 3]
[4, 5, 6]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행렬곱 A &amp;times; B는 A의 각 행과 B의 각 열을 짝지어서 대응되는 원소끼리 곱한 뒤 그 결과를 모두 더하는 방식으로 계산됩니다. 결과 행렬의 원소 하나를 구하는 데만 해도 여러 번의 곱셈과 덧셈이 필요하고 결과 행렬의 모든 원소를 구하려면 그 연산이 어마어마하게 반복됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 &lt;b&gt;각 원소를 구하는 계산이 서로 독립적&lt;/b&gt;이라는 것입니다. 결과 행렬의 (1,1) 원소를 구하는 계산과 (2,3) 원소를 구하는 계산은 서로 영향을 주지 않습니다. 그래서 &lt;b&gt;GPU처럼 같은 연산을 수많은 데이터에 동시에 적용하는 구조가 행렬곱에 아주 잘 맞습니다.&lt;/b&gt; 수천 개의 ALU가 동시에 결과 행렬의 서로 다른 원소들을 계산하고 마지막에 합치면 행렬곱이 끝납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 CPU는 코어 수가 적어서 동시에 처리할 수 있는 곱셈-덧셈의 개수가 훨씬 적고 큰 행렬 하나를 계산하는 데도 상대적으로 긴 시간이 걸립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 &quot;이 큰 행렬곱을 해줘&quot;라고 GPU에 작업을 던지면, GPU가 그 대규모 계산을 맡아서 빠르게 처리한 뒤 결과를 돌려줍니다. 딥러닝 프레임워크(PyTorch, TensorFlow 등)가 내부적으로 이런 역할 분담을 처리해 줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. FLOPS: 연산 능력을 재는 단위&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU와 GPU의 성능을 비교할 때 가장 자주 등장하는 단위가 &lt;b&gt;FLOPS&lt;/b&gt;입니다. FLOPS는 &lt;b&gt;FL&lt;/b&gt;oating-point &lt;b&gt;O&lt;/b&gt;perations &lt;b&gt;P&lt;/b&gt;er &lt;b&gt;S&lt;/b&gt;econd의 약자로, &lt;b&gt;초당 수행할 수 있는 부동소수점 연산의 횟수&lt;/b&gt;를 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;부동소수점 연산&quot;이란 소수점이 있는 숫자(예: 3.14, 0.0001, -2.5 등)를 더하거나 곱하는 연산을 말합니다. 딥러닝의 모든 계산은 부동소수점 연산으로 이루어지기 때문에, FLOPS는 곧 &lt;b&gt;&quot;이 하드웨어가 1초에 얼마나 많은 딥러닝 계산을 할 수 있는가&lt;/b&gt;&quot;를 나타내는 지표가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 너무 크기 때문에 보통은 접두어를 붙여서 표기합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GFLOPS (Giga FLOPS)&lt;/b&gt;: 10⁹, 즉 초당 10억 번의 부동소수점 연산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TFLOPS (Tera FLOPS)&lt;/b&gt;: 10&amp;sup1;&amp;sup2;, 즉 초당 1조 번의 부동소수점 연산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PFLOPS (Peta FLOPS)&lt;/b&gt;: 10&amp;sup1;⁵, 즉 초당 1000조 번의 부동소수점 연산&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 정수 연산만 세는 경우에는 &lt;b&gt;TOPS(Tera Operations Per Second)&lt;/b&gt; 라는 단위를 씁니다. 정수는 부동소수점보다 단순해서 연산 속도가 빠른데 이 구분은 뒤에서 INT8 설명할 때 다시 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 수치로 비교해 보면 차이가 분명합니다. 한 세대 최상급 CPU인 Intel Core i9-12900KF의 이론적 최대 성능은 약 800 GFLOPS 수준, &lt;b&gt;즉 0.8 TFLOPS&lt;/b&gt; 정도입니다. 반면 데이터센터용 GPU인 NVIDIA H100은 FP16 기준 약 &lt;b&gt;1,000 TFLOPS&lt;/b&gt;에 달합니다. 약 1,000배 이상 차이가 나는 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 숫자를 표현하는 방법: FP32, FP16, INT8&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비트와 정밀도의 관계&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터는 모든 숫자를 0과 1의 조합, 즉 &lt;b&gt;이진수&lt;/b&gt;로 표현합니다. 이때 하나의 숫자를 저장하는 데 몇 비트를 사용하느냐에 따라 표현할 수 있는 값의 범위와 &lt;b&gt;정밀도(precision)&lt;/b&gt; 가 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비트(bit)는 0 또는 1 중 하나를 저장하는 최소 단위입니다. 1비트로는 2가지(0 또는 1), 2비트로는 4가지(00, 01, 10, 11), n비트로는 2ⁿ가지의 서로 다른 값을 표현할 수 있습니다. 비트 수가 많을수록 한 숫자를 더 섬세하게 표현할 수 있지만, 그만큼 메모리를 더 많이 차지하고 연산 회로도 더 복잡해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부동소수점 형식의 구조&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부동소수점(floating-point) 형식은 과학적 표기법과 비슷한 방식으로 소수를 표현합니다. 예를 들어 &lt;code&gt;3.14&lt;/code&gt;를 &lt;code&gt;3.14 &amp;times; 10⁰&lt;/code&gt;처럼 &quot;유효숫자 &amp;times; 기수^지수&quot; 형태로 표현하는 방식을 이진수로 확장한 것입니다. 부동소수점 숫자는 크게 세 부분으로 나뉩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;부호 비트&lt;/b&gt;: 양수(0)인지 음수(1)인지를 나타내는 1비트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지수 비트&lt;/b&gt;: 숫자가 얼마나 큰지/작은지를 나타내는 부분. 비트 수가 많을수록 표현할 수 있는 숫자의 범위가 넓어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가수 비트(유효숫자)&lt;/b&gt;: 실제 숫자의 정밀한 값을 나타내는 부분. 비트 수가 많을수록 소수점 아래를 더 정확하게 표현할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 부분에 각각 몇 비트를 할당하느냐에 따라 다양한 부동소수점 형식이 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;딥러닝에서 자주 마주치는 세 가지 형식은 다음과 같습니다&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FP32 (32비트 부동소수점, 단정밀도)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부호 1비트 + 지수 8비트 + 가수 23비트&lt;/li&gt;
&lt;li&gt;전통적으로 과학 계산과 딥러닝에서 표준으로 쓰이는 형식&lt;/li&gt;
&lt;li&gt;넓은 범위와 높은 정밀도를 동시에 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FP16 (16비트 부동소수점, 반정밀도)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부호 1비트 + 지수 5비트 + 가수 10비트&lt;/li&gt;
&lt;li&gt;FP32의 딱 절반 크기&lt;/li&gt;
&lt;li&gt;메모리도 절반, 연산에 쓰는 회로도 단순해서 속도가 빠름&lt;/li&gt;
&lt;li&gt;대신 표현 범위와 정밀도가 줄어듦&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INT8 (8비트 정수)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부동소수점이 아니라 정수로 값을 표현&lt;/li&gt;
&lt;li&gt;가장 작은 8비트만 사용하므로 가장 빠르고 가장 적은 메모리를 차지&lt;/li&gt;
&lt;li&gt;소수점 이하가 없으므로 그대로 쓸 수 없고, 별도의 변환(양자화, quantization) 과정을 거침&lt;/li&gt;
&lt;li&gt;주로 학습이 끝난 모델을 &lt;b&gt;추론(inference, 이미 학습된 모델로 예측값을 뽑는 과정)&lt;/b&gt; 할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숫자로 보는 정밀도 차이&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 값을 각 형식으로 저장하면 어떤 차이가 나는지 살펴보겠습니다. 원주율 &lt;b&gt;3.14159265358979&amp;hellip;&lt;/b&gt; 를 저장할 때,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FP32로 저장&lt;/b&gt;: 약 &lt;code&gt;3.14159274&lt;/code&gt; &amp;mdash; 소수점 아래 6~7자리까지 정확&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FP16으로 저장&lt;/b&gt;: 약 &lt;code&gt;3.140625&lt;/code&gt; &amp;mdash; 소수점 아래 2~3자리까지만 정확, 뒤쪽은 반올림/버림됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;INT8로 저장&lt;/b&gt;: &lt;code&gt;3&lt;/code&gt; &amp;mdash; 정수만 저장되므로 소수점 이하가 전부 사라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 딥러닝은 낮은 정밀도로도 괜찮은가&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;소수점 이하가 이렇게 잘려나가는데 학습이 제대로 되나?&quot;라는 의문이 생길 수 있습니다. 그러나 딥러닝 모델은 수치의 미세한 오차에 생각보다 강합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 두 가지입니다. 첫째, 딥러닝의 가중치(weight, 신경망이 학습하는 매개변수) 값 자체가 아주 작은 실수들이고, 그 값들은 학습 과정에서 매 스텝마다 업데이트됩니다. 예를 들어 어떤 가중치가 &lt;code&gt;0.123456789&lt;/code&gt;인지 &lt;code&gt;0.123456&lt;/code&gt;인지는 모델의 최종 성능에 거의 영향을 주지 않습니다. 어차피 다음 업데이트에서 그 값이 또 조금 바뀌기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 어텐션 가중치처럼 확률적으로 정규화되는 값들도 마찬가지입니다. 어떤 두 단어 사이의 어텐션 가중치가 &lt;code&gt;0.0723&lt;/code&gt;이든 &lt;code&gt;0.0720&lt;/code&gt;이든, 예측 결과는 거의 똑같이 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현대 딥러닝에서는 &quot;정확도를 유지할 수 있는 만큼 최대한 낮은 정밀도로 계산하자&quot;는 전략이 표준이 되었습니다. 이 전략의 대표적인 형태가 뒤에서 다룰 &lt;b&gt;혼합 정밀도(mixed precision)&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. CUDA 코어와 텐서 코어: 두 종류의 연산 유닛&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU의 SM 안에는 두 종류의 연산 유닛이 들어 있습니다. &lt;b&gt;CUDA 코어와 텐서 코어&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CUDA 코어&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CUDA(Compute Unified Device Architecture)&lt;/b&gt; 는 NVIDIA가 만든 GPU 프로그래밍 플랫폼의 이름이고, &lt;b&gt;CUDA 코어&lt;/b&gt;는 그 플랫폼에서 프로그램이 실행되는 기본 연산 유닛을 가리킵니다. 앞에서 설명한 &lt;b&gt;SM 안의 ALU가 바로 CUDA 코어&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CUDA 코어의 특징은 &lt;b&gt;범용성&lt;/b&gt;입니다. 덧셈, 곱셈, 비교, 형 변환 같은 다양한 연산을 할 수 있고, 프로그램에서 지시하는 대로 자유롭게 사용됩니다. 다만 한 번에 할 수 있는 일은 &quot;숫자 하나에 대한 연산 하나&quot;입니다. 한 CUDA 코어는 한 클럭 사이클에 한 번의 부동소수점 연산(덧셈 한 번, 또는 곱셈 한 번, 또는 경우에 따라 곱셈-덧셈 한 번)을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;클럭 사이클(clock cycle)&lt;/b&gt; 은 GPU가 한 번 신호를 내보내는 기본 시간 단위입니다. CUDA 코어 하나가 1 GHz 클럭에서 동작한다면, 초당 10억 번의 연산을 처리한다고 보시면 됩니다. 흔하게 아시는 RTX 3060에는 &lt;b&gt;3,584개의 CUDA 코어&lt;/b&gt;가 들어 있습니다. 이 3,584개가 동시에 작동하면서 서로 다른 데이터에 대해 병렬로 연산을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 2&amp;times;2 행렬곱을 예로 들어 CUDA 코어가 어떻게 작동하는지 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[1, 2]   [5, 6]   [1&amp;times;5+2&amp;times;7, 1&amp;times;6+2&amp;times;8]   [19, 22]
[3, 4] &amp;times; [7, 8] = [3&amp;times;5+4&amp;times;7, 3&amp;times;6+4&amp;times;8] = [43, 50]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 행렬의 원소 하나, 예를 들어 &lt;code&gt;19 = 1&amp;times;5 + 2&amp;times;7&lt;/code&gt;을 구하려면 &lt;b&gt;곱셈 2번 + 덧셈 1번 = 총 3번의 연산&lt;/b&gt;이 필요합니다. 결과 행렬에 원소가 4개이므로, 2&amp;times;2 행렬곱 전체에는 &lt;b&gt;4 &amp;times; 3 = 12번의 연산&lt;/b&gt;이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CUDA 코어는 이 12번의 연산을 하나씩 처리합니다. 여러 CUDA 코어가 서로 다른 원소를 맡아 병렬로 진행할 수 있지만, 각 코어가 한 번에 하는 일은 어디까지나 &quot;숫자 하나 &amp;times; 숫자 하나&quot; 또는 &quot;숫자 하나 + 숫자 하나&quot; 수준입니다. 결과 행렬의 원소 4개를 CUDA 코어 4개에 하나씩 배정해도, 각 코어는 3번의 연산을 순차적으로 수행해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;텐서 코어&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;텐서 코어(Tensor Core)&lt;/b&gt; 는 행렬곱에 특화된 전용 연산 유닛입니다. 2017년 NVIDIA의 Volta 아키텍처(V100)에서 처음 도입된 이후, 세대를 거치며 발전해 왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텐서 코어의 가장 큰 특징은 &lt;b&gt;작은 행렬 단위를 한 번에 곱하고 더한다&lt;/b&gt;는 것입니다. CUDA 코어가 숫자 하나에 대해 연산 하나를 하는 것과 대비됩니다. 예를 들어 RTX 3060의 3세대 텐서 코어는 8&amp;times;4&amp;times;8 크기의 행렬 연산을 1 클럭 사이클에 처리합니다(정확한 형식은 뒤에서 다룹니다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텐서 코어가 수행하는 기본 연산은 다음과 같은 형태입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;D = A &amp;times; B + C
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 A, B, C, D는 모두 행렬입니다. 두 행렬 A와 B를 곱하고, 그 결과에 행렬 C를 더해서 D에 저장하는 연산을 &lt;b&gt;한 번에&lt;/b&gt; 수행합니다. 이렇게 곱셈과 덧셈을 묶어서 한 번에 처리하는 것을 &lt;b&gt;FMA(Fused Multiply-Add, 융합 곱셈-덧셈)&lt;/b&gt; 연산이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FMA 연산이 중요한 이유는 두 가지입니다. 첫째, 곱셈과 덧셈을 따로 수행할 때보다 하드웨어 회로가 단순해지고 속도가 빨라집니다. 둘째, 중간 결과를 반올림 없이 더 높은 정밀도로 누적할 수 있어서, 같은 연산을 CUDA 코어로 나눠서 할 때보다 수치적으로 더 정확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTX 3060에는 &lt;b&gt;112개의 3세대 텐서 코어&lt;/b&gt;가 탑재되어 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. RTX 3060의 실제 성능&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 RTX 3060 GPU라도 어떤 연산 유닛을 쓰고 어떤 정밀도로 계산하느냐에 따라 성능이 크게 달라집니다. 공식 스펙을 기준으로 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;height: 190px;&quot; width=&quot;779&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;정밀도&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;연산 유닛&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;성능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;FP32 (32비트)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;CUDA 코어&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;약 12.7 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;FP16 (16비트)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;텐서 코어 (dense)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;약 25.5 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;FP16 (16비트)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;텐서 코어 (sparsity)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;약 51 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;INT8 (8비트)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;텐서 코어 (dense)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;약 102 TOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TFLOPS&lt;/b&gt;: 앞서 설명한 Tera FLOPS. 초당 1조 번의 부동소수점 연산.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TOPS&lt;/b&gt;: 초당 1조 번의 정수 연산. INT8처럼 정수 연산인 경우 FLOPS 대신 TOPS로 표기합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dense(밀집)&lt;/b&gt;: 행렬에 0이 아닌 값들이 빽빽하게 들어 있는 일반적인 경우.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sparsity(희소성 가속)&lt;/b&gt;: 행렬에 0이 많은 경우, 0인 부분의 연산을 건너뛰어 속도를 두 배로 올리는 NVIDIA의 하드웨어 기능. Ampere 세대(RTX 30시리즈)부터 지원됩니다. 정확히는 &quot;2:4 희소성 패턴&quot;을 만족하도록 모델을 조정해야 적용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표가 의미하는 바는 이렇습니다. 가장 단순한 FP32 + CUDA 코어 조합과 비교했을 때, FP16 + 텐서 코어는 약 2배, 희소성 가속까지 적용하면 약 4배, INT8까지 내려가면 약 8배 빨라집니다. &lt;b&gt;같은 그래픽 카드인데도&lt;/b&gt; &quot;숫자를 몇 비트로 표현하는가&quot;, &quot;어떤 연산 유닛에 작업을 보내는가&quot;라는 두 가지 선택만으로 체감 성능이 수 배씩 차이 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 텐서 코어는 행렬을 어떻게 한 번에 처리하는가&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4&amp;times;4 행렬 FMA가 기본 단위&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텐서 코어가 수행하는 연산을 좀 더 구체적으로 들여다보면 다음과 같습니다. 1세대 텐서 코어를 기준으로 할 때, 텐서 코어 하나는 한 클럭에 다음 연산을 수행합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;D = A &amp;times; B + C
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 A와 B는 4&amp;times;4 크기의 FP16 행렬, C와 D는 4&amp;times;4 크기의 FP32 행렬입니다. 즉, &lt;b&gt;곱셈은 낮은 정밀도인 FP16으로 빠르게 수행하고, 누적(덧셈)은 높은 정밀도인 FP32로 수행&lt;/b&gt;합니다. 이렇게 서로 다른 정밀도를 섞어서 쓰는 방식을 &lt;b&gt;혼합 정밀도(mixed precision)&lt;/b&gt; 라고 합니다. 곱셈은 수치 오차에 상대적으로 덜 민감하기 때문에 FP16으로 빠르게 처리하고, 여러 번의 곱셈 결과를 쌓아 올리는 덧셈 단계는 오차가 누적되기 쉬우므로 FP32로 정확하게 유지하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4&amp;times;4 행렬곱은 총 몇 번의 연산일까요? 결과 행렬의 원소 하나를 구하려면 &lt;b&gt;곱셈 4번 + 덧셈 3번 = 7번&lt;/b&gt;이 필요하고, 일반적으로 &quot;곱셈 1번 + 덧셈 1번&quot;을 한 번의 연산으로 취급합니다. 4&amp;times;4 행렬곱은 총 &lt;b&gt;64번의 곱셈과 64번의 덧셈, 합쳐서 128번의 부동소수점 연산&lt;/b&gt;&amp;nbsp;에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CUDA 코어로 같은 연산을 하면 128 클럭이 필요하지만, 텐서 코어 하나는 이걸 &lt;b&gt;1 클럭&lt;/b&gt;에 끝냅니다. 단순 계산으로는 128배의 속도 향상입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세대별 텐서 코어의 발전&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텐서 코어는 NVIDIA GPU 세대가 올라갈수록 더 큰 행렬을 한 번에 처리할 수 있게 되었고, 지원하는 정밀도도 다양해졌습니다.&lt;/p&gt;
&lt;table style=&quot;height: 142px;&quot; width=&quot;854&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;GPU&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;세대&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;텐서 코어당 행렬 크기&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;클럭당 FP16 연산 수&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;지원 정밀도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;V100 (데이터센터)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;1세대&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;4&amp;times;4&amp;times;4&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;128 FLOPS&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;FP16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;RTX 3060 (게이밍)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;3세대&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;8&amp;times;4&amp;times;8&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;512 FLOPS&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;FP16, BF16, TF32, INT8, INT4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;H100 (데이터센터)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;4세대&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;8&amp;times;4&amp;times;16&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;1024 FLOPS&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;FP16, BF16, TF32, FP8, INT8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표에서 &quot;텐서 코어당 행렬 크기&quot;의 &lt;code&gt;A&amp;times;B&amp;times;C&lt;/code&gt; 표기는 &quot;A행 &amp;times; B열&quot; 행렬과 &quot;B행 &amp;times; C열&quot; 행렬의 곱을 한 번에 처리한다는 뜻입니다. 즉, 입력 행렬의 모양이 커질수록 한 번에 더 많은 연산을 할 수 있고, 그만큼 클럭당 처리하는 연산 수가 늘어납니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BF16 (bfloat16, Brain Floating Point 16)&lt;/b&gt;: 부호 1비트 + 지수 8비트 + 가수 7비트. FP16과 달리 지수 부분이 FP32와 동일하게 8비트입니다. 덕분에 표현 범위가 FP32만큼 넓어서 학습 과정에서 값이 너무 커지거나 작아져 발생하는 문제에 덜 취약합니다. 대신 가수가 7비트뿐이라 정밀도는 낮습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TF32 (TensorFloat-32)&lt;/b&gt;: 이름은 32인데 실제로는 19비트만 사용하는 형식. 부호 1비트 + 지수 8비트(FP32와 동일) + 가수 10비트(FP16과 동일). FP32처럼 보이지만 내부적으로는 더 빠르게 처리할 수 있는 형식입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FP8&lt;/b&gt;: 지수/가수 배분을 다르게 한 두 가지 변형(E4M3, E5M2)이 있는 8비트 부동소수점.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;INT4&lt;/b&gt;: 4비트 정수. 주로 초경량 추론에 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1세대와 4세대를 비교하면 텐서 코어 하나의 처리량이 약 8배 늘어났습니다. RTX 3060의 3세대 텐서 코어는 1세대 대비 한 클럭에 4배 많은 연산을 수행하는 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 트랜스포머 어텐션과 텐서 코어의 연결&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어텐션에서 가장 무거운 연산&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜스포머에서 가장 연산량이 많은 부분은 어텐션(attention) 메커니즘의 &lt;b&gt;Q &amp;middot; K 행렬곱&lt;/b&gt;입니다. (이전 게시글에서 상세하게 다루었습니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜스포머는 입력 문장을 토큰으로 만듭니다.&lt;/li&gt;
&lt;li&gt;각 토큰을 벡터로 바꾼 뒤, 이 벡터들에서 쿼리, 키, 값 이라는 세 종류의 벡터를 만듭니다. Q, K, V는 각각 선형 변환(하나의 행렬곱)으로 생성됩니다.&lt;/li&gt;
&lt;li&gt;어텐션의 핵심 계산은 &quot;각 토큰의 Q가 다른 모든 토큰의 K와 얼마나 닮았는지&quot;를 내적(두 벡터의 유사도 계산)으로 구하고, 이 결과를 바탕으로 V의 가중합을 만드는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 Q와 K는 각각 &quot;토큰 개수 &amp;times; 차원(토큰 하나를 몇 개의 숫자로 표현할 수 있는가)&quot; 크기의 행렬로 표현할 수 있기 때문에,&quot;행렬 x 행렬&quot;로, 하나의 큰 행렬곱으로 계산됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차원을 그대로 두고, 토큰의 관점에서, 한 번에 처리하는 토큰의 개수를 n이라고 하면, 이 행렬곱의 결과는 n&amp;times;n 크기의 행렬이 됩니다. 연산량은 토큰 개수의 제곱에 비례해서 커지므로 &lt;b&gt;O(n&amp;sup2;)&lt;/b&gt; 의 복잡도를 갖습니다. 문장이 2배 길어지면 연산량은 4배, 4배 길어지면 16배가 된다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 수준의 긴 문장을 다루는 최신 모델일수록 이 부분의 연산 비용이 전체 성능을 좌우합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큰 행렬을 작은 타일로 쪼개는 기법&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제는, 텐서 코어가 한 번에 처리하는 단위는 고작 4&amp;times;4 또는 8&amp;times;4&amp;times;8 같은 작은 행렬이라는 것입니다. 시퀀스 길이가 2048이면 Q &amp;times; Kᵀ는 2048&amp;times;2048짜리 결과를 만들어야 하는데, 이걸 어떻게 작은 텐서 코어 단위로 처리할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 &lt;b&gt;타일링(tiling)&lt;/b&gt; 입니다. 큰 행렬을 작은 블록으로 쪼갠 뒤, 각 타일 단위의 작은 행렬곱을 수많은 텐서 코어에 분배해서 동시에 처리하고, 그 결과를 합쳐서 전체 행렬곱을 완성하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 2048&amp;times;2048 행렬곱을 16&amp;times;16 타일로 쪼개면, 가로로 128개, 세로로 128개, 총 16,384개의 타일이 만들어집니다. 각 타일의 결과는 독립적으로 계산할 수 있으므로, GPU의 수백 개 텐서 코어에 골고루 분배되어 병렬로 처리됩니다. cuBLAS, cuDNN 같은 NVIDIA의 라이브러리가 이런 타일링을 자동으로 최적화해서 수행해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 타일링이 가능한 이유는 &lt;b&gt;행렬곱의 연산을 블록 단위로 분해 가능&lt;/b&gt;하기 때문입니다. 큰 행렬곱은 작은 행렬곱의 합으로 정확하게 표현될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멀티헤드 어텐션과 배치 행렬곱&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜스포머의 멀티 어텐션에서는 여러 개의 헤드를 사용합니다.&amp;nbsp;헤드가 8개라면, 같은 입력을 8번 서로 다른 관점으로 처리하고, 그 결과를 합치는 방식입니다. 각 헤드는 서로 독립적으로 자기만의 Q, K, V 행렬곱을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 헤드의 연산은 서로 영향을 주지 않기 때문에 GPU는 헤드 개수만큼의 독립된 행렬곱을 동시에 수행할 수 있습니다. GPU의 성능은 특정 코어가 놀지 않고 모두 일할 때 최대가 되는데 헤드 사이에 데이터를 주고받을 필요가 없으므로 수백 개의 텐서 코어에 작업을 균등하게 분배하게 되어 효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 혼합 정밀도 학습: 낮은 정밀도를 보완하는 법&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 텐서 코어가 FP16으로 곱하고 FP32로 누적한다는 이야기를 했습니다. 이 원리를 학습 전체로 확장한 것이 &lt;b&gt;혼합 정밀도(mixed precision) 학습&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼합 정밀도 학습의 기본 아이디어는 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;순전파(forward pass)와 역전파(backward pass)의 행렬곱&lt;/b&gt;: FP16으로 빠르게 계산. 여기가 전체 연산의 대부분을 차지하는 곳이기 때문에 이곳의 속도를 올리는 것이 가장 효과적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가중치 업데이트와 손실 스케일링&lt;/b&gt;: FP32로 정확하게 유지. 가중치가 업데이트될 때 값이 아주 작은 변화량이 누적되는데, 이 부분을 FP16으로 하면 너무 작은 변화량이 반올림돼서 사라지는 문제가 생깁니다. 그래서 &quot;마스터 가중치&quot;라고 부르는 FP32 복사본을 따로 두고, 업데이트는 그 복사본에 대해 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 몇 가지 용어를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;순전파(forward pass)&lt;/b&gt;: 입력 데이터를 신경망에 통과시켜 예측값을 계산하는 과정.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역전파(backward pass)&lt;/b&gt;: 예측이 정답과 얼마나 다른지를 계산한 뒤, 그 오차를 이용해 각 가중치를 얼마나 조정해야 할지 구하는 과정.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;손실 스케일링(loss scaling)&lt;/b&gt;: FP16은 표현 범위가 좁아서, 역전파 과정에서 아주 작은 값이 0으로 반올림되는 &lt;b&gt;언더플로(underflow)&lt;/b&gt; 문제가 자주 발생합니다. 이를 막기 위해 손실(loss) 값에 큰 수(예: 1024)를 곱해서 역전파를 수행한 뒤, 가중치를 업데이트하기 직전에 다시 나눠주는 기법입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전략 덕분에 혼합 정밀도 학습은 다음과 같은 효과를 냅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;학습 속도가 2~3배 빨라짐&lt;/b&gt; (주로 텐서 코어가 활용되기 때문)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPU 메모리 사용량이 절반 가까이 줄어듦&lt;/b&gt; (중간 활성화값이 FP16이므로)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종 모델의 성능은 FP32 학습과 거의 동일하게 유지됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;PyTorch에서는 torch.cuda.amp(Automatic Mixed Precision) 모듈을 사용하면 몇 줄의 코드만으로 혼합 정밀도 학습을 켤 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 데이터센터 GPU vs 게이밍 GPU&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 NVIDIA GPU라도 데이터센터용과 게이밍용은 규모가 꽤 다릅니다. 텐서 코어의 기본 원리와 아키텍처는 거의 같지만, 탑재된 양과 보조 구성 요소가 다릅니다.&lt;/p&gt;
&lt;table style=&quot;height: 236px;&quot; width=&quot;859&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;RTX 3060 (게이밍)&lt;/th&gt;
&lt;th&gt;A100 (데이터센터)&lt;/th&gt;
&lt;th&gt;H100 (데이터센터)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;텐서 코어 수&lt;/td&gt;
&lt;td&gt;112개&lt;/td&gt;
&lt;td&gt;432개&lt;/td&gt;
&lt;td&gt;528개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP16 텐서 성능&lt;/td&gt;
&lt;td&gt;~51 TFLOPS&lt;/td&gt;
&lt;td&gt;~312 TFLOPS&lt;/td&gt;
&lt;td&gt;~990 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU 메모리&lt;/td&gt;
&lt;td&gt;12GB GDDR6&lt;/td&gt;
&lt;td&gt;80GB HBM2e&lt;/td&gt;
&lt;td&gt;80GB HBM3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 대역폭&lt;/td&gt;
&lt;td&gt;360 GB/s&lt;/td&gt;
&lt;td&gt;2,039 GB/s&lt;/td&gt;
&lt;td&gt;3,350 GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;소규모 추론, 학습 실험&lt;/td&gt;
&lt;td&gt;대규모 학습&lt;/td&gt;
&lt;td&gt;초대규모 학습&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GDDR6, HBM2e, HBM3&lt;/b&gt;: GPU에 장착되는 고속 메모리의 종류입니다. GDDR은 게이밍 GPU에 주로 쓰이고, HBM(High Bandwidth Memory)은 데이터센터 GPU에 쓰이는 초고대역폭 메모리입니다. 같은 용량이라도 HBM이 훨씬 빠르고 그만큼 단가도 높습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 대역폭(memory bandwidth)&lt;/b&gt;: 초당 GPU 메모리에서 연산 유닛으로 얼마나 많은 데이터를 옮길 수 있는지를 나타내는 값. 단위는 GB/s입니다. 딥러닝은 엄청난 양의 데이터를 계속 메모리에서 꺼내 써야 하기 때문에 연산 유닛이 아무리 빨라도 메모리 대역폭이 따라오지 못하면 성능이 안 나옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT 시리즈나 LLaMA 같은 대규모 언어 모델의 사전학습(pre-training)에는 A100이나 H100이 수백에서 수천 장 사용됩니다. 단일 GPU로는 모델이 메모리에 들어가지도 않고 학습 시간도 수십 년 단위가 되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 RTX 3060만 있어도 다음과 같은 일은 충분히 가능합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소규모 모델(수백만~수천만 파라미터)의 처음부터 학습&lt;/li&gt;
&lt;li&gt;공개된 대형 모델의 &lt;b&gt;파인튜닝(fine-tuning, 기존에 학습된 모델을 특정 작업에 맞게 추가 학습)&lt;/b&gt;, 특히 LoRA처럼 일부 파라미터만 학습하는 기법&lt;/li&gt;
&lt;li&gt;대형 모델의 양자화(quantization) 추론. INT8이나 INT4로 변환된 모델이라면 12GB 메모리 안에서도 꽤 큰 모델이 돌아갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텐서 코어의 원리 자체는 RTX 3060이나 H100이나 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, GPU가 트랜스포머를 빠르게 처리할 수 있는 이유는 단 하나의 기술 때문이 아닙니다. 구조적으로는 &lt;b&gt;수많은 ALU를 가진 SM들&lt;/b&gt;이 있고 그 안에 &lt;b&gt;행렬곱 전용인 텐서 코어&lt;/b&gt;가 있으며이 텐서 코어가 &lt;b&gt;FP16 같은 낮은 정밀도로 빠르게 곱하고 FP32로 누적&lt;/b&gt;합니다. 소프트웨어 수준에서는 혼합 정밀도 학습과 타일링, 배치 행렬곱 같은 기법이 이 하드웨어를 최대한 활용합니다. 그리고 트랜스포머의 멀티헤드 어텐션 기법의 병렬성이 GPU에서 효율적이기에 GPU를 사용하고 있는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mixed Precision Training.&amp;nbsp;&lt;a href=&quot;https://arxiv.org/abs/1710.03740&quot;&gt;https://arxiv.org/abs/1710.03740&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Accelerating AI Training with TF32 Tensor Cores. &lt;a href=&quot;https://developer.nvidia.com/blog/accelerating-ai-training-with-tf32-tensor-cores/&quot;&gt;https://developer.nvidia.com/blog/accelerating-ai-training-with-tf32-tensor-cores/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Using Tensor Cores for Mixed-Precision Scientific Computing. &lt;a href=&quot;https://developer.nvidia.com/blog/tensor-cores-mixed-precision-scientific-computing/&quot;&gt;https://developer.nvidia.com/blog/tensor-cores-mixed-precision-scientific-computing/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Harnessing GPU Tensor Cores for Fast FP16 Arithmetic to Speed up Mixed-Precision Iterative Refinement Solvers. &lt;a href=&quot;https://www.netlib.org/utk/people/JackDongarra/PAPERS/haidar_fp16_sc18.pdf&quot;&gt;https://www.netlib.org/utk/people/JackDongarra/PAPERS/haidar_fp16_sc18.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;RTX 3060 스펙&amp;nbsp;&lt;a href=&quot;https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti/&quot;&gt;https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3060-3060ti/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CPU와 GPU의 차이, 그리고 딥러닝. &lt;a href=&quot;https://yozm.wishket.com/magazine/detail/2294/&quot;&gt;https://yozm.wishket.com/magazine/detail/2294/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발지식/AI</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/112</guid>
      <comments>https://halfmoonbearlog.tistory.com/112#entry112comment</comments>
      <pubDate>Fri, 17 Apr 2026 17:58:54 +0900</pubDate>
    </item>
    <item>
      <title>에어갭 환경에서 Kubespray로 Kubernetes 클러스터 설치하기</title>
      <link>https://halfmoonbearlog.tistory.com/111</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온프레미스 환경에서 인터넷이 완전히 차단된 상태, 에어갭(air-gap) 환경에서 쿠버네티스를 설치하는 방법에 대한 포스팅입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://drive.google.com/drive/folders/1WsFwJxH0P3-fV5r5VvyUQ-nW9u-wBsEj?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://drive.google.com/drive/folders/1WsFwJxH0P3-fV5r5VvyUQ-nW9u-wBsEj?usp=sharing&lt;/a&gt; (다운로드)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;table style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; height: 149px;&quot; width=&quot;854&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;호스트명&lt;/th&gt;
&lt;th&gt;&lt;b&gt;OS&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;IP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Master&lt;/td&gt;
&lt;td&gt;master&lt;/td&gt;
&lt;td&gt;Rocky 8.10&lt;/td&gt;
&lt;td&gt;192.168.59.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker1&lt;/td&gt;
&lt;td&gt;node1&lt;/td&gt;
&lt;td&gt;Rocky 8.10&lt;/td&gt;
&lt;td&gt;192.168.59.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker2&lt;/td&gt;
&lt;td&gt;node2&lt;/td&gt;
&lt;td&gt;Rocky 8.10&lt;/td&gt;
&lt;td&gt;192.168.59.22&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에어갭 환경을 재현하기 위해 VM을 위와 같이 세팅합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VM들은 NAT Network로 묶고&lt;/li&gt;
&lt;li&gt;로컬 터미널에서 접근하기 위해 22번 포트로 포트포워딩 설정을 해줍니다.&lt;/li&gt;
&lt;li&gt;DNS를 비활성화하여 외부 통신이 차단된 상태로 준비합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 모든 VM에서 블랙홀 DNS 설정 (외부 resolve 차단)
for host in master node1 node2; do
  ssh $host &quot;echo 'nameserver 192.0.2.1' &amp;gt; /etc/resolv.conf&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 Kubespray로 설치하고, 오프라인에 필요한 rpm, pip, 바이너리, 컨테이너 이미지는 사전에 빌드해서 &lt;code&gt;/root/kubespray-offline-build/&lt;/code&gt; 아래에 준비해둔 상태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://drive.google.com/drive/folders/1WsFwJxH0P3-fV5r5VvyUQ-nW9u-wBsEj?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 링크&lt;/a&gt;를 참고하시면 컨테이너 이미지를 포함한 kubespray-offline-build를 받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: 노드 사전 준비&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 전에 모든 노드에서 공통으로 해줘야 하는 작업들이 있습니다. SSH 키 교환, 방화벽 해제, 스왑 비활성화, 시간 동기화 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# SSH 키 생성 및 교환 (master에서만 진행주셔도 됩니다)
ssh-keygen -t rsa -f ~/.ssh/id_rsa
ssh-copy-id -i ~/.ssh/id_rsa.pub root@master
ssh-copy-id -i ~/.ssh/id_rsa.pub root@node1
ssh-copy-id -i ~/.ssh/id_rsa.pub root@node2

# 방화벽 해제
for host in master node1 node2; do
  ssh $host &quot;sudo systemctl stop firewalld &amp;amp;&amp;amp; sudo systemctl disable firewalld&quot;
  ssh $host &quot;sudo iptables -F&quot;
  ssh $host &quot;sudo nft flush ruleset&quot;
done

# 스왑 비활성화
for host in master node1 node2; do
  ssh $host &quot;sudo swapoff -a &amp;amp;&amp;amp; sudo sed -i '/swap/d' /etc/fstab&quot;
done

# 시간 동기화 (master 기준으로 맞춤)
MASTER_TIME=$(date '+%Y-%m-%d %H:%M:%S')
for host in node1 node2; do
  ssh $host &quot;timedatectl set-ntp false; date -s '${MASTER_TIME}'; hwclock --systohc&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 키는 Kubespray(Ansible)가 master에서 각 노드에 접속해서 명령을 실행하기 위해 필요합니다. 나중에 나올 파일 서버와는 별개의 목적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: RPM 패키지 설치 및 온라인 repo 차단&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전에 빌드해둔 RPM 패키지를 모든 노드에 설치합니다. 설치 후에는 yum repo를 비활성화해서 온라인 저장소로 접근하지 못하게 막아야 합니다. 에어갭 환경에서 온라인 repo가 활성화되어 있으면 Kubespray 실행 중에 타임아웃이 걸릴 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# yum repo 온라인 비활성화
for host in master node1 node2; do
  ssh $host &quot;sudo sed -i 's/enabled=1/enabled=0/g' /etc/yum.repos.d/*.repo&quot;
done

# RPM 파일 복사
cp -r /root/kubespray-offline-build/rpms/ /root/rpms

for host in node1 node2; do
  ssh $host &quot;mkdir -p /root/rpms&quot;
  scp /root/rpms/*.rpm $host:/root/rpms
done

# 모든 노드에 설치
for host in master node1 node2; do
  ssh $host &quot;sudo rpm -Uvh --force /root/rpms/*.rpm&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rpm -Uvh&lt;/code&gt;에서 &lt;code&gt;-U&lt;/code&gt;는 이미 설치되어 있으면 업그레이드, 없으면 새로 설치하는 옵션입니다. &lt;code&gt;-v&lt;/code&gt;는 상세 출력, &lt;code&gt;-h&lt;/code&gt;는 진행률 표시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: Kubespray 의존성 설치 (master만)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;해당 Kubespray는 Python 3.9 이상을 요구합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rocky 8.10에는 Python 3.6이 기본으로 설치되어 있는데 이 버전으로는 Kubespray가 동작하지 않기 때문에 Python 3.9을 별도로 설치해줘야 합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Python 3.9 설치
rpm -Uvh --force /root/kubespray-offline-build/python39_rpms/*.rpm
python3.9 -m ensurepip --default-pip

# SELinux 바인딩 연결 (시스템 Python이 3.6인 경우)
ln -s /usr/lib64/python3.6/site-packages/selinux \
      /usr/lib64/python3.9/site-packages/selinux
ln -s /usr/lib64/python3.6/site-packages/_selinux.cpython-36m-x86_64-linux-gnu.so \
      /usr/lib64/python3.9/site-packages/_selinux.so

# pip 오프라인 설치
python3.9 -m pip install --no-index \
  --find-links /root/kubespray-offline-build/pip \
  -r /root/kubespray-offline-build/kubespray/requirements_main.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--no-index&lt;/code&gt;는 PyPI 같은 온라인 저장소를 참조하지 않겠다는 의미이고, &lt;code&gt;--find-links&lt;/code&gt;는 로컬에 있는 whl 파일들을 참조하라는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계: 파일 서버 구축 (master)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubespray가 쿠버네티스를 설치할 때 kubelet, kubeadm, etcd 같은 바이너리를 다운로드해야 합니다. 내부 플레이북이 curl/wget으로 URL에서 받아오는 방식이라, 이 바이너리들을 HTTP로 서빙해주는 파일 서버가 필요합니다. Kubespray가 node1에 &quot;kubelet 바이너리를 다운로드해라&quot;라고 명령을 보내면, node1이 HTTP로 어딘가에서 받아와야 하고 파일 서버에서 바이너리를 받아옵니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;cd /root/kubespray-offline-build/files
nohup python3.9 -m http.server 8080 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 Python HTTP 서버로 올립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5단계: containerd 수동 설치 (master)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;containerd는 컨테이너를 실제로 만들고 실행하는 런타임입니다. 다음 단계에서 레지스트리 컨테이너를 띄워야 하기 때문에 master에 먼저 수동으로 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# containerd 바이너리 설치
tar -xzf /root/kubespray-offline-build/files/containerd/containerd/releases/download/v1.6.8/containerd-1.6.8-linux-amd64.tar.gz -C /usr/local/

# runc 설치
cp /root/kubespray-offline-build/files/opencontainers/runc/releases/download/v1.1.4/runc.amd64 /usr/local/bin/runc
chmod +x /usr/local/bin/runc
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd 서비스 파일을 만들어서 containerd를 데몬으로 등록합니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/systemd/system/containerd.service
[Unit]
Description=containerd container runtime
After=network.target

[Service]
ExecStart=/usr/local/bin/containerd
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
systemctl daemon-reload
systemctl enable --now containerd
systemctl status containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 용량이 부족한 경우 &lt;code&gt;config.toml&lt;/code&gt;에서 &lt;code&gt;root&lt;/code&gt; 경로와 &lt;code&gt;state&lt;/code&gt; 경로를 넉넉한 마운트 지점으로 변경해줘야 합니다. &lt;code&gt;df -h&lt;/code&gt;로 확인해보시면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6단계: 프라이빗 레지스트리 실행&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nerdctl은 containerd를 위한 Docker 호환 CLI입니다. &lt;code&gt;docker run&lt;/code&gt;, &lt;code&gt;docker load&lt;/code&gt; 같은 명령을 containerd 환경에서 그대로 쓸 수 있게 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# nerdctl 설치
tar -xzf /root/kubespray-offline-build/files/containerd/nerdctl/releases/download/v0.22.2/nerdctl-0.22.2-linux-amd64.tar.gz -C /usr/local/bin/

# CNI 플러그인 설치 (컨테이너 네트워크 설정에 필요)
mkdir -p /opt/cni/bin/
tar -xzf /root/kubespray-offline-build/files/containernetworking/plugins/releases/download/v1.1.1/cni-plugins-*.tgz -C /opt/cni/bin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;registry:2.8.1 이미지를 로드하고 컨테이너로 실행합니다. kube-apiserver, coredns 같은 이미지를 인터넷 없이 pull 하려면 이 레지스트리에 미리 등록해두고 여기서 가져오게 해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# 레지스트리 데이터 디렉토리 (용량 넉넉한 곳으로)
mkdir -p /home/docker-registry

# 이미지 로드 및 실행
nerdctl load -i /root/kubespray-offline-build/images/docker.io_library_registry-2.8.1.tar
nerdctl run -d --name registry --network host --restart=always \
  -v /home/docker-registry:/var/lib/registry registry:2.8.1

# 확인
nerdctl ps
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--network host&lt;/code&gt;로 실행하면 별도 포트 매핑 없이 호스트의 5000 포트에서 바로 접근할 수 있습니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7단계: 이미지 등록 (load &amp;rarr; tag &amp;rarr; push)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계가 제일 중요합니다. 오프라인으로 준비한 tar 파일들(kube-apiserver, kube-scheduler, kube-controller-manager, coredns, pause, calico/node, metrics-server 같은 것들)을 containerd에 로드하고, 프라이빗 레지스트리에 push 해야 합니다. 흐름은 이렇습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;load&lt;/b&gt;: tar &amp;rarr; containerd에 이미지 등록 (master 로컬에만 존재)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tag&lt;/b&gt;: 레지스트리 주소로 이미지 이름 변경 (엔드포인트처럼 작동)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;push&lt;/b&gt;: 레지스트리 서버에 업로드&lt;/li&gt;
&lt;li&gt;이후 각 노드가 레지스트리에서 &lt;b&gt;pull&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;insecure registry 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;containerd는 기본적으로 레지스트리에 HTTPS로 통신합니다. 그런데 지금 올린 레지스트리는 HTTP로 돌아가고 있습니다. &lt;code&gt;config.toml&lt;/code&gt;에 HTTP 접근을 허용하는 설정을 추가해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/etc/containerd/config.toml&lt;/code&gt;의 &lt;code&gt;[plugins]&lt;/code&gt; 부분을 아래로 교체합니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[plugins]
  [plugins.&quot;io.containerd.grpc.v1.cri&quot;]
    sandbox_image = &quot;192.168.59.11:5000/pause:3.6&quot;
    max_container_log_line_size = -1
    [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd]
      default_runtime_name = &quot;runc&quot;
      snapshotter = &quot;overlayfs&quot;
      [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc]
          runtime_type = &quot;io.containerd.runc.v2&quot;
          runtime_engine = &quot;&quot;
          runtime_root = &quot;&quot;
          [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc.options]
            systemdCgroup = true
    [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry]
      [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;docker.io&quot;]
          endpoint = [&quot;http://192.168.59.11:5000&quot;]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;quay.io&quot;]
          endpoint = [&quot;http://192.168.59.11:5000&quot;]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;registry.k8s.io&quot;]
          endpoint = [&quot;http://192.168.59.11:5000&quot;]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;gcr.io&quot;]
          endpoint = [&quot;http://192.168.59.11:5000&quot;]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;ghcr.io&quot;]
          endpoint = [&quot;http://192.168.59.11:5000&quot;]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.mirrors.&quot;192.168.59.11:5000&quot;]
          endpoint = [&quot;http://192.168.59.11:5000&quot;]
      [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.configs]
        [plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry.configs.&quot;192.168.59.11:5000&quot;.tls]
          insecure_skip_verify = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mirror 설정의 핵심은 docker.io, quay.io, registry.k8s.io 등 공개 레지스트리로 향하는 pull 요청을 전부 로컬 레지스트리(&lt;code&gt;192.168.59.11:5000&lt;/code&gt;)로 리다이렉트하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;certs.d 설정도 생성합니다. containerd 1.6 이하에서는 &lt;code&gt;_default&lt;/code&gt; 디렉토리를 지원하지 않기 때문에 레지스트리 주소로 디렉토리를 만들어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;mkdir -p &quot;/etc/containerd/certs.d/192.168.59.11:5000&quot;
cat &amp;gt; &quot;/etc/containerd/certs.d/192.168.59.11:5000/hosts.toml&quot; &amp;lt;&amp;lt; 'EOF'
server = &quot;http://192.168.59.11:5000&quot;

[host.&quot;http://192.168.59.11:5000&quot;]
  capabilities = [&quot;pull&quot;, &quot;resolve&quot;]
  skip_verify = true
EOF

systemctl restart containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 node1, node2에도 전파합니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;for host in node1 node2; do
  ssh $host &quot;mkdir -p /etc/containerd/&quot;
  scp /etc/containerd/config.toml $host:/etc/containerd/
  ssh $host &quot;mkdir -p '/etc/containerd/certs.d/192.168.59.11:5000'&quot;
  scp &quot;/etc/containerd/certs.d/192.168.59.11:5000/hosts.toml&quot; \
    &quot;$host:/etc/containerd/certs.d/192.168.59.11:5000/hosts.toml&quot;
  ssh $host &quot;systemctl restart containerd&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 로드 및 push&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 모든 이미지를 containerd에 로드
for img in /root/kubespray-offline-build/images/*.tar; do
  nerdctl -n k8s.io load -i &quot;$img&quot;
done

# 로드 확인
nerdctl images
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tag + push를 자동으로 처리하는 스크립트입니다. 이미지 이름에서 레지스트리 prefix를 떼고, 로컬 레지스트리 주소를 붙여서 push 합니다. (링크에 있는 폴더를 다운받으실 경우 images 아래에 install.sh가 준비되어 있습니다.)&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
REGISTRY=&quot;192.168.59.11:5000&quot;

nerdctl -n k8s.io images --format '{{.Repository}}:{{.Tag}}' \
  | grep -v none | grep -v &quot;${REGISTRY}&quot; | while read image; do
  name=&quot;$image&quot;
  name=$(echo &quot;$name&quot; | sed 's@^registry\.k8s\.io/@@')
  name=$(echo &quot;$name&quot; | sed 's@^quay\.io/@@')
  name=$(echo &quot;$name&quot; | sed 's@^ghcr\.io/@@')
  name=$(echo &quot;$name&quot; | sed 's@^docker\.io/@@')

  if [[ &quot;$name&quot; != */* ]]; then
    name=&quot;library/${name}&quot;
  fi

  new_name=&quot;${REGISTRY}/${name}&quot;
  echo &quot;push: ${image} &amp;rarr; ${new_name}&quot;
  nerdctl -n k8s.io tag &quot;$image&quot; &quot;$new_name&quot;
  nerdctl -n k8s.io push --insecure-registry &quot;$new_name&quot;
done
echo &quot;완료!&quot;

# 이미지 pulling 확인
nerdctl -n k8s.io pull 192.168.59.11:5000/calico/node:v3.23.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8단계: inventory 설정&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubespray의 inventory를 생성하고 노드 구성을 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;cd /root/kubespray-offline-build/kubespray
cp -rfp inventory/offline-prep inventory/offline
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/root/kubespray-offline-build/kubespray/`inventory/offline/inventory.ini&lt;/code&gt;를 수정합니다. (해당 링크에 있는 파일은 이미 수정되어 있습니다)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[all]
node1  ansible_host=node1  ip=192.168.59.21
node2  ansible_host=node2  ip=192.168.59.22
master ansible_host=master ip=192.168.59.11 etcd_member_name=etcd1

[kube_control_plane]
master

[etcd]
master

[kube_node]
node1
node2

[calico_rr]

[k8s_cluster:children]
kube_control_plane
kube_node
calico_rr
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9단계: offline.yml 설정&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubespray가 바이너리와 이미지를 로컬 파일 서버/레지스트리에서 가져오도록 offline.yml을 설정합니다. 이 설정이 에어갭 설치의 핵심입니다. (이 부분도 링크에 있는 폴더를 다운받으시면 준비되어 있습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/root/kubespray-offline-build/kubespray/inventory/offline/group_vars/all/offline.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;download_run_once: true        # 파일을 1번만 받아 다른 노드에 전파
download_delegate: master      # 다운로드 작업을 master에 위임

files_repo: &quot;http://192.168.59.11:8080&quot;

kubeadm_download_url: &quot;{{ files_repo }}/kubernetes-release/release/{{ kubeadm_version }}/bin/linux/{{ image_arch }}/kubeadm&quot;
kubectl_download_url: &quot;{{ files_repo }}/kubernetes-release/release/{{ kube_version }}/bin/linux/{{ image_arch }}/kubectl&quot;
kubelet_download_url: &quot;{{ files_repo }}/kubernetes-release/release/{{ kube_version }}/bin/linux/{{ image_arch }}/kubelet&quot;

cni_download_url: &quot;{{ files_repo }}/containernetworking/plugins/releases/download/{{ cni_version }}/cni-plugins-linux-{{ image_arch }}-{{ cni_version }}.tgz&quot;
etcd_download_url: &quot;{{ files_repo }}/etcd-io/etcd/releases/download/{{ etcd_version }}/etcd-{{ etcd_version }}-linux-{{ image_arch }}.tar.gz&quot;
crictl_download_url: &quot;{{ files_repo }}/kubernetes-sigs/cri-tools/releases/download/{{ crictl_version }}/crictl-{{ crictl_version }}-{{ ansible_system | lower }}-{{ image_arch }}.tar.gz&quot;
runc_download_url: &quot;{{ files_repo }}/opencontainers/runc/releases/download/{{ runc_version }}/runc.{{ image_arch }}&quot;
containerd_download_url: &quot;{{ files_repo }}/containerd/containerd/releases/download/v{{ containerd_version }}/containerd-{{ containerd_version }}-linux-{{ image_arch }}.tar.gz&quot;
nerdctl_download_url: &quot;{{ files_repo }}/containerd/nerdctl/releases/download/v{{ nerdctl_version }}/nerdctl-{{ nerdctl_version }}-{{ ansible_system | lower }}-{{ image_arch }}.tar.gz&quot;

calicoctl_download_url: &quot;{{ files_repo }}/projectcalico/calico/releases/download/{{ calico_ctl_version }}/calicoctl-linux-{{ image_arch }}&quot;
calico_crds_download_url: &quot;{{ files_repo }}/projectcalico/calico/archive/{{ calico_version }}.tar.gz&quot;

helm_download_url: &quot;{{ files_repo }}/helm-{{ helm_version }}-linux-{{ image_arch }}.tar.gz&quot;

registry_host: &quot;192.168.59.11:5000&quot;
kube_image_repo: &quot;{{ registry_host }}&quot;
gcr_image_repo: &quot;{{ registry_host }}&quot;
docker_image_repo: &quot;{{ registry_host }}&quot;
quay_image_repo: &quot;{{ registry_host }}&quot;
github_image_repo: &quot;{{ registry_host }}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;files_repo&lt;/code&gt;는 4단계에서 올린 파일 서버를, &lt;code&gt;registry_host&lt;/code&gt;는 6단계에서 올린 프라이빗 레지스트리를 가리킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubespray가 기본 지원하는 k8s 버전이 아닌 다른 버전을 설치하고 싶다면 &lt;code&gt;group_vars/k8s_cluster/k8s_cluster.yml&lt;/code&gt;에서 &lt;code&gt;kube_version&lt;/code&gt;을 수정하면 됩니다. (현재 1.23.7로 설정되어 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10단계: 설치 실행&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ping 체크부터 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ansible -i /root/kubespray-offline-build/kubespray/inventory/offline/inventory.ini all -m ping
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 SUCCESS가 뜨면 플레이북을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;ansible-playbook \
  -i /root/kubespray-offline-build/kubespray/inventory/offline/inventory.ini \
  --become --become-user=root \
  /root/kubespray-offline-build/kubespray/cluster.yml \
  --skip-tags=packages
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--skip-tags=packages&lt;/code&gt;는 온라인 패키지 설치 태스크를 건너뛰는 옵션입니다. 에어갭 환경에서는 필수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11단계: 설치 후 정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;CoreDNS / nodelocaldns forward 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에어갭 환경에서는 외부 DNS 포워딩이 의미가 없습니다. CoreDNS와 nodelocaldns configmap에서 &lt;code&gt;forward&lt;/code&gt; 부분과 &lt;code&gt;loop&lt;/code&gt; 플러그인을 삭제합니다. 그대로 두면 외부 도메인 resolve 시도 &amp;rarr; 타임아웃 &amp;rarr; DNS 전반 불안정으로 이어질 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;kubectl edit configmap coredns -n kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Before
.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
      pods insecure
      fallthrough in-addr.arpa ip6.arpa
    }
    forward . /etc/resolv.conf {    # &amp;larr; 삭제
      prefer_udp                    # &amp;larr; 삭제
    }                               # &amp;larr; 삭제
    loop                            # &amp;larr; 삭제
    cache 30
    reload
    loadbalance
}

# After
.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
      pods insecure
      fallthrough in-addr.arpa ip6.arpa
    }
    cache 30
    reload
    loadbalance
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nodelocaldns는 블록이 여러 개 있는데, &lt;code&gt;.:53&lt;/code&gt; 블록의 &lt;code&gt;forward&lt;/code&gt;와 &lt;code&gt;loop&lt;/code&gt;을 삭제합니다 . &lt;code&gt;cluster.local:53&lt;/code&gt; 블록의 forward는 CoreDNS(&lt;code&gt;10.233.0.3&lt;/code&gt;)를 가리키는 거라 그대로 둡니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;kubectl edit configmap nodelocaldns -n kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# Before
.:53 {
    errors
    cache 30
    reload
    loop                              # &amp;larr; 삭제
    bind 169.254.25.10
    forward . /etc/resolv.conf        # &amp;larr; 삭제
}

# After
.:53 {
    errors
    cache 30
    reload
    bind 169.254.25.10
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;containerd config.toml 재설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ansible이 플레이북 실행 중에 &lt;code&gt;config.toml&lt;/code&gt;을 자기 기본값으로 덮어씁니다. 설치가 끝난 후 7단계에서 설정했던 insecure registry 설정을 다시 적용해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# master에서 config.toml 재설정 후
systemctl restart containerd

# node1, node2에도 전파
for host in node1 node2; do
  scp /etc/containerd/config.toml $host:/etc/containerd/
  ssh $host &quot;mkdir -p '/etc/containerd/certs.d/192.168.59.11:5000'&quot;
  scp &quot;/etc/containerd/certs.d/192.168.59.11:5000/hosts.toml&quot; \
    &quot;$host:/etc/containerd/certs.d/192.168.59.11:5000/hosts.toml&quot;
  ssh $host &quot;systemctl restart containerd &amp;amp;&amp;amp; systemctl status containerd&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 못 당겨서 문제가 생긴 pod가 있으면 delete 하면 재생성되면서 정상적으로 뜹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12단계: 설치 확인&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get po -A
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 노드가 Ready 상태이고, kube-system 네임스페이스의 pod들이 전부 Running이면 설치 완료입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에어갭 환경에서 쿠버네티스를 설치하려면 바이너리를 파일 서버로 서빙하고, 컨테이너 이미지를 프라이빗 레지스트리에 올려두고, Kubespray 설정에서 이쪽을 바라보게 바꿔주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수할 수 있는 부분은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;containerd config.toml&lt;/b&gt;: Ansible이 설치 중에 덮어쓰기 때문에 설치 후 반드시 다시 설정해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;insecure registry&lt;/b&gt;: HTTP 레지스트리를 쓰려면 config.toml의 mirror 설정 + certs.d 설정 둘 다 해줘야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 tag 규칙&lt;/b&gt;: push 할 때 원본 이미지에서 레지스트리 prefix를 떼고 로컬 주소를 붙여야 합니다. 안 그러면 pull 시 이미지를 못 찾습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CoreDNS forward&lt;/b&gt;: 에어갭이면 외부 DNS forward를 꺼야 합니다. 안 그러면 DNS 루프로 장애가 전파될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발지식/Ops</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/111</guid>
      <comments>https://halfmoonbearlog.tistory.com/111#entry111comment</comments>
      <pubDate>Wed, 15 Apr 2026 20:10:22 +0900</pubDate>
    </item>
    <item>
      <title>트랜스포머 쉽게 이해하기&amp;nbsp;(1) - 인코더</title>
      <link>https://halfmoonbearlog.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;트랜스포머에서의 인코딩 과정 (토큰화, 벡터화 멀티헤드 어텐션 등) 전체를 다룹니다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인코더 블록의 전체 구조&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코더는 아래의 단계들을 하나의 블록으로 묶어서 여러 번 반복합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;멀티헤드 어텐션&lt;/b&gt; (쿼리, 키, 벨류를 이용한 연산)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;잔차 연결 및 층 정규화&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앞먹임 신경망&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 블록을 통과한 결과가 다음 블록의 입력이 되고, 이 과정을 반복할수록 단어 간의 관계가 점점 더 정교하게 표현됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코딩 과정을 차근차근 설명해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: 쿼리, 키, 벨류 벡터 만들기&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;I am study&quot;&lt;/b&gt;라는 문장이 입력되면, 먼저 각 단어를 토큰화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &quot;studying&quot;이라는 단어는 &quot;study&quot;와 &quot;##ing&quot;처럼 더 작은 단위로 쪼개질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나눠진 각 토큰은 숫자로 변환되어 &lt;b&gt;토큰 임베딩 벡터&lt;/b&gt;(512차원)로 표현됩니다. 여기에 단어의 순서 정보를 담은 &lt;b&gt;위치 벡터&lt;/b&gt;(512차원)를 더하면, 인코더 블록에 들어가는 입력이 완성됩니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&quot;I&quot;라는 단어 하나가 512개의 숫자로 이루어진 벡터로 표현되는 것입니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;이 입력 벡터로부터, 각 단어마다 세 종류의 벡터가 만들어집니다. &lt;b&gt;쿼리, 키, 벨류 벡터&lt;/b&gt;는 각각 역할이 다르지만, 만들어지는 방식은 동일합니다. &lt;b&gt;하나의 입력 벡터에 각기 다른 가중치 행렬을 곱해서 만듭니다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;예를 들어 &quot;I&quot;에 대해서는 다음과 같습니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(I의 임베딩 벡터 + 위치 벡터) &amp;times; &lt;b&gt;쿼리 가중치&lt;/b&gt; = &lt;b&gt;쿼리 벡터&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;(I의 임베딩 벡터 + 위치 벡터) &amp;times; &lt;b&gt;키 가중치&lt;/b&gt; = &lt;b&gt;키 벡터&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;(I의 임베딩 벡터 + 위치 벡터) &amp;times; &lt;b&gt;값 가중치&lt;/b&gt; = &lt;b&gt;값 벡터&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;같은 과정이 &quot;am&quot;과 &quot;study&quot;에 대해서도 각각 수행됩니다. 즉, 모든 단어가 자신만의 쿼리&amp;middot;키&amp;middot;벨류 벡터를 갖게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQvRRZ/dJMcaju6l5y/sCxCivRZBgTyTpZtB9t4zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQvRRZ/dJMcaju6l5y/sCxCivRZBgTyTpZtB9t4zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQvRRZ/dJMcaju6l5y/sCxCivRZBgTyTpZtB9t4zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQvRRZ%2FdJMcaju6l5y%2FsCxCivRZBgTyTpZtB9t4zK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;826&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: 어텐션 계산 &amp;mdash; 단어 간의 관계 파악하기&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 한 단어의 쿼리 벡터가 모든 단어의 키 벡터와 연산을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;I&quot;를 기준으로 설명하겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;I&quot;의 쿼리 벡터&lt;/b&gt;를 &lt;b&gt;&quot;I&quot;의 키 벡터&lt;/b&gt;, &lt;b&gt;&quot;am&quot;의 키 벡터&lt;/b&gt;, &lt;b&gt;&quot;study&quot;의 키 벡터&lt;/b&gt;와 각각 연산합니다.&lt;/li&gt;
&lt;li&gt;그럼 &quot;I&quot;라는 단어 하나에 대해 3개의 값이 나옵니다. (이 값들은 &quot;I&quot;가 각 단어와 얼마나 관련이 있는지를 나타냅니다.)&lt;/li&gt;
&lt;li&gt;이 3개의 값을 &lt;b&gt;정규화&lt;/b&gt;합니다. (정규화란 이 값들의 합이 1이 되도록 변환하는 것입니다.) 예를 들어 0.55, 0.30, 0.15처럼 변환되면, &quot;I&quot;는 자기 자신과 55%의 관련성을, &quot;am&quot;과는 30%의 관련성을, &quot;study&quot;와는 15%의 관련성을 가진다는 뜻입니다.&lt;/li&gt;
&lt;li&gt;이 정규화된 값들을 각 단어의 &lt;b&gt;벨류 벡터&lt;/b&gt;와 연산합니다. (정규화된 값을 벨류 벡터에 곱한 뒤 모두 더합니다.) 그 결과 &lt;b&gt;64차원의 벡터&lt;/b&gt; 하나가 만들어집니다. 이 벡터가 바로 &quot;I&quot;라는 단어가 문장 전체의 맥락을 반영하여 새롭게 표현된 결과입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 과정을 &quot;am&quot;과 &quot;study&quot;에 대해서도 각각 수행합니다. 따라서 단어 3개에 대해 64차원 벡터 3개가 생성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;881&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sghE5/dJMcagrCQVN/LpMf4YPMpdm4RcikacinF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sghE5/dJMcagrCQVN/LpMf4YPMpdm4RcikacinF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sghE5/dJMcagrCQVN/LpMf4YPMpdm4RcikacinF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsghE5%2FdJMcagrCQVN%2FLpMf4YPMpdm4RcikacinF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1279&quot; height=&quot;881&quot; data-origin-width=&quot;1279&quot; data-origin-height=&quot;881&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: 멀티헤드 어텐션 &amp;mdash; 여러 관점에서 바라보기&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 쿼리-키-벨류 연산은 하나의 &lt;b&gt;헤드&lt;/b&gt;에서 일어나는 일입니다. 멀티헤드 어텐션은 이 과정을 &lt;b&gt;여러 개의 헤드&lt;/b&gt;에서 동시에 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 헤드는 &lt;b&gt;서로 다른 가중치 행렬&lt;/b&gt;을 사용합니다. 따라서 같은 단어라 하더라도 헤드마다 다른 쿼리, 키, 벨류 벡터가 만들어지고, 결과적으로 각 헤드는 단어 간의 관계를 서로 다른 관점에서 파악하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 헤드의 결과는 단어당 64차원 벡터입니다. 이 벡터들을 모든 헤드에 대해 &lt;b&gt;이어붙입니다&lt;/b&gt;. 예를 들어 헤드가 8개라면, 64차원 &amp;times; 8 = 512차원의 벡터가 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음, 이어붙인 벡터에 &lt;b&gt;멀티헤드 어텐션 가중치&lt;/b&gt;를 곱합니다. 이 연산을 통해 여러 헤드가 각각 발견한 정보를 하나로 통합한 최종 결과가 만들어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1171&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cf5Te2/dJMcahYm5uE/k7KgP5CZi5FYZvkaaoi160/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cf5Te2/dJMcahYm5uE/k7KgP5CZi5FYZvkaaoi160/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cf5Te2/dJMcahYm5uE/k7KgP5CZi5FYZvkaaoi160/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcf5Te2%2FdJMcahYm5uE%2Fk7KgP5CZi5FYZvkaaoi160%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1171&quot; height=&quot;675&quot; data-origin-width=&quot;1171&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;img src=&quot;Files/image%203.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계: 잔차 연결과 층 정규화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티헤드 어텐션의 결과가 나오면, 바로 다음 단계로 넘어가지 않고 &lt;b&gt;잔차 연결&lt;/b&gt;을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잔차 연결이란, &lt;b&gt;멀티헤드 어텐션 블록에 들어가기 직전의 입력&lt;/b&gt;(토큰 임베딩 벡터 + 위치 벡터)을 그대로 보존해 두었다가, 멀티헤드 어텐션을 거쳐 나온 결과에 &lt;b&gt;더해주는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;잔차 연결 결과 = 블록의 입력(원본) + 멀티헤드 어텐션 결과 (3단계의 결과)&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이렇게 할까요? 멀티헤드 어텐션을 거치면서 원래 입력이 가지고 있던 정보가 손실될 수 있습니다. 원본 입력을 더해줌으로써 원래의 정보를 보존하면서도 새롭게 학습한 관계 정보를 함께 가져갈 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잔차 연결 이후에는 &lt;b&gt;층 정규화&lt;/b&gt;를 수행합니다. 층 정규화는 벡터의 값들이 너무 크거나 작아지지 않도록 안정적인 범위로 조정해주는 과정입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjLJuZ/dJMcaju6l5X/bm8SSLVTnStdKEGtiwCS81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjLJuZ/dJMcaju6l5X/bm8SSLVTnStdKEGtiwCS81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjLJuZ/dJMcaju6l5X/bm8SSLVTnStdKEGtiwCS81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjLJuZ%2FdJMcaju6l5X%2Fbm8SSLVTnStdKEGtiwCS81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1145&quot; height=&quot;812&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Files/image%204.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5단계: 앞먹임 신경망&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;층 정규화까지 마친 결과는 &lt;b&gt;앞먹임 신경망&lt;/b&gt;으로 전달됩니다. 앞먹임 신경망은 각 단어의 벡터를 독립적으로 변환하여, 더 풍부한 표현을 만들어냅니다. 앞먹임 신경망을 거친 후에도 다시 잔차 연결과 층 정규화가 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;멀티헤드 어텐션 &amp;rarr; 잔차 연결 및 층 정규화 &amp;rarr; 앞먹임 신경망 &amp;rarr; 잔차 연결 및 층 정규화&lt;/b&gt;까지가 인코더의 블록 하나입니다. 이 블록을 여러 번 쌓아서 인코더가 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 셀프 어텐션과 크로스 어텐션이라는 개념이 나옵니다.&lt;br /&gt;&lt;b&gt;셀프 어텐션은 Q, K, V가 모두 같은 문장에서 나오게 됩니다&lt;/b&gt;. &amp;ldquo;I am studying&amp;rdquo;라는 하나의 문장에서 Q, K, V를 만드는 것을 셀프 어텐션이라고 합니다.&lt;br /&gt;&lt;b&gt;반면 크로스 어텐션은 Q를 한쪽 문장에서, K와 V는 다른 쪽 문장에서&lt;/b&gt; 만드는 것을 말합니다. 예를 들어 한국어로 &amp;ldquo;나는&amp;rdquo;에서 Q를 만들고 &amp;ldquo;I am studying&amp;rdquo;에서 K, V를 만드는 것을 크로스 어텐션이라고 합니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜스포머에서 사용자가 결정할 수 있는 변수(초모수, Hyper-Parameter)는 인코더 반복 횟수, 어텐션 헤드 수, 토큰 임베딩의 차원, 가중치 행렬의 크기, 토큰 사전의 크기 등이 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인코더 레이어의 전체 흐름 요약&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 인코더 레이어는 다음과 같은 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;입력(512차원)
  &amp;rarr; 멀티헤드 셀프 어텐션 (8개의 어텐션 헤드)
  &amp;rarr; 잔차 연결 (어텐션 블록의 입력 + 어텐션 출력)
  &amp;rarr; 층 정규화
  &amp;rarr; 앞먹임 신경망(FFN, Feed-Forward Network)
  &amp;rarr; 잔차 연결 (FFN 입력 + FFN 출력)
  &amp;rarr; 층 정규화
  &amp;rarr; 출력(512차원)

* 첫 번째 인코더 레이어 안의 잔차 연결 입력: 단어의 토큰 임베딩 + 위치 임베딩
* 두 번째 인코더 레이어 안의 잔차 연결 입력: 이전 레이어의 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인코더 레이어를 &lt;b&gt;N번 반복&lt;/b&gt;합니다. 원 논문(Attention Is All You Need)에서는 N=6입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>개발지식/AI</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/110</guid>
      <comments>https://halfmoonbearlog.tistory.com/110#entry110comment</comments>
      <pubDate>Wed, 15 Apr 2026 20:08:47 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 깊이 이해하기 (6) &amp;mdash; 배포 전략, Pod 라이프사이클, 그리고 워크로드 오브젝트</title>
      <link>https://halfmoonbearlog.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;배포 전략의 종류와 차이점, Pod의 생성부터 종료까지의 라이프사이클, 그리고 Job, DaemonSet, StatefulSet 같은 워크로드 오브젝트와 커스텀 리소스를 다루는 글입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포 전략&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;대부분의 워크로드는 Deployment를 통해 관리됩니다. &lt;b&gt;Deployment가 ReplicaSet을 생성하고, ReplicaSet이 실제 Pod를 만드는 구조입니다.&lt;/b&gt; Deployment는 ReplicaSet이 저장한 리비전(revision)을 관리하며, spec.revisionHistoryLimit으로 보관할 리비전 개수를 설정할 수 있습니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아래는 Deployment의 배포 전략입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Recreate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Pod를 모두 삭제한 후 새 Pod를 생성합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 구버전과 신버전이 동시에 존재하지 않아 충돌 가능성이 없음&lt;/li&gt;
&lt;li&gt;단점: 삭제 후 생성까지 다운타임이 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spec:
  strategy:
    type: Recreate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Rolling Update (기본값)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Pod를 점진적으로 교체합니다. 업데이트 도중에도 사용자 요청을 처리할 수 있는 Pod가 항상 존재합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # 기본 레플리카 수 대비 추가로 생성 가능한 Pod 수
      maxUnavailable: 0    # 업데이트 중 사용 불가능한 Pod 수
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;maxSurge: 1, maxUnavailable: 0&lt;/code&gt; &amp;rarr; 항상 원래 레플리카 수 이상이 유지됨. 안전하지만 느림&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxSurge: 0, maxUnavailable: 1&lt;/code&gt; &amp;rarr; 하나를 먼저 삭제하고 새것을 생성. 리소스 절약&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxSurge: 2, maxUnavailable: 1&lt;/code&gt; &amp;rarr; 빠르게 교체. 리소스를 더 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Blue-Green 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스에 내장된 전략은 아니지만 Service의 라벨 셀렉터를 활용해서 구현합니다. 새로운 버전의 Pod를 미리 전부 생성해놓고, 준비가 완료되면 Service의 selector를 변경해서 트래픽을 한 번에 전환합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 기존: version: blue를 바라봄
# 전환: version: green으로 selector 변경
spec:
  selector:
    app: web
    version: green
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애 상황을 정밀하게 테스트하려면 Istio 같은 서비스 메쉬를 도입해서 트래픽 비율 제어(카나리 배포)까지 고려할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pod의 라이프사이클&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pod 하나가 생성 요청부터 종료까지 어떤 단계를 거치는지 전체 흐름을 먼저 보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: Pending &amp;mdash; 스케줄링 대기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod 생성 요청이 kube-apiserver에 의해 승인되었지만 아직 노드에 배정되지 않은 상태입니다.&lt;/li&gt;
&lt;li&gt;리소스가 부족하거나, Taint/Affinity 조건에 맞는 노드가 없으면 여기서 머무릅니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt;로 확인하면 Events 섹션에 &lt;code&gt;FailedScheduling&lt;/code&gt; 같은 메시지가 보입니다. Pending이 길어지면 스케줄링 조건부터 확인해보는 게 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: Init 컨테이너 &amp;mdash; 메인 컨테이너 전에 실행&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드에 배정되면 메인 컨테이너보다 먼저 Init 컨테이너가 실행됩니다.&lt;/li&gt;
&lt;li&gt;여러 개를 정의할 수 있고 &lt;b&gt;정의된 순서대로&lt;/b&gt; 하나씩 실행됩니다. 앞의 것이 성공해야 다음 것이 시작되고 하나라도 실패하면 해당 Init 컨테이너가 재시작됩니다.&lt;/li&gt;
&lt;li&gt;Init 컨테이너의 프로세스가 비정상 종료 코드를 반환하면 &lt;code&gt;Init:Error&lt;/code&gt;, 반복적으로 실패하면 &lt;code&gt;Init:CrashLoopBackOff&lt;/code&gt; 상태가 됩니다.&lt;/li&gt;
&lt;li&gt;활용 예시로는 DB 마이그레이션 스크립트 실행, 설정 파일 다운로드, 의존하는 서비스가 준비될 때까지 대기하는 경우 등이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 메인 컨테이너 시작 &amp;mdash; postStart와 Startup Probe&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Init 컨테이너가 전부 성공하면 메인 컨테이너가 시작됩니다. 이때 두 가지가 동시에 진행됩니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;postStart 훅&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;spec.containers[].lifecycle.postStart&lt;/code&gt;로 정의합니다.&lt;/li&gt;
&lt;li&gt;컨테이너 시작 직후 실행할 작업인데 &lt;b&gt;메인 프로세스(EntryPoint)와 비동기적으로 실행되기 때문에 어느 쪽이 먼저 실행될지 보장되지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;lifecycle:
  postStart:
    httpGet:              # 특정 URL로 HTTP 요청
      path: /init
      port: 8080
    # 또는
    exec:                 # 컨테이너 내부에서 명령어 실행
      command: [&quot;/bin/sh&quot;, &quot;-c&quot;, &quot;echo started &amp;gt; /tmp/status&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Startup Probe&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너의 초기화 작업이 완료되었는지 확인합니다.&lt;/li&gt;
&lt;li&gt;이 Probe가 성공해야 Liveness/Readiness Probe가 시작됩니다. &lt;b&gt;실패하면 컨테이너가 재시작됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;DB 마이그레이션 같은 무거운 초기화가 포함된 앱에서 Startup Probe 없이 Liveness Probe만 설정하면 아직 초기화 중인데 Liveness가 &quot;죽었다&quot;고 판단해서 무한 재시작 루프에 빠질 수 있습니다. Startup Probe가 이걸 방지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: Running &amp;mdash; Liveness &amp;amp; Readiness Probe&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Startup Probe가 성공하면 Pod는 Running 상태가 되고, 이제 두 가지 Probe가 주기적으로 동작합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Liveness Probe&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너가 살아있는지 확인합니다. 실패하면 kubelet이 컨테이너를 &lt;b&gt;재시작&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;데드락이나 무한 루프에 빠져서 응답은 없지만 프로세스는 살아있는 상황을 탐지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Readiness Probe&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트래픽을 받을 준비가 되었는지 확인합니다. 실패하면 &lt;b&gt;Service의 Endpoint에서 해당 Pod를 제외&lt;/b&gt;합니다. 재시작은 하지 않습니다.&lt;/li&gt;
&lt;li&gt;일시적으로 부하가 높아서 요청을 처리하기 어려운 상황에서 트래픽을 잠시 빼는 용도입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘의 차이 아래와 같으며 &lt;code&gt;kubectl get endpoints &amp;lt;서비스이름&amp;gt;&lt;/code&gt;으로 현재 트래픽을 받고 있는 Pod 목록을 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Liveness 실패 &amp;rarr; 컨테이너 재시작&lt;/li&gt;
&lt;li&gt;Readiness 실패 &amp;rarr; 트래픽만 차단(컨테이너는 유지)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 Probe 모두 동일한 체크 방식을 지원합니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 1. HTTP GET &amp;mdash; 200~399 응답이면 성공
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10        # 10초마다 체크
  failureThreshold: 3      # 3번 연속 실패하면 액션 수행

# 2. exec &amp;mdash; 종료 코드 0이면 성공
readinessProbe:
  exec:
    command: [&quot;cat&quot;, &quot;/tmp/healthy&quot;]

# 3. TCP Socket &amp;mdash; 연결이 수립되면 성공
startupProbe:
  tcpSocket:
    port: 3306
  failureThreshold: 30     # 30번까지 재시도 (&amp;times; periodSeconds = 최대 대기 시간)
  periodSeconds: 10&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비정상 경로: CrashLoopBackOff&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 시작 직후 반복적으로 실패하면 이 상태에 빠집니다. kubelet은 재시작을 시도하되, 주기를 점진적으로 늘립니다(10초 &amp;rarr; 20초 &amp;rarr; 40초 &amp;rarr; ... &amp;rarr; 최대 5분). 이미지가 잘못되었거나, 환경 변수 누락, 포트 충돌 등이 원인인 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Restart Policy &amp;mdash; 재시작 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;spec.restartPolicy&lt;/code&gt;로 컨테이너가 종료됐을 때 어떻게 할지를 설정합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;주로 쓰는 곳&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Always&lt;/b&gt; (기본값)&lt;/td&gt;
&lt;td&gt;종료 이유와 무관하게 항상 재시작&lt;/td&gt;
&lt;td&gt;Deployment, DaemonSet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;OnFailure&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;exit code &amp;ne; 0일 때만 재시작&lt;/td&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Never&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;재시작하지 않음&lt;/td&gt;
&lt;td&gt;Job, CronJob&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계: Termination &amp;mdash; 종료 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kubectl delete pod&lt;/code&gt;를 실행하면 Pod는 곧바로 사라지는 것이 아니라 정해진 순서를 밟습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Files/image%202.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은 &lt;b&gt;애플리케이션에서 SIGTERM 핸들러를 구현해야 한다&lt;/b&gt;는 점입니다. &lt;b&gt;SIGTERM을 받으면 처리 중인 요청을 마무리하고, DB 커넥션을 정리하고, 리소스를 해제하는 Graceful Shutdown 로직이 필요합니다&lt;/b&gt;. 이걸 구현하지 않으면 요청이 처리 도중 끊기거나, 30초 후 SIGKILL로 강제 종료되면서 데이터가 유실될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;워크로드 오브젝트&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 작업을 수행하고 종료하기 위한 오브젝트입니다. Pod가 작업을 완료하면 &lt;b&gt;Completed&lt;/b&gt; 상태가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: batch/v1
kind: Job
metadata:
  name: data-migration
spec:
  completions: 3        # 3개의 Pod가 성공적으로 종료되어야 Job 완료
  parallelism: 2        # 동시에 2개의 Pod를 실행
  template:
    spec:
      restartPolicy: Never    # Job에서는 Never 또는 OnFailure를 사용
      containers:
      - name: migrate
        image: migration:v1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job의 작업은 &lt;b&gt;멱등성(idempotency)&lt;/b&gt;을 보장해야 합니다. 노드 장애 등으로 Pod가 재생성될 수 있기 때문에, 같은 작업이 여러 번 실행되더라도 결과가 동일해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CronJob&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주기적으로 Job을 생성하는 오브젝트입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
spec:
  schedule: &quot;0 2 * * *&quot;    # 매일 새벽 2시
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: report
            image: report-generator:v1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DaemonSet&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 노드에 동일한 Pod를 하나씩 생성하는 오브젝트입니다. 노드가 추가되면 자동으로 해당 노드에도 Pod가 생성되고, 노드가 제거되면 함께 삭제됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대표적인 사용 사례&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;kube-proxy&lt;/b&gt;: 모든 노드에서 네트워크 프록시 역할&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Calico&lt;/b&gt;: 모든 노드에서 CNI 플러그인 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;node-exporter&lt;/b&gt;: 모든 노드에서 호스트 메트릭 수집&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fluentd/Filebeat&lt;/b&gt;: 모든 노드에서 로그 수집&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DaemonSet의 Pod는 QoS 클래스를 &lt;b&gt;Guaranteed&lt;/b&gt;로 설정하는 것이 좋습니다. requests와 limits를 동일하게 설정하면 리소스 부족 시에도 가장 늦게 퇴출되어 안정적으로 동작합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;StatefulSet&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태가 있는 워크로드를 위한 오브젝트입니다. Deployment가 만드는 Pod 이름은 랜덤 해시가 붙지만(예: &lt;code&gt;web-5d4b7f9c-xk2j3&lt;/code&gt;), StatefulSet은 순서가 있는 &lt;b&gt;고유한 이름&lt;/b&gt;을 부여합니다(예: &lt;code&gt;web-0&lt;/code&gt;, &lt;code&gt;web-1&lt;/code&gt;, &lt;code&gt;web-2&lt;/code&gt;).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Headless Service&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatefulSet은 개별 Pod에 직접 접근해야 하는 경우가 많습니다. &lt;code&gt;spec.serviceName&lt;/code&gt;에 &lt;b&gt;Headless Service&lt;/b&gt;(ClusterIP가 None인 Service)를 지정하면 Pod 이름을 통해 DNS로 직접 접근할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  clusterIP: None          # Headless Service
  selector:
    app: mysql
  ports:
  - port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql       # Headless Service 이름
  replicas: 3
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정하면 각 Pod에 &lt;code&gt;mysql-0.mysql.default.svc.cluster.local&lt;/code&gt; 같은 DNS 레코드가 생성됩니다. &lt;code&gt;nslookup mysql&lt;/code&gt;을 실행하면 접근 가능한 모든 Pod의 IP가 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;volumeClaimTemplates&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatefulSet은 Pod마다 &lt;b&gt;개별 PVC를 자동 생성&lt;/b&gt;하는 기능을 지원합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spec:
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [&quot;ReadWriteOnce&quot;]
      storageClassName: standard
      resources:
        requests:
          storage: 10Gi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 사용하면 각 Pod에 대해 별도의 PVC가 생성되고, StorageClass에 동적 프로비저닝이 설정되어 있다면 PV도 자동으로 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 &lt;b&gt;StatefulSet을 삭제해도 volumeClaimTemplates로 생성된 PVC와 PV는 자동으로 삭제되지 않는다&lt;/b&gt;는 것입니다. 데이터 보호를 위한 의도적인 설계이며, 필요 없어진 볼륨은 직접 삭제해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커스텀 리소스 (Custom Resource)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;쿠버네티스의 Deployment, Service 같은 기본 오브젝트 외에 사용자가 직접 새로운 리소스 타입을 정의할 수 있습니다. 이전 글에서 다뤘던 Jaeger Operator가 대표적인 예시입니다. &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 리소스를 만드는 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;컨트롤러를 구현하고 실행한다&lt;/b&gt; &amp;mdash; CR의 생성/변경/삭제를 watch하고, 바람직한 상태(Desired State)가 되도록 작업을 수행하는 프로그램&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CRD(Custom Resource Definition)를 생성한다&lt;/b&gt; &amp;mdash; 커스텀 리소스의 스키마(어떤 필드가 있고, 타입은 무엇인지)를 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CRD에 정의된 바를 기반으로 CR(Custom Resource)을 생성한다&lt;/b&gt; &amp;mdash; 실제 인스턴스&lt;/li&gt;
&lt;li&gt;1에서 생성한 컨트롤러가 CR 생성을 감지하고 &lt;b&gt;Reconcile&lt;/b&gt; 작업을 수행한다 (Reconcile - 아래에 설명이 있습니다)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# CRD 목록 확인
kubectl get crd

# CR 조회 (CRD가 등록되면 kubectl로 조회 가능)
kubectl get jaeger
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reconcile이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reconcile은 &lt;b&gt;현재 상태(Current State)를 바람직한 상태(Desired State)와 비교하고, 차이가 있으면 이를 맞추기 위한 작업을 수행하는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 모든 컨트롤러가 이 패턴을 따릅니다. 예를 들어 ReplicaSet 컨트롤러는 &quot;현재 Pod가 3개인데 원하는 상태는 5개&quot;면 2개를 추가로 생성합니다. 커스텀 컨트롤러도 마찬가지로, CR에 정의된 Desired State를 읽고 클러스터의 실제 상태를 조정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reconcile 함수는 이벤트 기반(edge-triggered)이 아니라 &lt;b&gt;레벨 기반(level-triggered)&lt;/b&gt; 으로 동작합니다. 특정 이벤트에 반응하는 것이 아니라, 호출될 때마다 현재 상태 전체를 확인하고 Desired State와 비교합니다. 그래서 중간에 어떤 이벤트가 발생했는지와 무관하게 항상 최종 상태를 목표로 작업합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴 덕분에 쿠버네티스를 &lt;b&gt;선언적(Declarative)&lt;/b&gt; 시스템이라고 합니다. &lt;b&gt;&lt;code&gt;kubectl apply -f&lt;/code&gt;는 쿠버네티스에게 &quot;이 상태가 되어라&quot;라고 알려주기만 하고 실제로 어떻게 도달할지는 컨트롤러가 Reconcile 루프를 통해 결정합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 401px;&quot; width=&quot;864&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recreate&lt;/td&gt;
&lt;td&gt;삭제 후 생성. 다운타임 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling Update&lt;/td&gt;
&lt;td&gt;점진적 교체. 기본 전략&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blue-Green&lt;/td&gt;
&lt;td&gt;새 버전 준비 후 라우팅 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod 라이프사이클&lt;/td&gt;
&lt;td&gt;Pending &amp;rarr; Init &amp;rarr; Running &amp;rarr; Terminating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe&lt;/td&gt;
&lt;td&gt;Startup &amp;rarr; Liveness + Readiness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graceful Shutdown&lt;/td&gt;
&lt;td&gt;SIGTERM 처리 로직 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job / CronJob&lt;/td&gt;
&lt;td&gt;일회성 / 주기적 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DaemonSet&lt;/td&gt;
&lt;td&gt;모든 노드에 동일 Pod 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StatefulSet&lt;/td&gt;
&lt;td&gt;고유 이름, 개별 볼륨, Headless Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Resource&lt;/td&gt;
&lt;td&gt;CRD 정의 &amp;rarr; CR 생성 &amp;rarr; 컨트롤러가 Reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>개발지식/Ops</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/109</guid>
      <comments>https://halfmoonbearlog.tistory.com/109#entry109comment</comments>
      <pubDate>Wed, 15 Apr 2026 20:06:54 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 깊이 이해하기 (5) &amp;mdash; 스케줄링과 모니터링, Pod는 어떻게 노드에 배치되는가</title>
      <link>https://halfmoonbearlog.tistory.com/108</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 클러스터에서 Pod가 어떤 노드에 생성되는지, 그리고 클러스터 상태를 어떻게 모니터링하는지를 다루는 글입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스케줄링: Pod가 노드에 배치되는 과정&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod 생성의 전체 흐름&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kubectl apply&lt;/code&gt;로 리소스를 생성하면 여러 컴포넌트가 순차적으로 동작합니다. 전체 흐름을 따라가 보겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 &lt;code&gt;kubectl apply -f&lt;/code&gt;로 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;kube-apiserver가 요청을 받아 &lt;b&gt;Authentication &amp;rarr; Authorization &amp;rarr; Admission Controller&lt;/b&gt;를 순서대로 통과시킨다.&lt;/li&gt;
&lt;li&gt;통과하면 etcd에 Pod 정보를 저장하는데, 이때 &lt;code&gt;nodeName&lt;/code&gt;은 비어 있는 상태다.&lt;/li&gt;
&lt;li&gt;kube-apiserver는 etcd를 watch하고 있다가 nodeName이 없는 새 Pod를 발견하면 kube-scheduler에게 알린다.&lt;/li&gt;
&lt;li&gt;kube-scheduler가 적절한 노드를 선택하고, 결과를 kube-apiserver에 전달한다.&lt;/li&gt;
&lt;li&gt;kube-apiserver는 etcd에 해당 Pod의 nodeName을 업데이트한다.&lt;/li&gt;
&lt;li&gt;해당 노드의 kubelet은 kube-apiserver에 걸어놓은 watch를 통해 자신에게 할당된 Pod를 감지하고, 실제로 컨테이너를 생성한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 etcd는 분산 키-값 저장소로, 클러스터에 생성된 모든 오브젝트의 메타데이터가 저장되어 있습니다. 중요한 점은 etcd의 데이터는 반드시 kube-apiserver를 통해서만 접근할 수 있다는 것입니다. &lt;code&gt;kubectl get pods&lt;/code&gt;를 실행하면 apiserver가 etcd에서 정보를 읽어서 반환하는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스케줄러의 노드 선택 기준&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-scheduler는 크게 &lt;b&gt;필터링(Filtering)&lt;/b&gt;단계와 &lt;b&gt;스코어링(Scoring)&lt;/b&gt; 단계를 거칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필터링 &amp;mdash; 후보에서 제외되는 노드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod의 &lt;code&gt;requests&lt;/code&gt;에 명시된 리소스를 충족할 수 없는 노드 (requests는 스케줄링의 필수 기준)&lt;/li&gt;
&lt;li&gt;장애가 발생한 노드 (NotReady, Unreachable 등)&lt;/li&gt;
&lt;li&gt;nodeSelector, nodeAffinity 조건에 맞지 않는 노드&lt;/li&gt;
&lt;li&gt;Taint가 설정되어 있고 Pod에 해당 Toleration이 없는 노드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스코어링 &amp;mdash; 남은 후보 중 점수가 높은 노드 선택&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가용 리소스가 많을수록 점수가 올라감&lt;/li&gt;
&lt;li&gt;Pod가 사용하는 컨테이너 이미지가 이미 노드에 존재하면 점수가 올라감 (이미지 pull 시간 절약)&lt;/li&gt;
&lt;li&gt;Pod/Node Affinity의 preferred 조건 매칭 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터링 후 남은 노드들의 점수를 총합해서 가장 높은 노드에 Pod가 배치됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;노드에 Pod를 할당하는 방법&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. nodeName 직접 지정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 방법으로, Pod YAML에 &lt;code&gt;nodeName&lt;/code&gt;을 직접 명시합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;spec:
  nodeName: worker-node-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케줄러를 거치지 않고 해당 노드에 직접 배치됩니다. 테스트 용도 외에는 거의 쓰지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. nodeSelector&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드에 라벨을 정의하고, Pod에서 해당 라벨과 일치하는 노드를 선택합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;kubectl label nodes worker-node-1 disktype=ssd
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spec:
  nodeSelector:
    disktype: ssd
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Node Affinity&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nodeSelector보다 유연한 조건을 설정할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;requiredDuringSchedulingIgnoredDuringExecution&lt;/b&gt; (Hard 조건): 반드시 만족해야 스케줄링됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;preferredDuringSchedulingIgnoredDuringExecution&lt;/b&gt; (Soft 조건): 가능하면 만족하는 노드를 선호하지만, 불가능하면 다른 노드에도 배치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - ap-northeast-2a
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름의 &lt;code&gt;IgnoredDuringExecution&lt;/code&gt; 부분은 &quot;이미 실행 중인 Pod는 노드 라벨이 바뀌더라도 퇴출하지 않는다&quot;는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Pod Affinity&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 라벨을 가진 Pod가 이미 실행 중인 노드에 함께 배치합니다. 예를 들어 캐시 서버와 애플리케이션 서버를 같은 노드에 두고 싶을 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - redis-cache
        topologyKey: kubernetes.io/hostname
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;topologyKey&lt;/code&gt;는 &quot;같은 곳&quot;의 범위를 정의합니다. &lt;code&gt;kubernetes.io/hostname&lt;/code&gt;이면 같은 노드, &lt;code&gt;topology.kubernetes.io/zone&lt;/code&gt;이면 같은 가용 영역(AZ)이 됩니다. 노드에 붙어 있는 라벨의 키를 기준으로 같은 값을 가진 노드들이 하나의 토폴로지 도메인으로 묶입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Pod Anti-Affinity&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod Affinity의 반대 개념으로 &lt;b&gt;특정 라벨을 가진 Pod가 실행 중인 토폴로지 도메인을 피해서 배치&lt;/b&gt;합니다. 고가용성을 위해 같은 Deployment의 레플리카를 서로 다른 노드에 분산시킬 때 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - web-frontend
        topologyKey: kubernetes.io/hostname
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 &lt;code&gt;app=web-frontend&lt;/code&gt; 라벨을 가진 Pod가 이미 존재하는 노드에는 새 Pod를 배치하지 않습니다. 3개 레플리카가 3개의 서로 다른 노드에 분산되는 효과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Taints와 Tolerations&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Affinity가 &quot;이 노드에 가고 싶다&quot;는 Pod 입장의 설정이라면, Taint는 &quot;이 노드에 오지 마라&quot;는 노드 입장의 설정입니다. &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Taint 설정&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;kubectl taint nodes worker-node-1 gpu=true:NoSchedule
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Effect 종류&lt;/h4&gt;
&lt;table style=&quot;height: 159px;&quot; width=&quot;792&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;Effect&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;NoSchedule&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;새 Pod의 스케줄링을 막음. 이미 실행 중인 Pod에는 영향 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;NoExecute&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;새 Pod 스케줄링을 막음. 이미 실행 중인 Pod도 퇴출시킴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;PreferNoSchedule&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;가능하면 스케줄링하지 않음. Soft 버전의 NoSchedule&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Toleration 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Taint가 설정된 노드에 Pod를 배치하려면 Pod에 Toleration을 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spec:
  tolerations:
  - key: &quot;gpu&quot;
    operator: &quot;Equal&quot;
    value: &quot;true&quot;
    effect: &quot;NoSchedule&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마스터 노드의 Taint&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 기본적으로 컨트롤 플레인 노드(마스터 노드)에 Taint를 설정합니다. 그래서 일반 워크로드 Pod가 마스터 노드에 스케줄링되지 않습니다. 컨트롤 플레인 컴포넌트들(kube-apiserver, kube-scheduler 등)은 이 Taint에 대한 Toleration을 갖고 있어서 마스터 노드에서 실행될 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 Taint 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 Node Controller는 노드에 문제가 발생하면 자동으로 Taint를 추가합니다. &lt;code&gt;kubectl describe node &amp;lt;노드이름&amp;gt;&lt;/code&gt;의 Taints 섹션에서 확인할 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;height: 246px;&quot; width=&quot;785&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Taint&lt;/th&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node.kubernetes.io/not-ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;노드가 아직 Ready 상태가 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node.kubernetes.io/unreachable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;노드가 네트워크에서 도달 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node.kubernetes.io/memory-pressure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;메모리 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node.kubernetes.io/disk-pressure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;디스크 공간 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node.kubernetes.io/pid-pressure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PID 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node.kubernetes.io/network-unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;네트워크 사용 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoExecute Effect의 Taint가 추가되었을 때, Pod에 &lt;code&gt;tolerationSeconds&lt;/code&gt;가 설정되어 있으면 즉시 퇴출되지 않고 해당 시간만큼 대기한 후 퇴출됩니다. 쿠버네티스는 &lt;code&gt;not-ready&lt;/code&gt;와 &lt;code&gt;unreachable&lt;/code&gt; Taint에 대해 기본적으로 &lt;code&gt;tolerationSeconds: 300&lt;/code&gt;(5분)을 자동으로 추가합니다. 즉, 노드에 장애가 발생하면 5분간 기다린 뒤 Pod가 다른 노드로 이동하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cordon, Drain, PodDisruptionBudget&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cordon&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;kubectl cordon &amp;lt;노드이름&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 노드에 새로운 Pod가 스케줄링되지 않도록 막습니다. 내부적으로는 &lt;code&gt;node.kubernetes.io/unschedulable:NoSchedule&lt;/code&gt; Taint가 추가됩니다. 이미 실행 중인 Pod에는 영향이 없습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;kubectl uncordon &amp;lt;노드이름&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cordon을 해제하여 다시 스케줄링 가능하게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Drain&lt;/h3&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;kubectl drain &amp;lt;노드이름&amp;gt; --ignore-daemonsets --delete-emptydir-data
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 노드에서 실행 중인 Pod를 안전하게 퇴출(Eviction)합니다. 퇴출된 Pod는 Deployment, ReplicaSet 등에 의해 관리되고 있다면 다른 노드에서 자동으로 재생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DaemonSet에 의해 생성된 Pod는 drain 대상이 아닙니다. &lt;code&gt;--ignore-daemonsets&lt;/code&gt; 옵션이 필요합니다.&lt;/li&gt;
&lt;li&gt;Deployment, ReplicaSet, Job, StatefulSet에 의해 생성되지 않은 &lt;b&gt;단독 Pod&lt;/b&gt;는 drain이 실패합니다. 이런 Pod는 다른 노드에서 재생성될 수 없기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PodDisruptionBudget (PDB)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;drain 등으로 Pod Eviction이 발생할 때, 최소한의 가용성을 보장하기 위한 오브젝트입니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  maxUnavailable: 1          # 동시에 최대 1개까지만 종료 가능
  selector:
    matchLabels:
      app: web
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 &lt;code&gt;minAvailable&lt;/code&gt;을 사용할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spec:
  minAvailable: 2             # 최소 2개의 Pod가 항상 정상 상태 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kubectl get pdb&lt;/code&gt;로 현재 설정된 PDB 목록을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커스텀 스케줄러&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 스케줄러(default-scheduler) 외에 직접 스케줄러를 구현할 수 있습니다. 동작 방식은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커스텀 스케줄러가 kube-apiserver의 watch API를 통해 새로 생성된 Pod 정보를 받아온다.&lt;/li&gt;
&lt;li&gt;Pod의 &lt;code&gt;nodeName&lt;/code&gt;이 비어 있고, &lt;code&gt;schedulerName&lt;/code&gt;이 자신의 이름과 일치하면 스케줄링을 시작한다.&lt;/li&gt;
&lt;li&gt;노드 필터링 &amp;rarr; 스코어링 알고리즘 수행 후, Binding API 요청을 통해 Pod의 &lt;code&gt;nodeName&lt;/code&gt;을 설정한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 스케줄러는 &lt;code&gt;schedulerName&lt;/code&gt;이 &lt;code&gt;default-scheduler&lt;/code&gt;인 Pod만 처리합니다. Pod YAML에서 &lt;code&gt;schedulerName&lt;/code&gt;을 지정하면 해당 커스텀 스케줄러가 담당하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spec:
  schedulerName: my-custom-scheduler
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모니터링&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;metrics-server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;metrics-server는 클러스터의 리소스 사용량 데이터를 수집하는 컴포넌트입니다. &lt;code&gt;kubectl top&lt;/code&gt; 명령, HPA(Horizontal Pod Autoscaler) 등 다양한 기능의 기반이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 수집 흐름&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 노드의 kubelet은 &lt;b&gt;cAdvisor&lt;/b&gt;를 내장하고 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;cAdvisor는 컨테이너의 CPU, 메모리 사용량 등을 수집하고 &lt;code&gt;/stats/summary&lt;/code&gt; 엔드포인트로 노출합니다.&lt;/li&gt;
&lt;li&gt;metrics-server는 이 엔드포인트에 주기적으로 요청을 보내서 데이터를 수집합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;kubelet (cAdvisor) &amp;rarr; metrics-server 수집 &amp;rarr; kubectl top 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;확장된 API Server로 등록&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;metrics-server를 배포하면 &lt;code&gt;apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io&lt;/code&gt;가 생성됩니다. 이는 metrics-server가 쿠버네티스의 &lt;b&gt;확장 API Server(Aggregated API Server)&lt;/b&gt; 로 등록되었다는 의미입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 &lt;code&gt;kubectl top&lt;/code&gt;으로 요청을 보내면&lt;/li&gt;
&lt;li&gt;kube-apiserver가 해당 요청을 metrics-server로 프록시하고&lt;/li&gt;
&lt;li&gt;metrics-server가 처리한 결과를 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;설치 시 참고&lt;br /&gt;설치 시 Deployment의 args에 --kubelet-insecure-tls 를 추가하면 kubelet의 인증서 신뢰 여부와 무관하게 통신할 수 있습니다. &lt;br /&gt;테스트 환경에서 유용합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kubelet 메트릭 직접 조회하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet에 직접 요청을 보내서 메트릭을 가져올 수도 있습니다. 이 경우 적절한 권한이 필요합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ClusterRole로 kubelet 메트릭 조회 권한을 정의 후&lt;/li&gt;
&lt;li&gt;ServiceAccount를 생성하고, ClusterRoleBinding으로 연결.&lt;/li&gt;
&lt;li&gt;해당 ServiceAccount의 토큰을 curl 요청의 Authorization 헤더에 포함시켜 요청.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;/metrics 엔드포인트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 여러 컴포넌트는 &lt;code&gt;/metrics&lt;/code&gt; 경로를 통해 Prometheus 형식의 메트릭을 노출합니다. 이렇게 메트릭을 노출하는 것을 &lt;b&gt;Exporter&lt;/b&gt; 패턴이라고 합니다. kubelet, kube-apiserver 등이 대표적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;node-exporter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node-exporter는 호스트(노드) 수준의 메트릭을 제공합니다. 파일 시스템 사용량, 네트워크 패킷 수, CPU 사용률 등 OS 레벨의 다양한 정보를 수집합니다. kube-prometheus 같은 모니터링 스택에 포함되어 있으며, DaemonSet으로 모든 노드에 배포됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;table style=&quot;height: 283px;&quot; width=&quot;752&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;스케줄링 흐름&lt;/td&gt;
&lt;td&gt;apply &amp;rarr; etcd 저장 &amp;rarr; scheduler 노드 선택 &amp;rarr; kubelet Pod 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nodeSelector / Node Affinity&lt;/td&gt;
&lt;td&gt;Pod가 원하는 노드를 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod Affinity / Anti-Affinity&lt;/td&gt;
&lt;td&gt;다른 Pod와의 관계로 노드를 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taint / Toleration&lt;/td&gt;
&lt;td&gt;노드가 Pod를 거부 / Pod가 이를 무시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cordon / Drain&lt;/td&gt;
&lt;td&gt;노드 유지보수를 위한 스케줄링 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDB&lt;/td&gt;
&lt;td&gt;Eviction 시 최소 가용성 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metrics-server&lt;/td&gt;
&lt;td&gt;kubelet의 cAdvisor 데이터를 수집, kubectl top 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>개발지식/Ops</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/108</guid>
      <comments>https://halfmoonbearlog.tistory.com/108#entry108comment</comments>
      <pubDate>Wed, 15 Apr 2026 20:04:32 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 깊이 이해하기 (4) &amp;mdash; ServiceAccount, RBAC, 자원 관리, 그리고 Admission Controller</title>
      <link>https://halfmoonbearlog.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 보안 모델과 자원 관리를 이해하려면, kubectl 명령이 실행될 때 내부에서 어떤 일이 일어나는지를 먼저 파악해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API 요청의 전체 흐름&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;kubectl 명령 실행
  &amp;rarr; kube-apiserver의 HTTP 핸들러가 수신
    &amp;rarr; Authentication (이 사용자가 쿠버네티스 사용자가 맞는지 인증)
      &amp;rarr; Authorization (이 사용자가 해당 작업을 할 권한이 있는지 인가)
        &amp;rarr; Admission Controller (추가적인 검증과 변형)
          &amp;rarr; etcd에 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication과 Authorization은 ServiceAccount, X.509 인증서 등으로 처리됩니다. Admission Controller는 이 글의 뒤에서 자세히 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ServiceAccount &amp;mdash; Pod의 신원 증명&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kubeconfig의 구조&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl은 &lt;code&gt;~/.kube/config&lt;/code&gt; 파일을 읽어서 어떤 클러스터에 어떤 자격증명으로 접근할지를 결정합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Config
clusters:                    # 클러스터 접속 정보
- cluster:
    certificate-authority-data: &amp;lt;base64 인코딩된 CA 인증서&amp;gt;
    server: https://192.168.1.10:6443
  name: my-cluster
users:                       # 인증 정보
- name: admin
  user:
    client-certificate-data: &amp;lt;base64 인코딩된 클라이언트 인증서&amp;gt;
    client-key-data: &amp;lt;base64 인코딩된 클라이언트 키&amp;gt;
contexts:                    # cluster + user 조합
- context:
    cluster: my-cluster
    user: admin
    namespace: default
  name: my-context
current-context: my-context
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;users&lt;/code&gt; 항목에는 클러스터의 루트 CA에서 발급한 클라이언트 인증서가 들어갑니다. kubeadm으로 클러스터를 구성하면 &lt;code&gt;/etc/kubernetes/pki/&lt;/code&gt; 디렉터리에 인증서 체인이 생성됩니다.&lt;/p&gt;
&lt;table style=&quot;height: 92px;&quot; width=&quot;818&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;파일&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;code&gt;ca.crt&lt;/code&gt; / &lt;code&gt;ca.key&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;X.509 형식의 루트 CA 인증서와 개인키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;code&gt;apiserver.crt&lt;/code&gt; / &lt;code&gt;apiserver.key&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;API 서버용 하위 인증서 (서버 인증에 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubeadm의 경우 kube-apiserver는 &lt;code&gt;&amp;lt;마스터 노드 IP&amp;gt;:6443&lt;/code&gt;에서 리스닝하며, HTTPS만 처리합니다. 이때 사용되는 것이 self-signed 인증서(자체 CA에서 발급한 인증서)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ServiceAccount의 동작&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServiceAccount(SA)는 Pod가 kube-apiserver와 통신할 때 사용하는 신원 증명입니다. 사람이 아닌 프로세스(Pod)를 위한 계정이라고 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 1.24 이전에는 SA를 생성하면 해당 SA용 Secret(JWT 토큰 포함)이 자동으로 생성되었습니다. 1.24부터는 자동 생성되지 않고, TokenRequest API를 통해 시간 제한이 있는 토큰을 발급받는 방식으로 변경되었습니다. 레거시 방식이 필요하면 수동으로 Secret을 생성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 토큰은 kube-apiserver가 발급합니다. 구체적으로는 kube-apiserver가 &lt;code&gt;--service-account-signing-key-file&lt;/code&gt;에 지정된 개인키로 JWT 토큰에 서명하고, kubelet이 Projected Volume을 통해 이 토큰을 Pod 내부에 마운트합니다. 토큰에는 만료 시간이 설정되어 있어서(kubeadm 기본값은 1시간), 만료되면 kubelet이 자동으로 갱신해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pod에서 API 서버 접근&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod는 생성 시 자동으로 SA의 토큰을 마운트합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;spec:
  serviceAccountName: my-sa    # 사용할 SA 지정. 미지정 시 default SA 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod 내부에서 토큰은 &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/&lt;/code&gt; 경로에 마운트됩니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/
  ├── token        # JWT 토큰
  ├── ca.crt       # 클러스터 CA 인증서
  └── namespace    # 현재 네임스페이스
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod에서 쿠버네티스 SDK를 사용할 때 (python 코드로 쿠버네티스에 요청을 보낼 때)&amp;nbsp;&lt;code&gt;config.load_incluster_config()&lt;/code&gt;를 호출하면, 이 경로에서 토큰과 CA 인증서를 자동으로 읽어 인증을 처리합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from kubernetes import client, config

config.load_incluster_config()  # 마운트된 SA 토큰/CA 인증서를 자동으로 읽음
v1 = client.CoreV1Api()
services = v1.list_namespaced_service(namespace='default')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 API 호출은 클러스터 내부의 kubernetes Service (&lt;code&gt;kubernetes.default.svc.cluster.local&lt;/code&gt;, 기본적으로 ClusterIP &lt;code&gt;10.96.0.1&lt;/code&gt;)로 전달되고 이 Service가 kube-apiserver Pod로 라우팅합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kubeconfig에 SA 등록&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SA 토큰으로 kubectl을 사용하고 싶다면 아래처럼 설정하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# SA의 토큰을 credentials로 등록
kubectl config set-credentials my-sa --token=&amp;lt;JWT 토큰&amp;gt;

# 새로운 context 생성 (클러스터 + SA 조합)
kubectl config set-context my-sa-context \
  --cluster=my-cluster \
  --user=my-sa

# context 전환
kubectl config use-context my-sa-context
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;use-context&lt;/code&gt;로 전환하면 이후의 모든 kubectl 명령이 해당 SA의 권한으로 실행됩니다. &lt;code&gt;kubectl config get-contexts&lt;/code&gt;로 현재 사용 가능한 컨텍스트 목록을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RBAC &amp;mdash; Role 기반 접근 제어&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Role과 ClusterRole&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Role&lt;/b&gt;은 특정 네임스페이스 내의 오브젝트에 대한 권한을 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default          # 이 Role이 적용되는 네임스페이스
  name: pod-reader
rules:
- apiGroups: [&quot;&quot;]             # 코어 API 그룹 (Pod, Service 등)
  resources: [&quot;pods&quot;]         # 대상 리소스
  verbs: [&quot;get&quot;, &quot;list&quot;]     # 허용되는 동작
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;apiGroups&lt;/code&gt;에 대해 보충하면, 쿠버네티스의 모든 리소스는 API 그룹에 속합니다. &lt;code&gt;kubectl api-resources&lt;/code&gt; 명령으로 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;kubectl api-resources

# NAME          SHORTNAMES   APIVERSION                NAMESPACED   KIND
# pods          po           v1                        true         Pod
# services      svc          v1                        true         Service
# deployments   deploy       apps/v1                   true         Deployment
# ingresses     ing          networking.k8s.io/v1      true         Ingress
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;APIVERSION 열에서 슬래시(&lt;code&gt;/&lt;/code&gt;) 앞부분이 API 그룹입니다. &lt;code&gt;v1&lt;/code&gt;처럼 그룹 없이 버전만 있는 것은 코어 그룹(&lt;code&gt;&quot;&quot;&lt;/code&gt;)이고, &lt;code&gt;apps/v1&lt;/code&gt;이면 &lt;code&gt;apps&lt;/code&gt; 그룹, &lt;code&gt;networking.k8s.io/v1&lt;/code&gt;이면 &lt;code&gt;networking.k8s.io&lt;/code&gt; 그룹입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ClusterRole&lt;/b&gt;은 클러스터 전역 권한을 정의합니다. Node, PersistentVolume 같은 네임스페이스에 종속되지 않는 리소스에 대한 권한은 ClusterRole로만 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RoleBinding과 ClusterRoleBinding&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Role을 정의하는 것만으로는 아무 효과가 없습니다. Role을 SA(또는 User, Group)에 연결하는 Binding이 필요합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: default
subjects:
- kind: ServiceAccount
  name: my-sa
  namespace: default
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 SA는 여러 RoleBinding에 의해 권한을 부여받을 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ClusterRole Aggregation&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClusterRole Aggregation은 여러 ClusterRole의 규칙을 하나로 합치는 기능입니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 자식 ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-endpoints
  labels:
    rbac.authorization.k8s.io/aggregate-to-monitoring: &quot;true&quot;  # 이 라벨이 키
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;services&quot;, &quot;endpoints&quot;, &quot;pods&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
---
# 부모 ClusterRole (규칙을 직접 정의하지 않고 aggregation으로 수집)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring
aggregationRule:
  clusterRoleSelectors:
  - matchLabels:
      rbac.authorization.k8s.io/aggregate-to-monitoring: &quot;true&quot;
rules: []  # 이 필드는 자동으로 채워짐
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 ClusterRole은 &lt;code&gt;aggregationRule&lt;/code&gt;에 지정된 라벨을 가진 모든 자식 ClusterRole의 rules를 자동으로 합칩니다. 새로운 자식 ClusterRole을 추가하면 부모에 자동 반영됩니다. 쿠버네티스의 기본 ClusterRole인 &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;view&lt;/code&gt;가 이 패턴을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자원 관리 &amp;mdash; requests, limits, 그리고 QoS&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;requests와 limits의 차이&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;spec:
  containers:
  - name: app
    resources:
      requests:
        cpu: &quot;250m&quot;
        memory: &quot;256Mi&quot;
      limits:
        cpu: &quot;500m&quot;
        memory: &quot;512Mi&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;requests&lt;/td&gt;
&lt;td&gt;컨테이너에게 최소 보장되는 리소스 양. 스케줄러는 이 값을 기준으로 Pod를 노드에 배치합니다. 노드에 requests만큼의 여유 리소스가 없으면 Pod가 스케줄링되지 않습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;limits&lt;/td&gt;
&lt;td&gt;컨테이너가 사용할 수 있는 최대 리소스 양입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kubectl describe node &amp;lt;노드명&amp;gt;&lt;/code&gt;으로 노드의 리소스 할당 현황을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU와 메모리의 경합 처리 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 중요합니다. &lt;b&gt;CPU와 메모리는 리소스 초과 시 처리 방식이 근본적으로 다릅니다.&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 82px;&quot;&gt;리소스&lt;/th&gt;
&lt;th style=&quot;width: 199px;&quot;&gt;유형&lt;/th&gt;
&lt;th style=&quot;width: 573px;&quot;&gt;limits 초과 시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 82px;&quot;&gt;CPU&lt;/td&gt;
&lt;td style=&quot;width: 199px;&quot;&gt;Compressible Resource&lt;/td&gt;
&lt;td style=&quot;width: 573px;&quot;&gt;스로틀링됩니다. 컨테이너가 강제로 느려지지만 종료되지는 않습니다. CPU는 시분할이 가능한 리소스이기 때문입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 82px;&quot;&gt;메모리&lt;/td&gt;
&lt;td style=&quot;width: 199px;&quot;&gt;Incompressible Resource&lt;/td&gt;
&lt;td style=&quot;width: 573px;&quot;&gt;컨테이너가 OOM Killed됩니다. 메모리는 회수가 즉시 가능하지 않으므로 초과 사용을 허용할 수 없습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 초과로 강제 종료된 컨테이너는 restartPolicy에 따라 같은 노드에서 재시작되거나 상위 컨트롤러에 의해 새 Pod가 다른 노드에 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오버커밋(Overcommit)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오버커밋은 노드의 실제 리소스보다 많은 requests를 허용하는 것이 아닙니다. &lt;b&gt;limits의 합이 노드의 실제 리소스를 초과하도록 허용하는 개념&lt;/b&gt;입니다. requests의 합은 노드 capacity를 초과할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 8Gi 메모리 노드에 requests 256Mi / limits 1Gi인 Pod를 20개 스케줄링할 수 있습니다. requests 합(5Gi)은 8Gi 이내이지만, limits 합(20Gi)은 8Gi를 초과합니다. 모든 Pod가 동시에 최대 메모리를 사용하지는 않을 것이라는 가정 하에 효율적으로 자원을 활용하는 전략입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node Conditions&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;kubectl describe node &amp;lt;노드명&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 상태에서 지속적으로 업데이트되는 Condition들이 있습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MemoryPressure&lt;/td&gt;
&lt;td&gt;True면 노드의 가용 메모리가 eviction threshold(기본 100Mi) 이하로 떨어진 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DiskPressure&lt;/td&gt;
&lt;td&gt;True면 디스크 공간이 부족한 상태. kubelet이 사용하지 않는 컨테이너 이미지를 삭제하기도 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점이 있습니다. kubelet이 MemoryPressure를 감지하기 전에 리눅스 커널의 OOM Killer가 먼저 동작할 수 있습니다. OOM Killer는 &lt;code&gt;oom_score&lt;/code&gt;가 높은 프로세스부터 종료시킵니다. kubelet의 &lt;code&gt;oom_score_adj&lt;/code&gt;는 &lt;code&gt;-999&lt;/code&gt;로 설정되어 있어서 거의 마지막에 종료됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;QoS 클래스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 Pod의 requests/limits 설정에 따라 자동으로 QoS 클래스를 부여합니다. 이 클래스는 리소스 경합 시 어떤 Pod를 먼저 퇴거(evict)할지 결정하는 기준이 됩니다.&lt;/p&gt;
&lt;table style=&quot;width: 889px; height: 245px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 201px;&quot;&gt;QoS 클래스&lt;/th&gt;
&lt;th style=&quot;width: 474px;&quot;&gt;조건&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 201px;&quot;&gt;Guaranteed (최고 우선순위)&lt;/td&gt;
&lt;td style=&quot;width: 474px;&quot;&gt;모든 컨테이너에 CPU/메모리 requests와 limits가 설정되어 있고, requests == limits일 때. limits만 설정해도 requests가 동일한 값으로 자동 설정됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 201px;&quot;&gt;Burstable (중간 우선순위)&lt;/td&gt;
&lt;td style=&quot;width: 474px;&quot;&gt;최소 하나의 컨테이너에 requests 또는 limits가 설정되어 있지만, Guaranteed 조건을 만족하지 않는 경우. 순간적으로 requests를 넘어서 limits까지 사용할 수 있습니다(burst).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 201px;&quot;&gt;BestEffort (최저 우선순위)&lt;/td&gt;
&lt;td style=&quot;width: 474px;&quot;&gt;requests와 limits를 모두 설정하지 않은 경우. limits가 없으므로 유휴 리소스가 있다면 제한 없이 사용할 수 있지만, requests도 없으므로 아무것도 보장되지 않습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴거 우선순위: kubelet이 리소스 압박을 감지하면 &lt;b&gt;BestEffort &amp;rarr; Burstable &amp;rarr; Guaranteed&lt;/b&gt; 순서로 Pod를 evict합니다. 같은 QoS 클래스 내에서는 메모리를 많이 사용하는 Pod가 먼저 대상이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kubelet eviction과 OOM Killer의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘은 동작 방식이 다릅니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;kubelet eviction&lt;/th&gt;
&lt;th&gt;OOM Killer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;주체&lt;/td&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;td&gt;리눅스 커널&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대상&lt;/td&gt;
&lt;td&gt;Pod 전체&lt;/td&gt;
&lt;td&gt;컨테이너 프로세스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동작&lt;/td&gt;
&lt;td&gt;Pod의 phase를 Failed로 설정하고 종료. 상위 컨트롤러(Deployment 등)가 새 Pod를 생성하면 스케줄러가 적절한 노드에 배치합니다.&lt;/td&gt;
&lt;td&gt;컨테이너 프로세스를 직접 종료. kubelet이 restartPolicy에 따라 같은 노드에서 재시작합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ResourceQuota &amp;mdash; 네임스페이스 단위의 총량 제한&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceQuota는 특정 네임스페이스에서 사용할 수 있는 리소스 총량의 합을 제한하는 네임스페이스 종속 오브젝트입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: v1
kind: ResourceQuota
metadata:
  name: my-quota
  namespace: dev
spec:
  hard:
    requests.cpu: &quot;10&quot;
    requests.memory: &quot;20Gi&quot;
    limits.cpu: &quot;20&quot;
    limits.memory: &quot;40Gi&quot;
    persistentvolumeclaims: &quot;10&quot;
    count/pods: &quot;20&quot;              # 생성 가능한 Pod 수 제한
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한 가능한 항목은 CPU, 메모리, PVC 크기, 임시 스토리지(ephemeral-storage), 네임스페이스 내 생성 가능한 리소스 개수(Pod, Service, ConfigMap 등)입니다.&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;kubectl get quota -n dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;scopes를 통한 세분화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceQuota는 scopes를 사용해 특정 조건의 Pod에만 적용할 수 있습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BestEffort&lt;/td&gt;
&lt;td&gt;QoS가 BestEffort인 Pod에만 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NotBestEffort&lt;/td&gt;
&lt;td&gt;QoS가 BestEffort가 아닌 Pod에만 적용 (Guaranteed, Burstable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminating&lt;/td&gt;
&lt;td&gt;activeDeadlineSeconds가 설정된 Pod에 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NotTerminating&lt;/td&gt;
&lt;td&gt;activeDeadlineSeconds가 설정되지 않은 Pod에 적용 (대부분의 일반 Pod)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트러블슈팅 팁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceQuota가 설정된 네임스페이스에서 Pod가 생성되지 않을 때, Deployment의 로그만 보면 원인을 못 찾을 수 있습니다. &lt;b&gt;Pod의 생성 주체는 Deployment가 아닌 ReplicaSet이므로 &lt;/b&gt;ReplicaSet의 이벤트를 확인해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;kubectl describe rs &amp;lt;replicaset-name&amp;gt; -n dev
# Events 섹션에서 &quot;exceeded quota&quot; 같은 메시지를 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LimitRange &amp;mdash; 개별 리소스의 범위 제한&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceQuota가 네임스페이스 전체의 총량을 제한한다면 LimitRange는 개별 컨테이너/Pod/PVC의 리소스 범위를 제한합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;apiVersion: v1
kind: LimitRange
metadata:
  name: my-limits
  namespace: dev
spec:
  limits:
  - type: Container
    default:              # 사용자가 limits를 지정하지 않으면 자동 설정되는 값
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;
    defaultRequest:       # 사용자가 requests를 지정하지 않으면 자동 설정되는 값
      cpu: &quot;100m&quot;
      memory: &quot;128Mi&quot;
    max:                  # 허용되는 최대값
      cpu: &quot;2&quot;
      memory: &quot;2Gi&quot;
    min:                  # 허용되는 최소값
      cpu: &quot;50m&quot;
      memory: &quot;64Mi&quot;
    maxLimitRequestRatio: # limits / requests의 최대 비율
      cpu: &quot;4&quot;
      memory: &quot;4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;type&lt;/code&gt;에는 Container, Pod, PersistentVolumeClaim을 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;kubectl get limits -n dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;maxLimitRequestRatio&lt;/code&gt;에 대해 보충하면, 이 값이 1이면 limits와 requests가 반드시 같아야 하므로 QoS Guaranteed만 가능하게 됩니다. 이 필드를 설정하면 requests도 limits도 없는 BestEffort Pod의 생성이 거절됩니다(비율을 계산할 수 없으므로).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Admission Controller &amp;mdash; API 요청의 마지막 관문&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Admission Controller는 Authentication, Authorization을 통과한 API 요청에 대해 추가적인 &lt;b&gt;검증(Validating)&lt;/b&gt; 과 &lt;b&gt;변형(Mutating)&lt;/b&gt; 을 수행하는 플러그인입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 것들과 연결하면 이렇습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 190px;&quot;&gt;동작&lt;/th&gt;
&lt;th style=&quot;width: 93px;&quot;&gt;유형&lt;/th&gt;
&lt;th style=&quot;width: 570px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 190px;&quot;&gt;LimitRange의 default 값 적용&lt;/td&gt;
&lt;td style=&quot;width: 93px;&quot;&gt;Mutating&lt;/td&gt;
&lt;td style=&quot;width: 570px;&quot;&gt;Pod에 requests/limits가 없으면 LimitRange에 설정된 default 값으로 API 요청 데이터를 변형합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 190px;&quot;&gt;ResourceQuota 검증&lt;/td&gt;
&lt;td style=&quot;width: 93px;&quot;&gt;Validating&lt;/td&gt;
&lt;td style=&quot;width: 570px;&quot;&gt;새 Pod 생성 시 할당량을 초과하는지 검증합니다. 초과하면 요청을 거부합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Istio 사이드카 주입의 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio가 Pod에 Envoy 프록시 사이드카 컨테이너를 자동으로 주입하는 것도 Admission Controller를 통해 이루어집니다. 구체적으로는 MutatingAdmissionWebhook을 사용합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 Pod 생성 요청을 보냅니다.&lt;/li&gt;
&lt;li&gt;API 서버가 Authentication &amp;rarr; Authorization을 통과시킵니다.&lt;/li&gt;
&lt;li&gt;MutatingAdmissionWebhook이 Istio의 istiod(Pilot)에게 Pod 스펙을 전달합니다.&lt;/li&gt;
&lt;li&gt;istiod가 Envoy 사이드카 컨테이너 정의를 추가한 수정된 Pod 스펙을 반환합니다.&lt;/li&gt;
&lt;li&gt;API 서버가 수정된 스펙으로 Pod를 생성합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 Istio 사이드카 주입이 활성화된 네임스페이스에서 Pod를 생성하면, 매니페스트에 정의하지 않은 &lt;code&gt;istio-proxy&lt;/code&gt; 컨테이너가 자동으로 추가되는 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4편에 걸쳐 쿠버네티스의 기본 개념부터 RBAC, 자원 관리, Admission Controller까지 정리했습니다. 각 개념이 독립적으로 존재하는 것이 아니라 서로 맞물려 동작한다는 것을 느꼈을 것입니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;계층&lt;/th&gt;
&lt;th&gt;흐름&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;워크로드&lt;/td&gt;
&lt;td&gt;Deployment &amp;rarr; ReplicaSet &amp;rarr; Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네트워크&lt;/td&gt;
&lt;td&gt;Service &amp;rarr; Endpoints &amp;rarr; Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스토리지&lt;/td&gt;
&lt;td&gt;PVC &amp;rarr; PV &amp;rarr; StorageClass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안&lt;/td&gt;
&lt;td&gt;SA &amp;rarr; Role/ClusterRole &amp;rarr; RoleBinding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 처리 파이프라인&lt;/td&gt;
&lt;td&gt;kubectl &amp;rarr; Authentication &amp;rarr; Authorization &amp;rarr; Admission Controller &amp;rarr; etcd&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조들을 머릿속에 그릴 수 있다면, 새로운 개념이 등장해도 어디에 위치하는지 빠르게 파악할 수 있습니다.&lt;/p&gt;</description>
      <category>개발지식/Ops</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/107</guid>
      <comments>https://halfmoonbearlog.tistory.com/107#entry107comment</comments>
      <pubDate>Sun, 12 Apr 2026 12:14:17 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 깊이 이해하기 (3) &amp;mdash; 스토리지: PV, PVC, 그리고 Reclaim Policy</title>
      <link>https://halfmoonbearlog.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 스토리지 모델을 정리한 내용이며, PV/PVC의 추상화 구조, 바인딩 원리, Reclaim Policy, StorageClass를 통한 다이나믹 프로비저닝, 삭제 보호 메커니즘을 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PV와 PVC &amp;mdash; 스토리지 추상화의 두 축&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스의 스토리지 모델은 &lt;b&gt;프로비저닝&lt;/b&gt;과 &lt;b&gt;사용&lt;/b&gt;을 분리하는 구조입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PersistentVolume (PV)&lt;/b&gt;: 클러스터 관리자가 미리 프로비저닝해둔 스토리지 리소스. 실제 디스크, NFS 마운트, 클라우드 디스크 등의 &lt;b&gt;구체적인 스토리지&lt;/b&gt;를 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PersistentVolumeClaim (PVC)&lt;/b&gt;: 사용자(개발자)가 &quot;이 정도 크기의 스토리지가 필요하다&quot;고 &lt;b&gt;요청하는 오브젝트&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PV는 네임스페이스에 종속되지 않는 클러스터 레벨 리소스이고, PVC는 네임스페이스에 종속됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 이렇게 많은 추상화 계층이 필요한가&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스가 &lt;code&gt;실제 디스크 &amp;rarr; PV &amp;rarr; PVC &amp;rarr; Pod&lt;/code&gt; 이렇게 여러 단계의 연결점을 만들어놓은 데는 이유가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;관심사의 분리&lt;/b&gt;입니다. 실제 디스크(NFS, EBS, Ceph 등)는 인프라 영역이고, Pod에서 스토리지를 사용하는 건 애플리케이션 영역입니다. 이 둘을 직접 연결하면 개발자가 스토리지의 구체적인 구현(IP 주소, 디스크 ID, 마운트 경로 등)을 알아야 하고, 스토리지 백엔드가 바뀌면 모든 Pod 매니페스트를 수정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PV는 &lt;b&gt;&quot;실제 스토리지를 쿠버네티스 오브젝트로 등록&quot;&lt;/b&gt;하는 역할입니다. NFS 서버의 경로든, AWS EBS 볼륨이든, 로컬 디스크든 어떤 종류의 스토리지라도 PV라는 동일한 인터페이스로 추상화합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PVC는 &lt;b&gt;&quot;개발자가 필요한 스토리지를 요청&quot;&lt;/b&gt;하는 역할입니다. 개발자는 &quot;10Gi짜리 ReadWriteOnce 스토리지가 필요하다&quot;고만 쓰면 되고, 그게 NFS인지 EBS인지 신경 쓸 필요가 없습니다. &lt;b&gt;쿠버네티스가 조건에 맞는 PV를 찾아서 바인딩해줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이렇습니다:&lt;/p&gt;
&lt;table style=&quot;height: 176px;&quot; width=&quot;821&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;실제 디스크/외부 스토리지&lt;/td&gt;
&lt;td&gt;물리적 또는 클라우드 스토리지 리소스 그 자체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PV&lt;/td&gt;
&lt;td&gt;실제 스토리지를 쿠버네티스가 인식할 수 있도록 등록한 오브젝트. 스토리지의 공급.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PVC&lt;/td&gt;
&lt;td&gt;개발자가 필요한 스토리지 스펙을 선언한 오브젝트. 스토리지의 소비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StorageClass&lt;/td&gt;
&lt;td&gt;PV를 수동으로 만들지 않고, PVC 생성 시 자동으로 PV를 프로비저닝하게 해주는 템플릿&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 스토리지 백엔드를 NFS에서 Ceph로 바꾸더라도 PV/StorageClass만 수정하면 되고, Pod나 PVC는 건드릴 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;capacity.storage에 대한 오해&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PV 매니페스트에서 &lt;code&gt;capacity.storage: 10Gi&lt;/code&gt;를 지정한다고 해서 새로운 디스크 파티션이 생성되는 것은 아닙니다. 이 값은 쿠버네티스가 PV-PVC 바인딩 시 매칭 기준으로 사용하는 &lt;b&gt;메타데이터&lt;/b&gt;일 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 NFS 기반 PV라면, 실제 NFS 서버에 10Gi 제한이 걸리지 않습니다. 스토리지의 실제 용량 제한은 백엔드 스토리지 시스템에서 처리해야 합니다. 다만 CSI 드라이버 기반의 클라우드 스토리지(AWS EBS, GCP PD 등)의 경우 실제로 해당 크기의 디스크가 생성되기도 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PV-PVC 바인딩의 원리&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PV와 PVC가 바인딩되려면 다음 조건들이 충족되어야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;storageClassName&lt;/code&gt;이 일치해야 합니다. PV의 &lt;code&gt;spec.storageClassName&lt;/code&gt;과 PVC의 &lt;code&gt;spec.storageClassName&lt;/code&gt;이 같아야 바인딩 후보가 됩니다.&lt;/li&gt;
&lt;li&gt;PVC가 요청한 크기 &lt;b&gt;이상&lt;/b&gt;의 capacity를 가진 PV여야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;accessModes&lt;/code&gt;가 호환되어야 합니다 (ReadWriteOnce, ReadOnlyMany, ReadWriteMany).&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;storageClassName&lt;/code&gt;이 같은 PV가 여러 개 있다면, 쿠버네티스는 조건을 만족하는 PV 중 &lt;b&gt;요청 크기 이상이면서 가장 작은 용량의 PV&lt;/b&gt;를 선택합니다. 예를 들어 5Gi를 요청했는데 10Gi, 20Gi, 50Gi PV가 있다면 10Gi PV가 바인딩됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PV의 생애주기&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;kubectl get pv
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PV는 다음과 같은 상태를 거칩니다.&lt;/p&gt;
&lt;table style=&quot;height: 172px;&quot; width=&quot;769&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Available&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PVC에 바인딩되지 않은 상태. 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Bound&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;특정 PVC에 바인딩된 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Released&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;바인딩되었던 PVC가 삭제된 상태. 데이터는 남아있지만 재사용 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Failed&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자동 회수(Reclaim)에 실패한 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Released 상태의 PV가 재사용 불가능한 이유는 PV에 이전 데이터가 남아있기 때문입니다. 보안과 데이터 무결성을 위해 쿠버네티스는 Released PV를 새로운 PVC에 자동으로 바인딩하지 않습니다. 재사용하고 싶다면 PV를 삭제하고 다시 생성하거나, &lt;code&gt;claimRef&lt;/code&gt;를 수동으로 제거하는 방법이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reclaim Policy &amp;mdash; PVC 삭제 후 PV의 운명&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reclaim Policy는 PVC가 삭제되었을 때 PV를 어떻게 처리할지를 결정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Retain&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PVC 삭제 &amp;rarr; PV 상태가 Released로 변경 &amp;rarr; 스토리지 데이터 보존 &amp;rarr; PV 재사용 불가&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 안전하게 보존하고 싶을 때 사용합니다. 관리자가 데이터를 수동으로 백업&amp;middot;확인한 후 PV를 삭제하고 필요시 재생성하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Delete&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PVC 삭제 &amp;rarr; PV 삭제 &amp;rarr; 외부 스토리지(EBS, PD 등)도 함께 삭제&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 다이나믹 프로비저닝의 기본값입니다. 주의하지 않으면 데이터를 영구적으로 잃을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Recycle (Deprecated)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PV의 데이터를 &lt;code&gt;rm -rf /volume/*&lt;/code&gt;으로 삭제하고 Available 상태로 되돌리는 정책이었습니다. 단순한 삭제만 수행해서 보안 문제가 있었고, 현재는 Dynamic Provisioning으로 대체되어 deprecated 상태입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다이나믹 프로비저닝과 StorageClass&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StorageClass는 &lt;b&gt;어떤 프로비저너를 써서, 어떤 파라미터로 스토리지를 만들지&lt;/b&gt;를 정의하는 오브젝트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 PV를 수동으로 만드는 것은 번거롭습니다. StorageClass를 사용하면 PVC 생성 시 자동으로 PV를 프로비저닝할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs    # 실제 스토리지를 생성해주는 프로비저너
parameters:
  type: gp3                           # 프로비저너에게 전달하는 스토리지 설정
reclaimPolicy: Delete                 # 이 StorageClass로 생성되는 PV의 기본 Reclaim Policy
volumeBindingMode: WaitForFirstConsumer
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;StorageClass 주요 필드&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 160px;&quot;&gt;필드&lt;/th&gt;
&lt;th style=&quot;width: 695px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 160px;&quot;&gt;&lt;code&gt;provisioner&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 695px;&quot;&gt;실제로 스토리지를 생성해주는 프로비저너. &lt;code&gt;kubernetes.io/aws-ebs&lt;/code&gt;는 AWS EBS 볼륨을 만들어주고, &lt;code&gt;kubernetes.io/gce-pd&lt;/code&gt;는 GCP 디스크를 만듭니다. 온프레미스에서는 Longhorn, Ceph CSI, NFS Subdir External Provisioner 같은 서드파티 프로비저너를 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 160px;&quot;&gt;&lt;code&gt;parameters&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 695px;&quot;&gt;프로비저너에게 전달하는 스토리지 설정. 위 예시에서 &lt;code&gt;type: gp3&lt;/code&gt;는 AWS EBS의 gp3 볼륨 타입을 쓰겠다는 뜻입니다. 프로비저너마다 지원하는 파라미터가 다릅니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 160px;&quot;&gt;&lt;code&gt;reclaimPolicy&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 695px;&quot;&gt;이 StorageClass로 다이나믹 프로비저닝된 PV의 기본 Reclaim Policy. &lt;code&gt;Delete&lt;/code&gt; 또는 &lt;code&gt;Retain&lt;/code&gt;을 지정할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 160px;&quot;&gt;&lt;code&gt;volumeBindingMode&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 695px;&quot;&gt;PV를 언제 생성하고 바인딩할지 결정합니다. 아래에서 자세히 설명합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;volumeBindingMode&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Immediate&lt;/b&gt;: PVC 생성 즉시 PV를 프로비저닝하고 바인딩합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WaitForFirstConsumer&lt;/b&gt;: PVC를 사용하는 Pod가 스케줄링될 때까지 PV 생성을 미룹니다. 이렇게 하면 Pod가 배치되는 노드의 가용 영역(AZ)에 맞춰서 PV를 생성할 수 있어서 다른 노드에 디스크가 생성돼서 마운트를 못하는 문제를 예방할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다이나믹 프로비저닝으로 생성된 PV는 StorageClass의 &lt;code&gt;reclaimPolicy&lt;/code&gt;를 상속합니다. StorageClass에서 &lt;code&gt;Delete&lt;/code&gt;로 설정했다면, 자동 생성된 모든 PV의 Reclaim Policy도 &lt;code&gt;Delete&lt;/code&gt;가 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 보호 메커니즘&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 스토리지의 안전을 위해 두 가지 보호 메커니즘을 갖고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PV 보호&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PVC와 바인딩된 상태(Bound)에서 PV를 삭제해도 실제 삭제는 즉시 이루어지지 않습니다. &lt;code&gt;kubectl delete pv&lt;/code&gt;를 실행하면 PV에 &lt;code&gt;deletionTimestamp&lt;/code&gt;가 찍히고 Terminating 상태가 되지만, PVC가 삭제될 때까지 대기합니다. &lt;code&gt;kubernetes.io/pv-protection&lt;/code&gt; finalizer에 의해 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PVC 보호&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 PVC를 사용하고 있는 상태에서 PVC를 삭제해도 실제 삭제는 즉시 이루어지지 않습니다. Pod가 삭제되어 PVC를 더 이상 사용하지 않을 때 비로소 PVC가 삭제됩니다. &lt;code&gt;kubernetes.io/pvc-protection&lt;/code&gt; finalizer에 의해 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 안전한 삭제 순서는 이렇습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Pod 삭제 &amp;rarr; PVC 삭제 &amp;rarr; PV 삭제(또는 Reclaim Policy에 따른 처리)&lt;/code&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 RBAC, ServiceAccount, 자원 관리(QoS, ResourceQuota, LimitRange), Admission Controller를 다루면서 쿠버네티스의 보안 모델과 리소스 관리 전략을 정리합니다.&lt;/p&gt;</description>
      <category>개발지식/Ops</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/106</guid>
      <comments>https://halfmoonbearlog.tistory.com/106#entry106comment</comments>
      <pubDate>Sun, 12 Apr 2026 12:13:38 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 깊이 이해하기 (2) &amp;mdash; Service와 Ingress: 네트워크의 모든 것</title>
      <link>https://halfmoonbearlog.tistory.com/105</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Service &amp;mdash; Pod를 네트워크에 연결하는 방법&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod는 생성&amp;middot;삭제될 때마다 IP가 바뀝니다. 이 불안정한 Pod에 안정적으로 접근하기 위한 추상화가 Service입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Endpoint 오브젝트의 역할&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service를 생성하면 쿠버네티스는 &lt;b&gt;Endpoints&lt;/b&gt;라는 별도의 오브젝트를 자동으로 생성합니다. 이것은 Service의 selector와 일치하는 Pod들의 &lt;code&gt;IP:Port&lt;/code&gt; 목록을 실시간으로 관리하는 오브젝트입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl get endpoints my-service

# 출력 예시:
# NAME         ENDPOINTS                                   AGE
# my-service   10.244.1.5:8080,10.244.2.3:8080             5m
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 생성되거나 삭제되면 Endpoints 오브젝트가 자동으로 업데이트됩니다. Service는 이 Endpoints를 참조해서 트래픽을 전달합니다. 따라서 Service와 Pod는 직접 연결되는 것이 아니라, &lt;b&gt;라벨 셀렉터 &amp;rarr; Endpoints &amp;rarr; Pod&lt;/b&gt; 라는 간접적인 경로로 연결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Kubernetes 1.21+에서는 Endpoints의 확장판인 &lt;b&gt;EndpointSlice&lt;/b&gt;가 기본으로 사용됩니다. Pod 수가 많아지면 하나의 Endpoints 오브젝트가 비대해지는 문제를 해결하기 위해 슬라이스 단위로 분할 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ClusterIP &amp;mdash; 클러스터 내부 통신&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClusterIP는 Service의 기본 타입입니다. &lt;b&gt;클러스터 내부에서만 접근 가능한 가상 IP&lt;/b&gt;를 부여합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  type: ClusterIP
  selector:
    app: my-app          # 이 라벨을 가진 Pod들로 트래픽 라우팅
  ports:
  - port: 80             # Service가 노출하는 포트
    targetPort: 8080     # Pod 내 컨테이너가 실제로 리스닝하는 포트
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;spec.selector.app&lt;/code&gt;이 어떤 라벨을 가진 Pod에 접근할지를 정하는 셀렉터입니다. 여기서 중요한 것은, ClusterIP Service는 Pod마다 1개씩 생성하는 것이 아니라 &lt;b&gt;동일한 라벨을 가진 Pod 그룹에 대해 1개의 Service를 생성&lt;/b&gt;하는 것입니다. 하나의 Service가 여러 Pod로 트래픽을 분배합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NodePort &amp;mdash; 클러스터 외부 노출&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodePort는 ClusterIP를 확장한 타입입니다. ClusterIP의 모든 기능을 포함하면서, 추가로 &lt;b&gt;모든 노드의 특정 포트를 개방&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30080      # 생략하면 30000-32767 범위에서 랜덤 할당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;외부에서 &amp;lt;노드IP&amp;gt;:30080으로 요청
&amp;rarr; kube-proxy가 이 요청을 수신
  &amp;rarr; kube-proxy가 해당 Service의 Endpoints 오브젝트를 참조하여 Pod IP를 확인
    &amp;rarr; iptables/IPVS 규칙에 의해 적절한 Pod로 직접 라우팅 (DNAT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-proxy는 Service가 생성될 때 iptables 또는 IPVS 규칙을 미리 설정해둡니다. NodePort로 들어온 요청은 이 규칙에 의해 &lt;b&gt;Endpoints에 등록된 Pod IP로 직접 DNAT&lt;/b&gt;(Destination NAT)됩니다. ClusterIP라는 가상 IP를 &quot;경유&quot;한다기보다는 kube-proxy가 설정한 네트워크 규칙이 Endpoints 기반으로 Pod에 직접 전달하는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 포인트&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개방된 포트(nodePort)는 &lt;b&gt;모든 노드에서 동일&lt;/b&gt;합니다. 예를 들어 30080이 할당되면, 노드1의 30080이든 노드2의 30080이든 어디로 접근해도 같은 Service에 도달합니다.&lt;/li&gt;
&lt;li&gt;각 노드의 IP는 당연히 다릅니다. 그래서 &lt;code&gt;192.168.1.10:30080&lt;/code&gt;, &lt;code&gt;192.168.1.11:30080&lt;/code&gt; 모두 유효한 접근 경로가 됩니다.&lt;/li&gt;
&lt;li&gt;NodePort 자체에 도메인을 직접 바인딩하는 것은 가능하지만 실무에서는 권장되지 않습니다. 노드가 추가/삭제되면 IP가 바뀌기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoadBalancer &amp;mdash; 클라우드 환경의 외부 노출&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoadBalancer는 NodePort를 한 번 더 확장한 타입입니다. 클라우드 프로바이더(AWS, GCP, Azure)의 L4 로드밸런서를 자동으로 프로비저닝합니다. 온프레미스에서는 MetalLB 같은 구현체가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; [도메인 &amp;rarr; DNS &amp;rarr; LoadBalancer IP]
  &amp;rarr; LoadBalancer가 노드들에 분배 (같은 nodePort, 다른 노드 IP)
    &amp;rarr; NodePort가 수신 &amp;rarr; kube-proxy가 iptables/IPVS 규칙으로 Pod에 직접 라우팅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인으로 접근하면 DNS가 로드밸런서의 외부 IP로 해석하고, 로드밸런서는 각 노드의 NodePort로 트래픽을 분배합니다. 이때 모든 노드가 동일한 nodePort를 쓰기 때문에 로드밸런서는 IP만 바꿔서 분배하면 됩니다. 각 노드에 도달한 요청은 kube-proxy의 iptables/IPVS 규칙에 의해 Endpoints 기반으로 적절한 Pod에 전달됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Ingress &amp;mdash; L7 라우팅의 핵심&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ingress와 Ingress Controller의 관계&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress를 이해하려면 두 가지를 분리해서 생각해야 합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Ingress 오브젝트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;이런 규칙으로 트래픽을 라우팅해달라&quot;는 선언입니다. 그 자체로는 아무 일도 하지 않습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Ingress Controller&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Ingress 오브젝트의 규칙을 실제로 실행하는 구현체입니다. Nginx Ingress Controller, Kong, Traefik, HAProxy 등이 있습니다. 실제로 Pod로 배포됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress Controller도 Pod이기 때문에, 외부에서 접근하려면 이 Pod를 노출하는 Service가 필요합니다. 보통 LoadBalancer 타입의 Service로 노출시킵니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;외부 &amp;rarr; LoadBalancer Service &amp;rarr; Ingress Controller Pod
  &amp;rarr; Ingress 규칙에 따라 &amp;rarr; 백엔드 Service &amp;rarr; 실제 애플리케이션 Pod
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ingress의 세 가지 핵심 기능&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 경로 기반 라우팅&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 도메인이지만 URL 경로에 따라 다른 서비스로 보냅니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /users
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 80
      - path: /orders
        pathType: Prefix
        backend:
          service:
            name: order-service
            port:
              number: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 호스트 기반 라우팅 (가상 호스팅)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 IP에 대해 다른 도메인 이름으로 다른 서비스에 라우팅합니다. 이것은 HTTP의 &lt;b&gt;Host 헤더&lt;/b&gt;를 기반으로 동작합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spec:
  rules:
  - host: home.co.kr
    http:
      paths:
      - path: /
        backend:
          service:
            name: home-service
            port:
              number: 80
  - host: shop.co.kr
    http:
      paths:
      - path: /
        backend:
          service:
            name: shop-service
            port:
              number: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. TLS 인증서 일괄 적용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서비스에 대한 HTTPS 종료(TLS Termination)를 Ingress 레벨에서 한 번에 처리합니다. 각 서비스마다 인증서를 설정할 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고) curl --resolve의 동작 원리&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;curl --resolve home.co.kr:80:192.100.1.1 http://home.co.kr/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 DNS 조회를 건너뛰고, &lt;code&gt;home.co.kr&lt;/code&gt;을 &lt;code&gt;192.100.1.1&lt;/code&gt;로 직접 해석하라는 의미입니다. 실제 TCP 연결은 &lt;code&gt;192.100.1.1&lt;/code&gt;로 가지만, HTTP 요청의 Host 헤더는 &lt;code&gt;home.co.kr&lt;/code&gt;로 설정됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 실제 전송되는 HTTP 요청:
GET / HTTP/1.1
Host: home.co.kr       &amp;larr; IP가 아닌 도메인이 들어감
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress Controller는 이 Host 헤더를 보고 라우팅을 결정합니다. &lt;code&gt;--resolve&lt;/code&gt;는 로컬 DNS만 오버라이드할 뿐, HTTP 프로토콜의 Host 헤더에는 영향을 주지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ingress의 트래픽 전달 방식&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress Controller의 구현체에 따라 두 가지 방식이 존재합니다.&lt;/p&gt;
&lt;table style=&quot;width: 822px; height: 105px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 183px;&quot;&gt;방식&lt;/th&gt;
&lt;th style=&quot;width: 486px;&quot;&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 183px;&quot;&gt;&lt;b&gt;Service를 통한 전달 (기본)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 486px;&quot;&gt;Ingress Controller &amp;rarr; 백엔드 Service의 ClusterIP &amp;rarr; Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 183px;&quot;&gt;&lt;b&gt;Pod 엔드포인트로 직접 전달&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 486px;&quot;&gt;Ingress Controller &amp;rarr; Endpoints에서 Pod IP 직접 조회 &amp;rarr; Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Service를 통한 전달&lt;/b&gt;: Service의 ClusterIP를 거쳐서 Pod에 도달하는 방식입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pod 엔드포인트를 통한 전달&lt;/b&gt;: &lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;Nginx Ingress Controller 같은 경우, 성능 최적화를 위해 Endpoints(혹은 EndpointSlice)에서 Pod IP를 직접 읽어와서 Service를 거치지 않고 Pod로 직접 트래픽을 보냅니다. 이 경우 Service는 Pod 디스커버리 용도로만 쓰이고, 실제 트래픽은 Service를 경유하지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식 모두 Ingress YAML에서의 선언은 동일합니다. 차이는 Ingress Controller의 내부 구현에 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 PV/PVC, StorageClass, Reclaim Policy를 다루면서 쿠버네티스의 스토리지 모델과 다이나믹 프로비저닝의 동작 원리를 정리합니다.&lt;/p&gt;</description>
      <category>개발지식/Ops</category>
      <author>반달bear</author>
      <guid isPermaLink="true">https://halfmoonbearlog.tistory.com/105</guid>
      <comments>https://halfmoonbearlog.tistory.com/105#entry105comment</comments>
      <pubDate>Sun, 12 Apr 2026 12:13:02 +0900</pubDate>
    </item>
  </channel>
</rss>