<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Subin's Blog</title>
    <link>https://zed6740.tistory.com/</link>
    <description>지금까지 공부한 것들을 정리하기 위한 블로그입니다. 
일상, 여행 글도 작성합니다.</description>
    <language>ko</language>
    <pubDate>Thu, 20 Aug 2026 09:22:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hsb_02</managingEditor>
    <image>
      <title>Subin's Blog</title>
      <url>https://tistory1.daumcdn.net/tistory/4763520/attach/5c0844a9669349b386c354b6b3c68f2b</url>
      <link>https://zed6740.tistory.com</link>
    </image>
    <item>
      <title>번외 &amp;mdash; 컨테이너 메모리와 런타임 메모리</title>
      <link>https://zed6740.tistory.com/225</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 막힌 자리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zed6740.tistory.com/224&quot;&gt;번외 &amp;mdash; 노드 메모리와 쿠버네티스 메모리&lt;/a&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; 커널이 센다. &lt;code&gt;/proc/meminfo&lt;/code&gt;의 &lt;code&gt;MemTotal&lt;/code&gt;&amp;middot;&lt;code&gt;MemAvailable&lt;/code&gt;, 지표로는 node-exporter의 &lt;code&gt;node_memory_*&lt;/code&gt;. 도로 가져갈 수 있는 것(페이지 캐시)과 없는 것(익명 메모리)으로 갈린다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cgroup 층&lt;/b&gt; &amp;mdash; 커널이 센다. cgroup 파일의 &lt;code&gt;memory.current&lt;/code&gt;&amp;middot;&lt;code&gt;memory.stat&lt;/code&gt;, 지표로는 cAdvisor의 &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;. 프로세스 묶음별로 세고, 그 묶음에 상한을 건다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿠버네티스 층&lt;/b&gt; &amp;mdash; kubelet이 계산해 적는다. &lt;code&gt;memory.max&lt;/code&gt;는 cgroup 파일에 쓰고 &lt;code&gt;allocatable&lt;/code&gt;&amp;middot;&lt;code&gt;requests&lt;/code&gt;는 apiserver에 보고한다. 지표로는 kube-state-metrics의 &lt;code&gt;kube_*&lt;/code&gt;. 노드를 나눈 선을 cgroup 파일로 옮기는 층이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 구분으로 인프라 대시보드 행1 &amp;mdash; 노드 한 대의 메모리를 선언(스케줄러가 보는 값)과 실사용(커널이 보는 값)으로 갈라 놓은 표 &amp;mdash; 의 뺄셈을 고쳤다.&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;code&gt;requests&lt;/code&gt;&amp;middot;&lt;code&gt;limit&lt;/code&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;requests&lt;/code&gt;&lt;/b&gt; &amp;mdash; 쿠버네티스만 아는 선언. 스케줄러가 자리를 잡을 때 쓰고, 노드가 빡빡해지면 어느 파드를 먼저 내보낼지도 정한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;limit&lt;/code&gt;&lt;/b&gt; &amp;mdash; cgroup 상한. 회수 불가한 사용이 넘으면 커널이 OOMKill 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 전수의 &lt;code&gt;requests&lt;/code&gt;&amp;middot;실사용&amp;middot;&lt;code&gt;limit&lt;/code&gt;을 한 표로 놓으니 어긋난 모양이 셋이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;모양&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;크기&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;무엇이 문제인가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;requests 과대 (requests &amp;gt; 사용)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;4,859 MiB&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;자리를 잡아 두고 안 쓴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;requests 과소 (사용 &amp;gt; requests)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1,018 MiB&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;압박이 오면 이 파드부터 나간다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;limit 임박 (peak 가 limit 의 76-90%)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;컨테이너 9개&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;조금 더 쓰면 OOMKill 이다&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;&lt;b&gt;과대&amp;middot;과소&amp;middot;임박 셋은 독립이 아니다.&lt;/b&gt; &lt;code&gt;requests&lt;/code&gt;를 넘겨 쓰는데 &lt;code&gt;limit&lt;/code&gt;에 여유가 많으면 선언이 작은 것이고, &lt;code&gt;requests&lt;/code&gt;는 맞는데 &lt;code&gt;limit&lt;/code&gt;만 좁으면 상한이 작은 것이다. &lt;b&gt;&lt;code&gt;requests&lt;/code&gt;&amp;middot;실사용&amp;middot;&lt;code&gt;limit&lt;/code&gt;을 같이 놓아야 어느 쪽을 고칠지가 갈린다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;AI가 낸 컴포넌트 표&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 모양을 잡으려면 컨테이너마다 값을 다시 정해야 한다. 나는 AI에게 클러스터 전수 분석을 맡겼고, AI가 지금 문제가 된다고 판단한 자리를 이렇게 냈다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 114px;&quot; align=&quot;left&quot;&gt;컴포넌트&lt;/th&gt;
&lt;th style=&quot;width: 605px;&quot; align=&quot;left&quot;&gt;지금 상태&lt;/th&gt;
&lt;th style=&quot;width: 137px;&quot; align=&quot;left&quot;&gt;낸 조치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;left&quot;&gt;Tempo&lt;/td&gt;
&lt;td style=&quot;width: 605px;&quot; align=&quot;left&quot;&gt;차트 기본값 밸러스트 1,024 MiB &amp;middot; GOMEMLIMIT 800 MiB &amp;middot; GC 12.58 회/초 &amp;middot; CPU 120 m. &lt;code&gt;go_memstats_heap_alloc&lt;/code&gt; 1,039 MiB 인데 &lt;code&gt;process_resident_memory&lt;/code&gt; 109 MiB&lt;/td&gt;
&lt;td style=&quot;width: 137px;&quot; align=&quot;left&quot;&gt;밸러스트를 0 으로 끈다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;left&quot;&gt;Kafka 브로커 &amp;times;3&lt;/td&gt;
&lt;td style=&quot;width: 605px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;-Xmx&lt;/code&gt; 없음 &amp;middot; MaxRAMPercentage 60 &amp;rarr; 힙 상한 921 Mi &amp;middot; 사용 621-643 Mi&lt;/td&gt;
&lt;td style=&quot;width: 137px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;-Xmx 512m&lt;/code&gt; 명시 후 재측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;left&quot;&gt;Argo controller&lt;/td&gt;
&lt;td style=&quot;width: 605px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;go_memstats_heap_alloc&lt;/code&gt; 127 Mi &amp;middot; &lt;code&gt;process_resident_memory&lt;/code&gt; 359-487 Mi&lt;/td&gt;
&lt;td style=&quot;width: 137px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;GOGC=50&lt;/code&gt; &amp;middot; &lt;code&gt;GOMEMLIMIT=384MiB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;left&quot;&gt;Strimzi operator&lt;/td&gt;
&lt;td style=&quot;width: 605px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;-Xmx&lt;/code&gt; 없음 &amp;middot; 실사용 281 Mi &amp;middot; request 256 Mi &amp;middot; limit 384 Mi&lt;/td&gt;
&lt;td style=&quot;width: 137px;&quot; align=&quot;left&quot;&gt;힙을 고정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 114px;&quot; align=&quot;left&quot;&gt;Mimir ingester &amp;times;3&lt;/td&gt;
&lt;td style=&quot;width: 605px;&quot; align=&quot;left&quot;&gt;request 768 Mi &amp;middot; limit 2 Gi&lt;/td&gt;
&lt;td style=&quot;width: 137px;&quot; align=&quot;left&quot;&gt;실측 RSS 로 재산정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 밖에 Alloy request 상향 &amp;middot; Loki 내장 캐시 상한 재배분 &amp;middot; Redis &lt;code&gt;maxmemory&lt;/code&gt; 설정이 같이 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&quot;지금 상태&quot; 열이 읽히지 않았다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tempo 행만 봐도 그렇다. &lt;code&gt;GOMEMLIMIT 800 MiB&lt;/code&gt;가 무엇을 거는 값인지, GC 12.58 회/초가 많은 것인지 적은 것인지, 1,039 MiB와 109 MiB가 나란히 있는 것이 정상인지. &lt;b&gt;판단이 서는 항목이 하나도 없었다.&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 data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;낱말&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;모르는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙 &amp;middot; 스택&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;둘이 어떻게 다른지 나올 때마다 헷갈린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;런타임&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;말은 자주 보는데 무엇을 가리키는지 흐릿하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;RSS&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;지표 이름은 읽히는데 무엇을 잰 값인지 모른다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;GC &amp;middot; GOGC&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;언제 왜 도는지 모른다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;-Xms &amp;middot; -Xmx &amp;middot; MaxRAMPercentage &amp;middot; GOMEMLIMIT&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;자바 쪽에서 힙 크기를 정한 적이 있고 여기서도 같은 종류가 나오는데, 서로 어떻게 다른지 모른다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;비힙&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙 밖에도 뭔가 있다는 것만 안다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;모르는 낱말은 AI가 낸 표 밖에도 있다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 낱말을 나는 클러스터를 세우는 동안 이미 여러 번 적어 뒀다. AI가 낸 컴포넌트 표에 없는 것만 꼽아도 이렇다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;적어 둔 자리&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;적어 둔 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;booking values&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;JAVA_OPTS: -XX:MaxRAMPercentage=75&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;topicOperator&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;-Xms: 64m&lt;/code&gt; &amp;middot; &lt;code&gt;-Xmx: 128m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;booking 대시보드 패널 설명&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&quot;limit 1Gi 의 75%인 768Mi가 힙이고, 나머지는 클래스 정보와 컴파일해 둔 기계어가 쓴다. 힙이 상한 가까이 붙어 있으면 GC가 잦아진다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나는 적을 때 설계를 AI 에 맡기고 이해는 뒤로 미뤘다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;절 배치&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 밖은 리눅스 층&amp;middot;cgroup 층&amp;middot;쿠버네티스 층으로 갈랐고, 그 끝에서 나온 것이 컨테이너에 걸리는 상한 &lt;code&gt;memory.max&lt;/code&gt;다. 이 편은 그 상한의 안쪽이다.&lt;/p&gt;
&lt;table style=&quot;height: 128px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot; align=&quot;left&quot;&gt;절&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot; align=&quot;left&quot;&gt;세우는 것&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot; align=&quot;left&quot;&gt;다음 절에 넘기는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;힙 &amp;middot; 스택 &amp;middot; 스레드당 스택&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;힙 크기는 코드만 봐선 모른다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;런타임 &amp;middot; 힙 지표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;힙에 얼마나 있는지는 런타임만 안다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;VSZ &amp;middot; RSS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;힙 지표와 RSS 가 벌어져 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;GC &amp;middot; GOGC &amp;middot; 손잡이&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;GC 페이스는 상한을 안 정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;두 상한 &amp;middot; &lt;code&gt;working_set&lt;/code&gt; &amp;middot; 비힙 &amp;middot; 사용자 수준 스레드 &amp;middot; 앱이 거는 상한&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&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;안을 파헤치다 보면 밖에서 정리해 둔 것과 다시 만난다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;밖에서 정리한 것&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;안에서 다시 만나는 자리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;익명 메모리(회수 불가) / 페이지 캐시(회수 가능)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;4절 &amp;mdash; RSS 안이 같은 기준으로 갈린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;익명 메모리의 예로 든 Kafka 브로커의 607 Mi&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;6절 &amp;mdash; 그 익명이 힙과 비힙으로 갈린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;memory.max&lt;/code&gt; &amp;mdash; 커널이 강제하는 상한&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;6절 &amp;mdash; 런타임이 스스로 지키는 상한과 마주 세운다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memory.max&lt;/code&gt;가 걸린 묶음 안에서 프로세스 하나가 무엇을 받는지부터 본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 힙과 스택&lt;/h2&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;높은 주소
┌──────────────────┐
│  스택             │   함수 호출마다 쌓이고, 함수가 끝나면 사라진다
├──────────────────┤
│     (빈 공간)     │
├──────────────────┤
│  힙               │   실행 중에 &quot;달라&quot;고 요청해서 받는다.  위로 자란다
├──────────────────┤
│  data &amp;middot; bss      │   전역 변수
├──────────────────┤
│  text            │   기계어 코드.  읽기 전용
└──────────────────┘
낮은 주소&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;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 65px;&quot; align=&quot;left&quot;&gt;조각&lt;/th&gt;
&lt;th style=&quot;width: 528px;&quot; align=&quot;left&quot;&gt;여기 들어가는 것&lt;/th&gt;
&lt;th style=&quot;width: 205px;&quot; align=&quot;left&quot;&gt;언제 정해지나&lt;/th&gt;
&lt;th style=&quot;width: 56px;&quot; align=&quot;left&quot;&gt;분류&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 65px;&quot; align=&quot;left&quot;&gt;text&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot; align=&quot;left&quot;&gt;함수 본체를 기계어로 번역한 것&lt;/td&gt;
&lt;td style=&quot;width: 205px;&quot; align=&quot;left&quot;&gt;컴파일할 때. 실행 내내 안 변한다&lt;/td&gt;
&lt;td style=&quot;width: 56px;&quot; align=&quot;left&quot;&gt;정적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 65px;&quot; align=&quot;left&quot;&gt;data&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot; align=&quot;left&quot;&gt;초기값이 있는 전역 변수 &amp;mdash; &lt;code&gt;var limit = 300000&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 205px;&quot; align=&quot;left&quot;&gt;컴파일할 때. 값까지 실행 파일 안에 들어 있다&lt;/td&gt;
&lt;td style=&quot;width: 56px;&quot; align=&quot;left&quot;&gt;정적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 65px;&quot; align=&quot;left&quot;&gt;bss&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot; align=&quot;left&quot;&gt;초기값이 없는 전역 변수 &amp;mdash; &lt;code&gt;var total int64&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 205px;&quot; align=&quot;left&quot;&gt;컴파일할 때 자리만. 값은 시작할 때 0으로 채운다&lt;/td&gt;
&lt;td style=&quot;width: 56px;&quot; align=&quot;left&quot;&gt;정적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 65px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;스택&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot; align=&quot;left&quot;&gt;지금 실행 중인 함수의 지역 변수&amp;middot;매개변수, 그 함수가 끝나면 돌아갈 자리&lt;/td&gt;
&lt;td style=&quot;width: 205px;&quot; align=&quot;left&quot;&gt;함수에 들어가면 잡히고 나오면 풀린다&lt;/td&gt;
&lt;td style=&quot;width: 56px;&quot; align=&quot;left&quot;&gt;자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 65px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;힙&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot; align=&quot;left&quot;&gt;프로그램이 &quot;이만큼 달라&quot;고 해서 받아 낸 자리. 요청 하나마다 만드는 객체, 네트워크나 디스크에서 읽어 들인 바이트, 나중에 쓰려고 들고 있는 캐시 &amp;mdash; &lt;b&gt;셋 다 몇 개가 생길지를 들어오는 일감이 정한다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 205px;&quot; align=&quot;left&quot;&gt;실행 중에 요청한 만큼. 언제 버릴지도 실행 중에 정해진다&lt;/td&gt;
&lt;td style=&quot;width: 56px;&quot; align=&quot;left&quot;&gt;동적&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 셋은 실행 파일에서 크기가 읽힌다. 남는 것이 스택과 힙인데, &lt;b&gt;스택은 스레드 수에 스택 크기를 곱하면 나오고 힙은 돌려 보기 전에는 모른다.&lt;/b&gt; 그 둘을 차례로 본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;스택은 스레드마다 하나씩, 늘리면 값이 붙는다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text&amp;middot;data&amp;middot;bss&amp;middot;스택&amp;middot;힙으로 나뉜다는 것은 프로세스가 실행 흐름 하나일 때의 그림이다. 스레드를 여럿 돌리면 갈린다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;프로세스 하나
├─ text &amp;middot; data &amp;middot; bss        하나.  스레드 전부가 공유
├─ 힙                       하나.  스레드 전부가 공유
│                             요청마다 만든 객체, 읽어 온 응답 바이트, 쌓아 둔 캐시
├─ 스레드 A ─ 스택 A       스레드 하나 몫으로 따로 예약
│               ├ main()    프레임
│               ├ handle()  프레임
│               └ parse()   프레임   &amp;larr; 지금 실행 중. 반환하면 이 칸만 풀린다
└─ 스레드 B ─ 스택 B
                └ ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;함수마다 생기는 것은 스택이 아니라 그 안의 프레임 한 칸이다.&lt;/b&gt; 스택은 스레드가 만들어질 때 한 번 잡히고, 함수를 부를 때마다 지역 변수&amp;middot;매개변수&amp;middot;돌아갈 주소를 담은 프레임이 그 안에 쌓인다. 반환하면 그 칸만 풀린다.&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;OS 스레드&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;스택 크기&lt;/b&gt; &amp;mdash; 리눅스는 OS 스레드 하나에 기본 8 MiB를 예약한다. 다만 프로그램이 스레드를 만들 때 이 값을 직접 넘길 수 있고, JVM은 그렇게 한다(리눅스 x64 기본 &lt;code&gt;-Xss&lt;/code&gt; 1 MiB)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문맥 교환&lt;/b&gt; &amp;mdash; CPU 하나가 여러 스레드를 번갈아 돌리려면 돌던 것의 실행 상태를 저장하고 다음 것을 복원해야 한다. 스레드는 이 일을 커널이 한다&lt;/li&gt;
&lt;/ul&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;h5&gt;&lt;b&gt;힙은 무엇이 어떻게 올라가고, 누가 버리나&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 올라가는 쪽이다. &lt;b&gt;소스 어디에도 &quot;이건 힙에 둬라&quot;라고 적지 않는다.&lt;/b&gt; 이 클러스터의 두 언어가 그것을 서로 다르게 정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Go &amp;mdash; 컴파일러가 가른다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func newTicket() *Ticket {
    t := Ticket{seat: &quot;A12&quot;}   // 함수 안에서 만든 값
    return &amp;amp;t                  // 그 값이 있는 자리의 주소를 밖으로 넘긴다
}&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;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;Java &amp;mdash; &lt;code&gt;new&lt;/code&gt; 를 쓰면 힙이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Ticket t = new Ticket(&quot;A12&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;스택 프레임            힙
┌───────────┐      ┌─────────────────┐
│ t = 0x7f&amp;hellip; │ ───► │  Ticket(&quot;A12&quot;)  │
└───────────┘      └─────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 &lt;code&gt;t&lt;/code&gt; 는 스택 프레임 안에 있지만 그 안에 든 것은 객체가 아니라 &lt;b&gt;힙에 있는 객체의 주소&lt;/b&gt;다. 가르는 판단이 없으니 &lt;code&gt;new&lt;/code&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;code&gt;new&lt;/code&gt; 로 다른데, &lt;b&gt;둘 다 프로그래머가 &quot;힙&quot;이라고 쓰지 않고 얼마나 쌓일지는 실행 중에 들어오는 일감이 정한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 버리는 쪽이다. 올라간 자리는 다 쓰고 나면 버려야 한다. 그리고 &lt;b&gt;버리는 문제가 남는 것은 힙뿐이다&lt;/b&gt; &amp;mdash; text와 data는 프로세스가 죽을 때까지 그대로 있고, 스택은 함수가 끝나면 저절로 풀린다. 힙만 함수 경계와 수명이 안 묶여 있어서 자동으로 풀릴 자리가 없다. 안 버리면 계속 쌓이고(누수), 아직 쓰는 것을 버리면 프로그램이 깨진다.&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; Go에도 Java에도 &quot;이제 이 자리를 반납한다&quot;고 적는 자리가 없다. 그런데도 힙이 끝없이 부풀지 않고 쓰던 객체가 사라지지도 않으니, 코드 밖에서 그 일을 하는 무엇이 있다는 뜻이다. 그것이 다음 절이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 런타임&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;그 &quot;코드 밖의 무엇&quot;의 이름이 &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;br /&gt;&amp;mdash; &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%9F%B0%ED%83%80%EC%9E%84_%EC%8B%9C%EC%8A%A4%ED%85%9C&quot;&gt;위키백과 &amp;middot; 런타임 시스템&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정의에 스택&amp;middot;힙 관리와 쓰레기 수집이 그대로 들어 있다. 이 글에서 쓰는 말로 줄이면, &lt;b&gt;내가 짠 코드와 같은 프로세스 안에서 함께 도는 코드&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;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; 앱이 &quot;객체 하나 만들자&quot;고 하면 힙에서 자리를 떼어 준다. 무엇이 올라갈지는 컴파일러와 &lt;code&gt;new&lt;/code&gt; 가 정했고, 그 자리를 실제로 떼어 주는 쪽이 이것이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다 쓴 것을 치운다&lt;/b&gt; &amp;mdash; 아무도 안 쓰게 된 자리를 되쓸 수 있게 만든다. 코드에 없던 그 문장이 여기 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 단위를 스케줄링한다&lt;/b&gt; &amp;mdash; 어느 코드를 어느 스레드 위에서 돌릴지 정한다. 셋 중 여기서 처음 나오는 것이다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;어느 언어에 있고, 어디서 오나&lt;/b&gt;&lt;/h5&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 align=&quot;left&quot;&gt;언어&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;컴파일 결과&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;런타임&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;이 클러스터의 파드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;C &amp;middot; C++&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;기계어&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;없다&lt;/b&gt; &amp;mdash; &lt;code&gt;free&lt;/code&gt;&amp;middot;&lt;code&gt;delete&lt;/code&gt; 를 직접 부른다&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Redis &amp;middot; MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Go&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;기계어&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실행 파일 안에 함께 묶여 나온다&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Tempo &amp;middot; Loki &amp;middot; Mimir &amp;middot; Alloy &amp;middot; Argo &amp;middot; queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Java&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;바이트코드 (JVM 이 읽는 중간 형식)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;그것을 읽어 돌리는 JVM 본체 &lt;code&gt;libjvm.so&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Kafka 브로커 &amp;middot; booking &amp;middot; Strimzi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;C 와 Go 는 둘 다 기계어로 컴파일되는데 런타임 유무가 갈린다.&lt;/b&gt; 컴파일 방식이 정하는 것이 아니고, 그래서 힙을 버린다고 적는 문장(&lt;code&gt;free&lt;/code&gt;)이 C 에만 있다.&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;Go 와 Java 는 둘 다 런타임이 있으니 하는 일 셋을 둘 다 한다. &lt;b&gt;다만 하는 방식이 갈리고, 셋 중 아직 안 본 것이 하나 남았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;실행 단위는 런타임이 굴린다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 힙이다 &amp;mdash; 런타임이 잡아 주고 런타임이 치운다. 그런데 런타임이 손대는 것이 힙만은 아니다.&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;스택은 스레드마다 붙는다. 그리고 CPU 하나가 여러 스레드를 번갈아 돌리려면 매번 실행 상태를 저장하고 복원해야 한다(문맥 교환). &lt;b&gt;둘 다 커널 몫이라, 스레드를 하나 늘릴 때마다 스택이 하나 더 잡히고 커널이 할 일도 그만큼 는다.&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;b&gt;Go 와 Java 의 런타임이 같은 우회로를 냈다.&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;b&gt;런타임이 그 몫을 스스로 마련해야 하고, 어디서 마련하느냐가 갈린다.&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;누가 만들고 굴리나&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;스택 크기&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;스택이 잡히는 자리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;OS 스레드&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;커널&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;리눅스 기본 8 MiB 예약. JVM 은 &lt;code&gt;-Xss&lt;/code&gt; 1 MiB&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙 밖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;고루틴 (Go)&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Go 런타임&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;2 KiB 로 시작해 필요하면 늘린다&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙 밖&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;가상 스레드 (Java 21)&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;JVM&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;필요한 만큼 자란다&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙 안&lt;/b&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;셋이 세 축에서 갈린다.&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; 고루틴과 가상 스레드는 런타임이 굴린다. 이 둘을 묶어 &lt;b&gt;사용자 수준 스레드&lt;/b&gt;라 한다. OS 스레드를 안 쓴다는 뜻은 아니고 그 위에 얹혀서 도는 것이며, &lt;b&gt;커널이 아는 것은 OS 스레드까지다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;런타임이 스택을 어디에 마련하나&lt;/b&gt; &amp;mdash; Go 런타임은 힙과 따로 셈하는 자리에 두고, JVM 은 가상 스레드의 스택을 힙 안에 객체로 둔다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그래서 그 몫을 덮는 값이 갈린다&lt;/b&gt; &amp;mdash; OS 스레드는 &lt;code&gt;-Xss&lt;/code&gt; &amp;times; 스레드 수로 힙 밖에서, 고루틴은 &lt;code&gt;GOMEMLIMIT&lt;/code&gt; 안에서, 가상 스레드는 &lt;code&gt;-Xmx&lt;/code&gt; 안에서 센다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름만 짚고 넘어간다. &lt;b&gt;셋이 각각 무엇을 정하는 값이고 그 값을 넘으면 어떻게 되는지는 6절이다&lt;/b&gt; &amp;mdash; 셋이 같은 종류가 아니다. 하는 일 셋 중 셋째가 여기까지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;힙에 얼마나 있는지는 런타임만 안다&lt;/b&gt;&lt;/h5&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;&amp;nbsp;&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;pre class=&quot;armasm&quot;&gt;&lt;code&gt;파드
 └ 컨테이너
     └ 프로세스 ─ 주소 공간 ─ text &amp;middot; data &amp;middot; bss &amp;middot; 스택 &amp;middot; 힙   &amp;larr; 런타임이 아는 칸&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;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;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Go    go_memstats_heap_alloc_bytes          할당돼 아직 해제되지 않은 힙 객체의 합
JVM   jvm_memory_used_bytes{area=&quot;heap&quot;}    힙 영역에 들어 있는 양&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;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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;힙 밖까지 세는 지표는 따로 있다.&lt;/b&gt; 프로세스에 물리 메모리가 얼마나 붙었는지는 OS 가 알고 있고, 그것을 내주는 것이 &lt;code&gt;process_resident_memory_bytes&lt;/code&gt; 다. 줄여서 &lt;b&gt;RSS&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;b&gt;여기까지가 이 절이 답한 것이다.&lt;/b&gt; 힙을 버리는 것은 런타임이고, 힙에 얼마가 들어 있는지는 런타임이 내주는 지표에 나온다. 그 숫자가 힙 한 칸짜리라는 것까지 왔다. &lt;b&gt;다음 절은 그 바깥을 세는 RSS 를 판다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. RSS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 지표는 &quot;힙에 객체가 얼마나 들어앉았나&quot;에 답했다. &lt;b&gt;RSS 가 답하는 질문은 다르다 &amp;mdash; &quot;이 프로세스가 물리 메모리를 얼마나 차지하고 있나&quot;다.&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;이 값을 읽으려면 &quot;물리 메모리를 차지한다&quot;가 무슨 일인지부터 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;주소를 받는 것과 메모리가 붙는 것&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스가 커널에게서 받는 것은 메모리 자체가 아니라 쓸 수 있는 주소의 범위였다. &lt;b&gt;그 &quot;메모리 자체&quot;가 물리 메모리&lt;/b&gt; &amp;mdash; RAM 칩의 실제 자리다.&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;커널은 RAM 을 통으로 다루지 않고 &lt;b&gt;4 KiB 조각&lt;/b&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스가 커널에게 주소 범위를 달라고 하면 &lt;b&gt;커널이 정해 주는 것은 범위뿐이다.&lt;/b&gt; &quot;이 범위는 네 것&quot;이라는 약속이고 표는 비어 있다. 물리 메모리는 한 장도 안 붙는다.&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;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; 힙이 모자라 런타임이 더 받아 오는 것도, 스레드를 만들 때 스택 몫을 잡는 것도, 실행 파일 코드가 올라오는 것도 전부 이 규칙을 따른다. 스택을 리눅스 기본 8 MiB 로 &lt;b&gt;예약&lt;/b&gt;한다고 했던 것이 이 얘기다 &amp;mdash; 주소만 잡아 둔 것이고, 함수가 깊이 안 들어가면 붙는 페이지는 몇 장뿐이다.&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: 52px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;th style=&quot;height: 10px;&quot; align=&quot;left&quot;&gt;값&lt;/th&gt;
&lt;th style=&quot;height: 10px;&quot; align=&quot;left&quot;&gt;세는 것&lt;/th&gt;
&lt;th style=&quot;height: 10px;&quot; align=&quot;left&quot;&gt;지표 이름&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;가상 크기 (VSZ)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;약속받은 주소의 합. 1 GiB 를 잡으면 바로 1 GiB 가 는다&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;&lt;code&gt;process_virtual_memory_bytes&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;RSS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;물리 페이지가 붙은 합. 값을 써 넣은 만큼만 는다&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;&lt;code&gt;process_resident_memory_bytes&lt;/code&gt;&lt;/b&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;&lt;b&gt;RSS 는 Resident Set Size&lt;/b&gt; &amp;mdash; 물리 메모리에 상주(resident)하는 집합의 크기라는 이름이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1 GiB를 잡아 놓고 한 번도 안 건드리면, VSZ 는 1 GiB 늘고 RSS 는 그대로다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;&lt;b&gt;RSS 안에 무엇이 들어 있나&lt;/b&gt;&lt;/h5&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 align=&quot;left&quot;&gt;RSS 안&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;무엇이 들어 있나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;익명 메모리&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;값을 써 넣은 힙 페이지 &amp;middot; 스택 &amp;middot; 전역. 디스크에 원본이 없어 버리면 잃는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;페이지 캐시 중 이 프로세스에 매핑된 것&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실행 파일 코드 &amp;middot; 공유 라이브러리. 디스크 사본이라 버려도 다시 읽으면 된다&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;&lt;b&gt;그래서 RSS 는 &quot;이 프로세스가 노드 메모리를 실제로 얼마나 점유하나&quot;에 답한다.&lt;/b&gt; 컨테이너에 프로세스가 하나면 사실상 그 컨테이너의 점유량이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;같은 프로세스를 세는 놈이 둘이다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 지표와 RSS, 같은 프로세스에 숫자가 둘이 됐다. &lt;b&gt;숫자만이 아니라 세는 놈까지 봐야 뜻이 정해진다.&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;지표&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;누가 세나&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;무엇을 보나&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;답하는 질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙 지표 &lt;code&gt;go_memstats_heap_alloc&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Go 런타임&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙에 객체가 들어앉았나&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙의 내역&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;RSS &lt;code&gt;process_resident_memory&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OS&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;물리 페이지가 붙었나&lt;/td&gt;
&lt;td align=&quot;left&quot;&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;&lt;b&gt;서로가 서로를 대신하지 못한다.&lt;/b&gt; RSS 는 물리 페이지가 붙었다는 사실만 알지 그것이 힙인지 스택인지, 산 객체의 자리인지 죽은 객체가 쓰던 자리인지 모른다. 힙 지표는 객체를 알지만, 배정은 적어 두는 일일 뿐이라 물리 페이지가 붙었는지 모른다.&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;VSZ 까지 셋을 층으로 놓으면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;커널이 힙으로 준 주소 범위                     VSZ (다른 구역들과 함께)
 └ 그중 런타임이 객체에 배정한 만큼             힙 지표 (heap_alloc)
    └ 그중 값이 써져 물리 페이지가 붙은 만큼     RSS 의 힙 몫 (스택&amp;middot;코드와 함께)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;힙 지표와 RSS 의 폭 &amp;mdash; 죽은 객체의 자리&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 가 낸 표의 Argo controller 행이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;힙 지표    127 Mi
RSS        359-487 Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSS 가 힙 밖까지 세니 더 큰 방향은 맞다.&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;그런데 힙 밖 몫(스택&amp;middot;실행 파일 코드)은 수십 Mi 수준이라 &lt;b&gt;세 배라는 폭이 안 나온다.&lt;/b&gt; 폭의 정체는 힙 페이지 하나의 일생을 따라가면 보인다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;① 배정       객체가 자리에 앉는다              힙 지표 +
② 값 써짐    물리 페이지가 붙는다              RSS +
③ 객체 죽음  아무도 안 가리키게 된다           둘 다 그대로
④ 치움       런타임이 자리를 회수한다           힙 지표 &amp;minus;
⑤ 반납       물리 페이지를 커널에 돌려준다       RSS &amp;minus;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;④ 치움의 이름이 GC(Garbage Collection) 다.&lt;/b&gt; 죽은 객체를 찾아 그 자리를 다음 객체를 앉혀도 되는 빈자리로 되돌리는 일이다. 물리 페이지를 건드리는 일이 아니라 &lt;b&gt;런타임이 제 기록의 표시를 바꾸는 일&lt;/b&gt;이라, 이 순간 RSS 는 안 움직인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⑤ 반납을 런타임은 서두르지 않는다.&lt;/b&gt; 그 빈자리는 곧 다음 객체가 앉을 자리고, 커널에 돌려줬다가 다시 받으면 그 요청이 한 번 더 든다. 그래서 런타임은 페이지를 쥔 채 되쓴다. 새 객체가 앉으면 힙 지표만 다시 늘고 RSS 는 그대로다 &amp;mdash; &lt;b&gt;자리는 비었다 찼다 돌지만, 페이지는 한 번 붙으면 오래 붙어 있는다.&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;b&gt;치워져서 다음 객체를 기다리는 페이지&lt;/b&gt;가 깔려 있다. 힙 지표에는 없고 RSS 에는 있는 몫 &amp;mdash; &lt;b&gt;Argo 폭의 대부분이 이것이다.&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;b&gt;GC 가 언제 도는지가 이 폭을 정한다&lt;/b&gt; &amp;mdash; 늦게 돌수록 죽은 객체와 빈 페이지가 더 쌓인다. 그 시점의 규칙과, 사람이 그것을 만지는 값이 다음 절이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. GC와 GOGC&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GC 가 치우는 것이 정확히 이것이다 &amp;mdash; 참조가 하나도 안 남은 객체.&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;찾아 치우는 데는 CPU 가 든다. 그래서 GC 는 맞바꾸기다 &amp;mdash; &lt;b&gt;자주 돌리면 CPU 를 계속 쓰고, 미루면 죽은 객체가 물리 페이지를 깔고 앉은 채 쌓인다.&lt;/b&gt; 어느 쪽으로 기울일지 정하는 자리가 이 절의 손잡이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;Go 는 살아있는 양의 두 배에서 돈다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 런타임에 &quot;언제 돌라&quot;를 정해 주는 값이 &lt;code&gt;GOGC&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;규칙은 한 줄인데, 읽으려면 힙 지표의 성질 하나가 먼저 필요하다. 죽은 객체는 치워지기 전까지 그대로 잡히므로 &lt;b&gt;힙 지표 = 살아있는 객체 + 아직 안 치운 죽은 객체&lt;/b&gt;다. GC 가 돌면 죽은 몫이 빠져 살아있는 양까지 떨어졌다가, 일감이 들어오는 만큼 다시 쌓인다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GOGC&lt;/code&gt; 기본값 100 &amp;mdash; &lt;b&gt;힙 지표가, 직전 GC 가 끝났을 때 살아있던 양의 두 배에 닿으면 돈다.&lt;/b&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;살아있는 양이 100 Mi 로 일정하다고 치고 &lt;code&gt;heap_alloc&lt;/code&gt; 을 시간축으로 그리면 이 모양이 나온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;힙 지표   100 ↗↗ 200 &amp;middot;GC↘ 100 ↗↗ 200 &amp;middot;GC↘ 100 &amp;hellip;      차오르다 GC 에 수직으로 떨어지기를 반복
RSS       ──────────── 200 언저리에서 거의 수평 ────────────&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 줄이 &lt;b&gt;톱니(sawtooth)&lt;/b&gt; 라 부르는 그래프다. 바닥이 살아있는 양(100), 꼭대기가 두 배 선(200)이고, 그 사이를 채우며 올라가는 것은 대부분 죽은 채 쌓이는 객체다.&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;힙 지표는 GC 마다 바닥까지 떨어지는데 RSS 는 같이 안 떨어진다.&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;그래서 살아있는 것은 100 인데 RSS 는 200 언저리다. 이 100 의 차이에 이름을 붙이면 둘이다.&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; 두 배 선까지 차게 두는 규칙의 몫. 살아있는 양만큼이 위에 얹힌다&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;이제 Argo controller 를 대 본다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;힙 지표   127 Mi
RSS       359-487 Mi      두 배로 쳐도 254 &amp;mdash; 그보다 크다&lt;/code&gt;&lt;/pre&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;127 이 톱니의 바닥인지 중간인지 &amp;mdash; 한 순간의 값으로는 모른다&lt;/li&gt;
&lt;li&gt;RSS 가 기억하는 것은 지금 톱니가 아니라 &lt;b&gt;역대 가장 높았던 꼭대기&lt;/b&gt;다&lt;/li&gt;
&lt;li&gt;힙 밖 몫(고루틴 스택&amp;middot;런타임 메타데이터&amp;middot;실행 파일 코드)은 안 쟀다 &amp;mdash; &lt;code&gt;go_memstats_heap_sys&lt;/code&gt;&amp;middot;&lt;code&gt;heap_idle&lt;/code&gt;&amp;middot;&lt;code&gt;stack_sys&lt;/code&gt; 를 떠야 갈린다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 그림에서 가져갈 것은 숫자가 아니라 방향이다. &lt;b&gt;살아있는 양보다 몇 배 큰 RSS 는 바빠서 생기는 것이 아니라 이 구조가 만든다.&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;code&gt;GOGC&lt;/code&gt; 를 낮추는 것은 &lt;b&gt;두 배 선을 끌어내리는 것&lt;/b&gt;이다. &lt;code&gt;GOGC=50&lt;/code&gt; 이면 살아있는 양의 1.5배 &amp;mdash; 위 예에서 150 &amp;mdash; 에서 돌아 톱니 꼭대기가 낮아지고, RSS 가 따라 내려온다. 대신 같은 쓰레기를 더 자주 치우니 CPU 를 더 쓴다. &lt;b&gt;메모리를 줄이는 대신 CPU 를 내는 손잡이고, Argo 의 폭이 정말 이 구조였는지는 이 값을 걸고 RSS 가 내려오는지로 확인된다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;JVM 은 Eden 이 차면 돈다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 도 고르는 기준은 같다 &amp;mdash; 참조가 없어진 객체. 갈리는 것은 &lt;b&gt;도는 시점을 재는 방식&lt;/b&gt;이다. Go 가 힙 지표가 선에 닿는 것을 신호로 쓴다면, &lt;b&gt;JVM 은 힙을 방 셋으로 나눠 두고 그중 한 방이 차는 것을 신호로 쓴다.&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;방&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;무엇이 들어가나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Eden&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;새로 만든 객체는 전부 여기부터&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Survivor&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;GC 에서 한 번 살아남은 객체가 잠깐 머문다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Tenured&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;오래 살아남은 객체가 옮겨 가서 산다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 객체는 Eden 에 쌓이고, &lt;b&gt;Eden 이 차면 GC 가 돈다.&lt;/b&gt; 살아남은 것은 다음 방으로 옮기고 Eden 을 비운다.&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;그럼 Eden 의 크기는 누가 정하나. &lt;b&gt;힙 전체 크기에서 비율로 나뉜다.&lt;/b&gt; JVM 은 힙 전체의 상한을 &lt;code&gt;-Xmx&lt;/code&gt; 라는 값으로 받는데(이 값이 어떻게 정해지는지는 다음 절), booking 의 &lt;code&gt;-Xmx&lt;/code&gt; 768 Mi 는 실측으로 이렇게 갈라져 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Tenured    512.0 MiB        768 의 3분의 2
Eden       204.8 MiB        남은 3분의 1 을 8 : 1 : 1 로
Survivor    25.6 MiB &amp;times; 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;&lt;code&gt;-Xmx&lt;/code&gt; 를 절반으로 내리면 Eden 도 절반이 되어, 같은 일감에서 GC 가 두 배 자주 돈다.&lt;/b&gt; 상한이 시점을 직접 정하지는 않지만 방 크기를 거쳐 빈도에 걸린다. Tenured 쪽 회수는 시점 규칙이 또 다르고, 이 편에서는 안 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&quot;언제 도나&quot;는 알았고 &quot;어디까지 자라나&quot;가 남았다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 온 길을 한 줄씩 놓으면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2절  힙 &amp;middot; 스택     힙의 객체만 수명이 함수에 안 묶인다.  참조가 사라져 죽은 뒤에도
                  누가 치우기 전까지 힙에 남고, 얼마나 커질지는 코드만 봐서는 모른다

3절  런타임        그 죽은 객체를 치우는 것이 런타임이고,
                  힙에 얼마가 들었는지도 런타임이 힙 지표로 내준다

4절  RSS           그런데 파드가 실제로 차지한 메모리는 OS 가 RSS 로 따로 세고,
                  힙 지표보다 몇 배 크다 &amp;mdash; 죽은 객체가 쓰던 페이지를 치운 뒤에도
                  커널에 바로 안 돌려주기 때문이다

5절  GC &amp;middot; GOGC     그 치움이 GC 고, 그래서 폭은 GC 가 언제 도는지가 정한다.
                  Go 는 힙 지표가 두 배 선에 닿을 때 돌고, JVM 은 Eden 이 찰 때 돈다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 절이 정한 것은 거기까지 &amp;mdash; &lt;b&gt;GC 가 도는 시점&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Go 에는 자람을 막는 선이 없다.&lt;/b&gt; &lt;code&gt;GOGC&lt;/code&gt; 가 정하는 것은 다음 GC 지점이 직전 살아남은 양의 몇 배냐일 뿐이라, 살아있는 양이 늘면 그 지점도 같이 올라간다. 일감이 늘어 살아있는 양이 두 배가 되면 GC 가 도는 힙 크기도 두 배가 된다. 위에서 100&amp;middot;200 으로 그린 톱니가 통째로 올라가는 것이고, 그것을 잡아 두는 값은 &lt;code&gt;GOGC&lt;/code&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;JVM 에는 선이 있다.&lt;/b&gt; Eden 이 차면 돈다는 규칙 자체가 방 크기에 매여 있고, 그 방들이 &lt;code&gt;-Xmx&lt;/code&gt; 를 나눈 값이기 때문이다. 다만 &lt;code&gt;-Xmx&lt;/code&gt; 가 어디서 오는지는 아직 안 봤다 &amp;mdash; booking 은 그 값을 직접 적지 않았는데도 768 Mi 로 서 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 1절에 적어 둔 모르는 낱말 중 셋이 남는데, 셋 다 방금 세운 그림의 빈자리에 하나씩 꽂힌다.&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;GOMEMLIMIT&lt;/code&gt;&lt;/b&gt; &amp;mdash; Go 의 없는 선 자리에 사람이 넣는 값. Tempo 에 800 MiB 로 걸려 있던 그것이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;-Xms&lt;/code&gt; &amp;middot; &lt;code&gt;-Xmx&lt;/code&gt; &amp;middot; &lt;code&gt;MaxRAMPercentage&lt;/code&gt;&lt;/b&gt; &amp;mdash; JVM 선의 출처. booking 이 &lt;code&gt;-Xmx&lt;/code&gt; 없이도 768 Mi 로 서 있는 이유가 이 안에 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비힙&lt;/b&gt; &amp;mdash; 선 바깥에 남는 몫. booking 은 힙 사용이 74 Mi 인데 컨테이너는 420 MiB 를 쓴다 &amp;mdash; 힙만으로는 설명이 안 되는 나머지다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋은 전부 &lt;b&gt;컨테이너 안쪽의 값&lt;/b&gt;이다. 그런데 이 편을 시작하게 만든 문제 &amp;mdash; 예약이 과대&amp;middot;과소로 어긋나고 peak 가 &lt;code&gt;limit&lt;/code&gt; 의 76-90% 에 붙은 컨테이너가 아홉이던 그 상태 &amp;mdash; 에서 파드를 실제로 죽이고 살리는 선은 &lt;b&gt;컨테이너 바깥에서 커널이 긋는 &lt;code&gt;limit&lt;/code&gt;&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;b&gt;안쪽과 바깥은 서로를 아는가.&lt;/b&gt; &lt;code&gt;limit&lt;/code&gt; 을 바꾸면 안쪽 선이 따라 움직이는가, 안쪽 선을 다 정하면 &lt;code&gt;limit&lt;/code&gt; 이 지켜지는가. 다음 절이 그 둘을 마주 세운다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;6. 두 상한&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 절 끝의 질문을 그대로 들고 간다 &amp;mdash; &lt;b&gt;&lt;code&gt;limit&lt;/code&gt; 을 바꾸면 안쪽 선이 따라 움직이는가. 안쪽 선을 다 정하면 &lt;code&gt;limit&lt;/code&gt; 이 지켜지는가.&lt;/b&gt; 답하려면 두 선을 한자리에 놓아야 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;실체&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;누가 지키나&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;넘으면&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;바깥 선&lt;/b&gt; &lt;code&gt;limit&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;cgroup 의 &lt;code&gt;memory.max&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;커널&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OOMKill &amp;mdash; 예고 없이 밖에서 죽는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;안쪽 선&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;런타임 설정 &amp;mdash; JVM 의 힙 상한 &amp;middot; Go 의 &lt;code&gt;GOMEMLIMIT&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;런타임이 스스로&lt;/td&gt;
&lt;td align=&quot;left&quot;&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;&lt;b&gt;기본값 상태에서 둘은 서로를 참조하지 않는다.&lt;/b&gt; 커널은 런타임의 힙 지표를 읽지 않고, 런타임이 &lt;code&gt;limit&lt;/code&gt; 을 아는지는 언어가 갈린다 &amp;mdash; &lt;b&gt;JVM 은 읽어 오고, Go 는 모른다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;바깥 선 &amp;mdash; 커널은 working_set 으로 판정한다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;limit&lt;/code&gt; 은 values 에 적는 쿠버네티스 값이지만, kubelet 이 cgroup 파일 &lt;code&gt;memory.max&lt;/code&gt; 에 써 넣는 순간부터 지키는 쪽은 커널이다. 그리고 커널이 판정에 쓰는 숫자는 힙 지표도 RSS 도 아니다 &amp;mdash; &lt;b&gt;cgroup 이 직접 세는 세 번째 숫자&lt;/b&gt;다.&lt;/p&gt;
&lt;table style=&quot;height: 90px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 27px;&quot;&gt;
&lt;th style=&quot;width: 304px; height: 27px;&quot; align=&quot;left&quot;&gt;지표&lt;/th&gt;
&lt;th style=&quot;width: 97px; height: 27px;&quot; align=&quot;left&quot;&gt;누가 세나&lt;/th&gt;
&lt;th style=&quot;width: 110px; height: 27px;&quot; align=&quot;left&quot;&gt;세는 범위&lt;/th&gt;
&lt;th style=&quot;width: 343px; height: 27px;&quot; align=&quot;left&quot;&gt;빼는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 304px; height: 21px;&quot; align=&quot;left&quot;&gt;RSS&lt;/td&gt;
&lt;td style=&quot;width: 97px; height: 21px;&quot; align=&quot;left&quot;&gt;OS&lt;/td&gt;
&lt;td style=&quot;width: 110px; height: 21px;&quot; align=&quot;left&quot;&gt;프로세스 하나&lt;/td&gt;
&lt;td style=&quot;width: 343px; height: 21px;&quot; align=&quot;left&quot;&gt;없다. 매핑된 파일 페이지도 넣는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 304px; height: 42px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 97px; height: 42px;&quot; align=&quot;left&quot;&gt;커널 (cgroup)&lt;/td&gt;
&lt;td style=&quot;width: 110px; height: 42px;&quot; align=&quot;left&quot;&gt;cgroup 이 묶은 것 전부&lt;/td&gt;
&lt;td style=&quot;width: 343px; height: 42px;&quot; align=&quot;left&quot;&gt;안 쓰는(inactive) 파일 캐시를 뺀다. 계속 읽고 쓰는 활성 캐시는 남는다&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;범위는 &lt;code&gt;working_set&lt;/code&gt; 쪽을 키우고 뺄셈은 깎으니, 어느 쪽이 클지는 그때그때 다르다. Strimzi operator 는 뺄셈이 이겼다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;VmRSS                          312 MiB      프로세스 하나인데 더 크다
container working_set          296 MiB      cgroup 전체인데 더 작다&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;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;working_set&lt;/code&gt; 이 &lt;code&gt;limit&lt;/code&gt; 을 넘으면 커널은 그 cgroup 의 프로세스에 SIGKILL 을 보낸다. 힙인지 비힙인지, 산 객체의 자리인지 죽은 객체가 쓰던 자리인지 &amp;mdash; &lt;b&gt;안쪽 사정은 하나도 안 보고 총량 하나로 자른다.&lt;/b&gt; 바깥 선은 여기까지다. 이제 안쪽을 언어별로 본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;JVM &amp;mdash; limit 을 읽어 와서 선을 긋는다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5절이 남긴 수수께끼부터 푼다. booking 은 &lt;code&gt;-Xmx&lt;/code&gt; 를 안 적었는데 힙 상한이 768 Mi 로 서 있었다. &lt;b&gt;768 = &lt;code&gt;limit&lt;/code&gt; 1 Gi 의 75%&lt;/b&gt; 고, values 에 적어 둔 것은 &lt;code&gt;MaxRAMPercentage=75&lt;/code&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;경로는 이렇다. JVM 의 힙 상한 기본 규칙은 컨테이너 이전 시대의 &quot;물리 RAM 의 4분의 1&quot;이다. 컨테이너 안에서 그대로 두면 노드 8 GiB 의 4분의 1 인 2 GiB 를 잡으려 들고, &lt;code&gt;limit&lt;/code&gt; 512 MiB 컨테이너라면 커널이 먼저 죽인다.&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;그래서 JDK 10 부터 &lt;b&gt;JVM 이 cgroup 파일을 직접 읽어 &quot;물리 RAM&quot; 자리에 컨테이너 &lt;code&gt;limit&lt;/code&gt; 을 놓는다&lt;/b&gt;(&lt;code&gt;UseContainerSupport&lt;/code&gt;, 기본 켜짐). 그 위에 손잡이 셋이 얹힌다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;손잡이&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;무엇을 정하나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;&lt;code&gt;-Xmx&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙 상한을 절대값으로 명시. 있으면 아래 비율은 무시된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;&lt;code&gt;MaxRAMPercentage&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;limit&lt;/code&gt; 의 몇 % 를 힙 상한으로. 기본값 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;&lt;code&gt;-Xms&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;시작할 때 잡아 두는 힙 크기. &lt;code&gt;-Xmx&lt;/code&gt; 와 같게 두면 늘었다 줄었다 하는 요동이 없어진다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topicOperator 에 &lt;code&gt;-Xms 64m&lt;/code&gt;&amp;middot;&lt;code&gt;-Xmx 128m&lt;/code&gt; 을 함께 적어 둔 것이 이 짝이다. 시작은 64 에서 하고 상한은 128 이라 그 사이에서만 오르내리고, &lt;b&gt;그렇게 실제로 확보해 둔 양을 커밋이라 한다&lt;/b&gt; &amp;mdash; 커밋은 사용과 상한 사이에 있다.&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;나는 values 에서 비율 쪽을 두 군데 명시해 뒀다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Kafka 브로커   limit 1,536 Mi &amp;times; 60%  =  921.6 Mi   실측 MaxHeapSize 922.0 MiB
booking       limit 1 Gi &amp;times; 75%      =  768 Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;-Xmx&lt;/code&gt; 가 없으면 비율이 선을 긋고, 비율은 &lt;code&gt;limit&lt;/code&gt; 에서 파생된다.&lt;/b&gt; 5절 끝 질문의 반쪽이 여기서 답이 된다 &amp;mdash; &lt;b&gt;JVM 쪽은 &lt;code&gt;limit&lt;/code&gt; 을 바꾸면 안쪽 선이 따라 움직인다.&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;이 선에 닿으면 나는 일도 바깥 선과 다르다. 힙이 상한에 닿으면 JVM 은 GC 를 몰아 돌리고, 그래도 자리가 안 나면 &lt;code&gt;OutOfMemoryError&lt;/code&gt; 를 던진다 &amp;mdash; OOMKill 처럼 밖에서 죽임당하는 것이 아니라 &lt;b&gt;앱 안에서 나는 실패&lt;/b&gt;다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;비힙 &amp;mdash; 선을 그어도 limit 은 안 나온다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5절 끝 질문의 남은 반쪽 &amp;mdash; 안쪽 선을 다 정하면 &lt;code&gt;limit&lt;/code&gt; 이 지켜지나. &lt;b&gt;아니다.&lt;/b&gt; 커널이 재는 &lt;code&gt;working_set&lt;/code&gt; 은 컨테이너 전부를 세는데 힙 상한은 힙 한 칸에만 그은 선이라, 선 바깥 몫이 &lt;code&gt;limit&lt;/code&gt; 안에서 같이 산다. 이 몫을 비힙이라 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;비힙&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;무엇&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;무엇이 늘리나&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;상한 손잡이&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;메타스페이스&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;클래스 정보&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;로드한 클래스 수&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;MaxMetaspaceSize&lt;/code&gt; (기본 무제한)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;코드캐시&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;바이트코드를 기계어로 번역해 둔 것 (JIT)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;자주 도는 메서드 수&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;ReservedCodeCacheSize&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;스레드 스택&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;스레드마다 하나씩&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;스레드 수&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;-Xss&lt;/code&gt; &amp;times; 스레드 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;다이렉트 버퍼&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;네트워크&amp;middot;디스크 I/O 용 버퍼&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;I/O 량&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;MaxDirectMemorySize&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;GC 관리 구조&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;회수에 쓰는 자료구조&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙 크기에 비례&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스레드 스택 행은 OS 스레드에만 해당한다.&lt;/b&gt; booking 처럼 가상 스레드를 쓰면 그 스택은 힙 안에 놓여 &lt;code&gt;-Xmx&lt;/code&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;메타스페이스&amp;middot;코드캐시&amp;middot;스레드 스택&amp;middot;다이렉트 버퍼&amp;middot;GC 관리 구조는 모두 익명 메모리다. 힙과 같은 성질인데 &lt;code&gt;-Xmx&lt;/code&gt; 밖에 있다. JVM 셋을 떠 보면 이 몫이 작지 않다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;&lt;code&gt;limit&lt;/code&gt;&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;힙 상한 (limit 대비)&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;힙 사용&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;비힙 (잰 것만)&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;&lt;code&gt;working_set&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Strimzi operator&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;384 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;58 Mi (15%)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;52 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;메타스페이스 86 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;296 Mi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;booking&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1,024 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;768 Mi (75%)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;74 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;합 181 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;420 Mi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Kafka 브로커&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1,536 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;922 Mi (60%)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;325 Mi (커밋 389)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;메타스페이스 68 Mi&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;621-643 Mi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비힙 열이 고르지 않은 것은 잴 수 있는 것이 파드마다 달라서다. booking 은 앱이 JVM 지표를 내보내 다섯 조각의 &lt;b&gt;합&lt;/b&gt;이 나오고, Strimzi&amp;middot;Kafka 는 &lt;code&gt;jcmd&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;이 표에서 대 볼 것은 &lt;b&gt;힙 사용과 &lt;code&gt;working_set&lt;/code&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;Strimzi&lt;/b&gt; &amp;mdash; 힙은 52 를 쓰는데 컨테이너는 296 을 쓴다. 메타스페이스(86) 하나가 힙 상한(58)보다 크다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;booking&lt;/b&gt; &amp;mdash; 힙은 74, 컨테이너는 420. 힙 상한 768 은 이 420 의 대부분과 상관없는 선이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka&lt;/b&gt; &amp;mdash; 힙 커밋 389 에 메타스페이스 68 을 더해도 164-186 이 빈다(621-643 의 양 끝). &lt;code&gt;working_set&lt;/code&gt; 은 안 쓰는 파일 캐시만 빼므로 계속 읽고 쓰는 로그 세그먼트의 활성 캐시가 여기 남아 있고, JVM 네이티브 몫과 가르려면 &lt;code&gt;-XX:NativeMemoryTracking&lt;/code&gt; 을 켜야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;limit&lt;/code&gt; 을 향해 차오르는 것은 &lt;code&gt;working_set&lt;/code&gt; 전체인데, 안쪽 선이 다스리는 것은 그중 힙 한 칸뿐이다.&lt;/b&gt; 안쪽 선을 다 정해도 &lt;code&gt;limit&lt;/code&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;Kafka 브로커의 607 Mi도 힙 단독이 아니다.&lt;/b&gt; 나는 그 값에 &quot;JVM 힙&quot;이라는 이름을 붙였는데, 익명 메모리를 잰 값이라 힙과 비힙이 함께 들어 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;Go &amp;mdash; limit 을 아예 모른다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 런타임에는 cgroup 을 읽는 단계 자체가 없다. 힙 상한 기본값은 int64 최대 &amp;mdash; 사실상 무한이다. 두 배 선은 살아있는 양을 따라 올라가는 선이라, &lt;code&gt;limit&lt;/code&gt; 1 GiB 컨테이너 안에서 다음 GC 지점이 2 GiB 로 잡혀도 런타임은 이상한 줄 모른다. &lt;b&gt;Go 파드가 죽는 선은 언제나 바깥의 OOMKill 이다.&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;b&gt;&lt;code&gt;GOMEMLIMIT&lt;/code&gt;&lt;/b&gt;(Go 1.19부터)이다. JVM 처럼 런타임이 읽어 오는 것이 아니라 &lt;b&gt;&lt;code&gt;limit&lt;/code&gt; 을 보고 사람이 옮겨 적는 값&lt;/b&gt;이고, &lt;code&gt;-Xmx&lt;/code&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; 힙만이 아니라 &lt;b&gt;힙 + 고루틴 스택 + 런타임 메타데이터&lt;/b&gt;를 한 값으로 묶는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성격&lt;/b&gt; &amp;mdash; 상한이 아니라 &lt;b&gt;목표&lt;/b&gt;다. 넘어도 실패하지 않는다. 살아있는 양이 목표를 넘으면, 런타임이 목표를 맞추려고 GC 를 계속 세게 돌릴 뿐이다&lt;/li&gt;
&lt;/ul&gt;
&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;h5&gt;&lt;b&gt;사용자 수준 스레드 &amp;mdash; 그 스택은 어느 선이 세나&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출발은 OS 스레드에 붙는 값이다. 스레드는 커널이 만들고 커널이 굴리며, 하나마다 스택이 예약되고(JVM 은 &lt;code&gt;-Xss&lt;/code&gt; 1 MiB) 번갈아 돌릴 때마다 문맥 교환이 든다. 동시 요청 수백 개를 스레드 수백 개로 받으면 그 값이 그대로 쌓인다.&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;Go 와 Java 의 런타임은 같은 우회로를 냈다. &lt;b&gt;커널이 아니라 런타임 자신이 만들고 굴리는 실행 단위를 두고, OS 스레드 위에 얹어 돌린다.&lt;/b&gt; 이런 실행 단위를 &lt;b&gt;사용자 수준 스레드&lt;/b&gt;라 하고, 이 클러스터에는 둘 다 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;누가 만들고 굴리나&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;스택&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;고루틴 (Go)&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Go 런타임&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙 밖. 2 KiB 로 시작해 필요하면 늘린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;가상 스레드 (Java 21)&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;JVM&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙 안.&lt;/b&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;동작의 핵심은 &lt;b&gt;기다릴 때 비켜 주는 것&lt;/b&gt;이다. 고루틴이나 가상 스레드가 DB 응답 같은 것을 기다리게 되면, 런타임이 그것을 OS 스레드에서 떼어 내고 다른 것을 그 자리에 얹는다. 커널은 이 교체를 모른다. 그래서 OS 스레드는 CPU 코어 수 언저리면 되고, 실행 단위는 수만 개를 띄워도 스레드만큼 값이 안 든다.&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;queue&lt;/b&gt; &amp;mdash; 대기열 순번을 들고 폴링을 받는 Go 서비스. 연결마다 고루틴 하나를 붙인다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;booking&lt;/b&gt; &amp;mdash; &lt;code&gt;spring.threads.virtual.enabled=true&lt;/code&gt;. 요청마다 가상 스레드 하나를 쓴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 수준 스레드가 이 절의 두 선과 만나는 자리는 스택이다. &lt;b&gt;커널이 안 만드는 실행 단위라 스택도 커널이 안 잡아 주고, 런타임이 마련한 그 스택이 어느 값 안에서 세어지는지가 갈린다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;OS 스레드 스택      -Xss &amp;times; 스레드 수     힙 밖 &amp;mdash; 비힙으로 limit 안에 든다
가상 스레드 스택     -Xmx 안             JVM 이 스택을 힙 안에 객체로 두므로
고루틴 스택         GOMEMLIMIT 안        힙 밖이지만 목표가 같이 묶어 센다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1절에서 서로 어떻게 다른지 몰랐던 &lt;code&gt;-Xmx&lt;/code&gt; 와 &lt;code&gt;GOMEMLIMIT&lt;/code&gt; 이 힙만 정하는 값이 아니라는 뜻이다. booking 이 동시 요청을 늘리면 그 스택 몫은 비힙이 아니라 &lt;code&gt;-Xmx&lt;/code&gt; 안에서 늘고, queue 가 연결을 늘리면 &lt;code&gt;GOMEMLIMIT&lt;/code&gt; 이 그 몫을 같이 센다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;런타임이 없는 언어는 앱이 직접 건다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C&amp;middot;C++에는 &quot;언제 회수하나&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;Redis&lt;/b&gt; &amp;mdash; &lt;code&gt;maxmemory&lt;/code&gt;가 그 자리다. 안 걸면 무한이라 데이터가 쌓이는 만큼 자라다 컨테이너 &lt;code&gt;limit&lt;/code&gt;에서 끝난다. 걸어 두면 그 선에서 오래된 키를 버리거나 쓰기를 거절하고, 기본은 거절이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MySQL&lt;/b&gt; &amp;mdash; InnoDB 버퍼 풀이 JVM의 힙에 해당하는 자리인데 기본값이 128 MiB로 고정돼 있다. &lt;b&gt;미설정의 문제가 무한이 아니라 부족이다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앱이 직접 거는 상한은 런타임이 있는 쪽에도 있다.&lt;/b&gt; Loki는 Go 프로그램이지만 조회를 빠르게 하려고 프로세스 안에 캐시를 여러 개 두고, 그 캐시마다 크기 상한이 따로 있다. 그 상한들의 합은 컨테이너 &lt;code&gt;limit&lt;/code&gt;과 무관하게 정해진다. &lt;b&gt;런타임 손잡이와 앱 설정은 다른 축이다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;총정리 &amp;mdash; 질문의 답과, 다 선 낱말들&lt;/b&gt;&lt;/h5&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;&lt;code&gt;limit&lt;/code&gt; 을 바꾸면 안쪽 선이 따라 움직이는가 &amp;mdash; JVM 만 그렇다.&lt;/b&gt; JVM 은 cgroup 을 읽어 &lt;code&gt;limit&lt;/code&gt; &amp;times; 비율로 선을 긋기 때문이다(booking 75% &amp;rarr; 768 Mi). &lt;code&gt;-Xmx&lt;/code&gt; 로 절대값을 적으면 그 연결도 끊긴다. Go 는 처음부터 &lt;code&gt;limit&lt;/code&gt; 을 모르니, &lt;code&gt;limit&lt;/code&gt; 을 바꾸면 &lt;code&gt;GOMEMLIMIT&lt;/code&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;안쪽 선을 다 정하면 &lt;code&gt;limit&lt;/code&gt; 이 지켜지는가 &amp;mdash; 아니다.&lt;/b&gt; JVM 은 비힙이 선 밖에서 &lt;code&gt;limit&lt;/code&gt; 안을 채우고, Go 의 선은 목표라 강제가 아니고, 앱이 거는 상한은 아예 다른 축이다. &lt;b&gt;&lt;code&gt;limit&lt;/code&gt; 을 지킬 책임은 어느 런타임에도 없고, 그래서 마지막 선은 언제나 커널의 OOMKill 이다.&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;&amp;nbsp;&lt;/p&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 align=&quot;left&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Go&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;JVM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙에 무엇이 올라가나&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;컴파일러가 정한다 (새어 나가면 힙)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;객체 본체는 힙, 참조는 스택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;실행 단위&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;고루틴. 런타임이 굴리고 스택 2 KiB 시작&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OS 스레드(커널, &lt;code&gt;-Xss&lt;/code&gt; 1 MiB) 또는 가상 스레드(JVM, 스택은 힙 안)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙을 통으로 쓰나&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;통으로 쓴다&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Eden &amp;middot; Survivor &amp;middot; Tenured 로 나눈다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;GC 가 도는 시점&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙 지표가 살아있는 양의 두 배에 닿을 때 (&lt;code&gt;GOGC&lt;/code&gt;)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Eden 이 찰 때. Eden 크기는 &lt;code&gt;-Xmx&lt;/code&gt; 에서 파생된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙 상한 기본값&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;없다 (int64 최대)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;limit&lt;/code&gt; 의 25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;cgroup 을 읽나&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;안 읽는다&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;읽는다 (&lt;code&gt;UseContainerSupport&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;잇는 손잡이&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;GOMEMLIMIT&lt;/code&gt; &amp;mdash; 사람이 옮겨 적는 목표&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;-Xmx&lt;/code&gt;&amp;middot;&lt;code&gt;MaxRAMPercentage&lt;/code&gt; &amp;mdash; 상한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;손잡이가 덮는 범위&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙 + 고루틴 스택 + 런타임 메타&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;힙만 (가상 스레드 스택 포함). 비힙은 밖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;안쪽 선을 넘으면&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실패 없음 &amp;mdash; GC 만 세게 돈다&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;OutOfMemoryError&lt;/code&gt; &amp;mdash; 앱 안에서 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;바깥 선을 넘으면&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OOMKill&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OOMKill&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1절 표 앞에서 판단이 안 서던 낱말들이 전부 섰다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;1절의 낱말&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;지금&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;힙 &amp;middot; 스택&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;주소 공간의 조각. 스택은 스레드마다 하나에 프레임이 쌓였다 풀리고, 힙만 수명이 함수에 안 묶여 치우는 문제가 남는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;런타임&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;내가 안 짰는데 같은 프로세스에서 함께 도는 코드. 힙을 잡아 주고, 치우고, 실행 단위를 굴린다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;RSS&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;프로세스에 물리 페이지가 붙은 양. 힙 지표와의 폭은 죽은 객체가 쓰던 페이지와 반납 지연이 만든다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;GC &amp;middot; GOGC&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;참조가 안 남은 객체를 치우는 일 &amp;middot; 그 시점(두 배 선)의 배율. 낮추면 메모리가 줄고 CPU 를 더 쓴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;-Xms &amp;middot; -Xmx &amp;middot; MaxRAMPercentage&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;JVM 안쪽 선의 세 손잡이 &amp;mdash; 시작 크기 &amp;middot; 절대값 상한 &amp;middot; &lt;code&gt;limit&lt;/code&gt; 파생 비율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;GOMEMLIMIT&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Go 안쪽 선. 사람이 옮겨 적는 목표 &amp;mdash; 힙과 고루틴 스택과 런타임 메타를 묶는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;비힙&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;-Xmx&lt;/code&gt; 밖에서 &lt;code&gt;limit&lt;/code&gt; 안을 채우는 몫 &amp;mdash; 메타스페이스 &amp;middot; 코드캐시 &amp;middot; OS 스레드 스택 &amp;middot; 버퍼 &amp;middot; GC 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 일은 이 낱말로 AI 가 낸 표를 내 눈으로 다시 읽고 컨테이너마다 값을 정하는 것이다. &lt;b&gt;멈춰 뒀던 스펙 재산정으로 돌아간 후 클러스터 안정화 작업을 마친 뒤 부하테스인 12부를 진행한다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/225</guid>
      <comments>https://zed6740.tistory.com/225#entry225comment</comments>
      <pubDate>Mon, 17 Aug 2026 15:00:02 +0900</pubDate>
    </item>
    <item>
      <title>번외 &amp;mdash; 노드 메모리와 쿠버네티스 메모리</title>
      <link>https://zed6740.tistory.com/224</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 막힌 자리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11부에서 앱이 제대로 도는지 검증했고, 12부에서는 대시보드를 보면서 파드 스펙을 실측한다. queue와 booking에 메모리와 CPU를 얼마나 줘야 하는지를 부하로 재서 정하는 것이 목표다.&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;노트북 한 대에 노드 3대를 올린 클러스터라 자원이 정해져 있다.&lt;/b&gt; 지금 파드들에 걸린 &lt;code&gt;requests&lt;/code&gt;와 &lt;code&gt;limits&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 12부의 순서가 &lt;b&gt;인프라 대시보드 &amp;rarr; 앱 대시보드&lt;/b&gt;가 됐다. 이 글은 그중 앞쪽, 인프라 대시보드에서 노드 메모리를 분석하다 막힌 것을 정리한 기록이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;노드 한 대가 이렇게 나뉘어 있다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 노드는 3대이고 셋 다 물리 메모리가 같다. 한 대를 그리면 이렇다. (단위 MiB는 1,048,576 바이트다. 1000 단위인 MB와 다르고, 파드 스펙에 적는 &lt;code&gt;Mi&lt;/code&gt;와 같다.)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;k3s-1   물리 7,941 MiB
┌───────────────────────────────────────────────────────────────┐
│  파드가 도는 자리                              상한 5,381 MiB   │
│    지금 차 있는 양                                  3,671 MiB   │
│    스케줄러가 보고 판단하는 선                       5,081 MiB   │
├───────────────────────────────────────────────────────────────┤
│  시스템 몫 (파드에게 안 주고 남긴 것)            예약 2,560 MiB   │
│    지금 쓰는 양                                     2,235 MiB   │
└───────────────────────────────────────────────────────────────┘&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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;파드가 도는 자리   커널이 파드를 한 칸에 가둔다.  그 칸이 cgroup 이다   &amp;rarr; 3절
                 파드는 이 칸 밖으로 못 나간다

시스템 예약       kubelet &amp;middot; containerd &amp;middot; sshd 몫을 미리 뗀 것         &amp;rarr; 4절
                 상한 5,381 과 선 5,081 이 다른 이유도 거기서

지금 차 있는 양    이 숫자를 재는 것이 cAdvisor 다                     &amp;rarr; 3절&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;대시보드로 보면서 던지게 되는 질문&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;이 노드에 파드를 더 앉힐 수 있나
지금 쓰고 있는 것 중 어디까지가 파드 몫이고 어디부터가 시스템 몫인가
노드마다 떼어 둔 예약 2,560 MiB 를 줄일 수 있나&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node_*        prometheus-node-exporter
container_*   cAdvisor            kubelet 안에 있다
kubelet_*     kubelet 자신
kube_*        kube-state-metrics&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서는 층을 따라간다. 리눅스가 메모리를 다루는 방식, 그 위에 cgroup이 얹은 것, 다시 그 위에 쿠버네티스가 얹은 것. &lt;b&gt;층마다 그 층을 재는 곳이 하나씩 나온다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 리눅스 층 &amp;mdash; 커널이 도로 가져갈 수 있는 것과 없는 것&lt;/h2&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;회수(reclaim)&lt;/b&gt; 라 부르고, 이 글에서 계속 나오는 말이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;회수할 수 있는지는 그 메모리가 어디서 왔느냐로 갈린다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 커널에게 하는 요청이 두 가지이고, 결과가 각각 다른 종류로 남는다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;① 메모리를 달라고 한다
   &amp;rarr; 커널이 빈 자리를 준다
   &amp;rarr; 프로그램이 거기에 계산 결과를 넣는다              = 익명 메모리
      그 값은 이 프로그램이 만든 것이라 디스크 어디에도 없다
      커널이 가져오면 없어진다                       
      
      &amp;rarr; 회수 못 한다

② 파일을 읽어 달라고 한다
   &amp;rarr; 커널이 디스크에서 읽어 메모리에 복사한다
   &amp;rarr; 프로그램이 그 사본을 읽는다
   &amp;rarr; 다 읽은 뒤에도 커널이 사본을 안 지운다            = 페이지 캐시
      원본이 디스크에 그대로 있다
      커널이 가져와도 다시 읽으면 된다                 
      
      &amp;rarr; 회수할 수 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;②에서 복사 단계가 빠질 수 없는 이유는 CPU가 디스크를 직접 못 읽기 때문이다. 커널이 먼저 메모리로 올려야 프로그램이 그 내용을 쓴다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;익명 메모리   Kafka 브로커 JVM 힙 607 &amp;middot; MySQL InnoDB 버퍼 풀 433
             Mimir 가 들고 있는 최근 시리즈 401 &amp;middot; k3s.service 1,708

페이지 캐시   Mimir 가 디스크에 쓴 블록 파일 358 &amp;middot; Alloy 195
             카프카 로그 세그먼트 159 &amp;middot; 컨테이너 이미지 레이어 &amp;middot; journald 로그&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;같은 파드가 양쪽에 다 나온다.&lt;/b&gt; &lt;code&gt;mimir-ingester-0&lt;/code&gt;은 최근 데이터를 메모리에 들고 있고(익명 401), 자기가 디스크에 쓴 블록 파일을 읽으면 그 사본이 남는다(캐시 358).&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;커널은 캐시를 안 지우고 쌓아 둔다&lt;/b&gt;&lt;/h5&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;커널이 이 상태를 파일&lt;/b&gt;&lt;b&gt; 하나에 적어 둔다 &amp;mdash; /proc/meminfo&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 나온 값들을 커널이 &lt;code&gt;/proc/meminfo&lt;/code&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;여기 적힌 항목 이름이 그대로 메트릭 이름이 된다.&lt;/b&gt; k3s-1에서 읽으면 50줄쯤 나오는데, 이 글에 걸리는 것만 뽑으면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;$ cat /proc/meminfo

MemTotal:        8131780 kB      7,941 MiB   물리 전체
MemFree:          241060 kB        235       아무것도 안 담기지 않은 자리
MemAvailable:    3265556 kB      3,189       커널이 계산한 여유
AnonPages:       4365980 kB      4,264       익명 메모리      회수 못 한다
Cached:          3019452 kB      2,949       페이지 캐시      회수할 수 있다
Buffers:          201788 kB        197       블록 장치 메타데이터
SReclaimable:      78256 kB         76       커널 자료구조 중 회수 가능분&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;단위가 kB라 1024로 나눠야 MiB가 된다. 나눈 값을 오른쪽에 적어 뒀고, 이 글의 숫자는 전부 MiB다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;AnonPages 4,264 + Cached 2,949 + Buffers 197 + MemFree 235 = 7,645
MemTotal 7,941 &amp;minus; 7,645 = 296   커널 자신이 쓰는 자료구조
                               Slab 178 &amp;middot; PageTables 26 &amp;middot; KernelStack 15 &amp;hellip;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;커널은 회수할 차례를 미리 정해 둔다 &amp;mdash; Active 와 Inactive&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 출력에 &lt;code&gt;Active&lt;/code&gt;와 &lt;code&gt;Inactive&lt;/code&gt;가 있고 그 아래 &lt;code&gt;(anon)&lt;/code&gt;&amp;middot;&lt;code&gt;(file)&lt;/code&gt;로 또 갈린다. 메모리가 모자란 순간에 무엇을 가져갈지 뒤지면 늦으니, &lt;b&gt;평소에 목록을 만들어 두고 회수할 때 앞에서부터 집는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Active     최근에 쓰였다.     나중에 가져간다
Inactive   한동안 안 쓰였다.  먼저 가져간다

처음 읽으면 Inactive &amp;rarr; 다시 읽히면 Active &amp;rarr; 안 읽히면 되돌아온다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회수 후보가 될 수 있는 것은 종류와 상관없이 전부 이 목록에 들어간다. 그래서 2&amp;times;2가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                       Active      Inactive
익명 메모리 (anon)        3,928         323
페이지 캐시 (file)        1,649       1,482
                        ─────       ─────
                        5,577       1,805   출력의 Active &amp;middot; Inactive 와 맞는다&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;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;code&gt;Active(file)&lt;/code&gt; 1,649와 &lt;code&gt;Inactive(file)&lt;/code&gt; 1,482뿐이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익명 메모리는&amp;nbsp;&lt;code&gt;Inactive(anon)&lt;/code&gt; 323은 목록에 올라 있어도 못 가져간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;커널이 계산해 주는 여유 &amp;mdash; MemAvailable&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;지금 새 프로그램을 띄우면 얼마까지 받을 수 있나&quot;에 답하려면&lt;b&gt; 비어 있는 자리와 회수할 수 있는 것을 더해야 한다.&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;b&gt;MemAvailable = MemFree + 페이지 캐시(Active+InActive) + ~~ = 회수 가능한 사용 가능한 값&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;b&gt;&lt;code&gt;MemAvailable&lt;/code&gt;&lt;/b&gt;이고 값이 3,189다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;MemFree            235     지금 비어 있는 자리
파일 캐시         3,131     Active(file) 1,649 + Inactive(file) 1,482
SReclaimable        76     커널 자료구조 중 버릴 수 있는 것
                 ─────
                 3,442     다 긁어모으면
MemAvailable     3,189     커널이 내놓은 답
                 ─────
                   253     안전분으로 남겨 둔 몫&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Active(file)&lt;/code&gt;와 &lt;code&gt;Inactive(file)&lt;/code&gt;가 똑같이 더해진다.&lt;/b&gt; 버리는 순서만 다를 뿐 커널은 둘 다 &quot;버려도 되는 것&quot;으로 센다. 이 구분이 갈리는 자리는 3절이다.&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;안전분 253을 빼는 이유는 여기까지 내려가면 커널이 메모리 만드는 일에 매달려 시스템이 느려지기 때문이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;이것이 node-exporter 가 내는 값이다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;node-exporter&lt;/b&gt;&lt;/span&gt;는 &lt;b&gt;리눅스 서버 한 대를 감시할 때 쓰는 표준 도구&lt;/b&gt;다. 쿠버네티스와 무관하게 서버가 있으면 붙여서 &lt;b&gt;CPU&amp;middot;메모리&amp;middot;디스크&amp;middot;네트워크&amp;middot;파일시스템을 본다.&lt;/b&gt; 커널이 &lt;code&gt;/proc&lt;/code&gt;과 &lt;code&gt;/sys&lt;/code&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;code&gt;/proc/meminfo&lt;/code&gt;에 적어 둔 것이고, 그 파일을 읽어 Mimir로 보내는 것이 &lt;b&gt;node-exporter&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;b&gt; &lt;b&gt;node-exporter는&lt;/b&gt;&amp;nbsp;컨테이너 안에서 도는데 호스트 파일을 읽는다.&lt;/b&gt; &lt;b&gt;또한 DaemonSet으로 노드마다 파드가 하나씩 뜨고, 그 파드에 호스트가 통째로 붙어 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;hostPID: true       호스트의 프로세스 이름공간을 그대로 쓴다
proc  -&amp;gt; /proc      호스트의 /proc 를 컨테이너 안에 붙였다
sys   -&amp;gt; /sys
root  -&amp;gt; /&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 보통 자기 안쪽만 본다. &lt;b&gt;node-exporter는 호스트 파일을 붙여서 노드 전체를 본다.&lt;/b&gt; 파드로 떠 있으면서도 노드 값을 내는 것이 그래서다. 이 클러스터에서 &lt;code&gt;observability-host&lt;/code&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;하는 일은 읽어서 형식만 바꾸는 것이다.&lt;/b&gt; 세는 것도 계산도 커널이 한다. 항목 이름 앞에 &lt;b&gt;&lt;code&gt;node_memory_&lt;/code&gt;&lt;/b&gt;가 붙고 뒤에 &lt;b&gt;&lt;code&gt;_bytes&lt;/code&gt;&lt;/b&gt;가 붙어서, &lt;code&gt;MemAvailable&lt;/code&gt;은 &lt;b&gt;&lt;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt;&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;b&gt;이 값이 필요한 이유&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;에 컨테이너별 항목이 없으니 낼 수도 없다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;답할 수 있다   이 노드에 지금 얼마까지 더 줄 수 있나        MemAvailable 3,189
답 못 한다     이 파드가 얼마 쓰나
              queue 가 자기 상한 256Mi 를 넘었나&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그런데 노드 한 대에 파드가 여럿 산다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s-1에는 queue&amp;middot;booking&amp;middot;MySQL&amp;middot;Kafka&amp;middot;Mimir가 같이 앉아 있다. 그중 하나가 메모리를 계속 먹으면 노드가 꽉 차고, 그 상태로 두면 커널이 프로세스를 골라 죽인다. &lt;b&gt;원인을 만든 파드가 아니라 멀쩡한 파드가 죽을 수 있다.&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;그래서 쿠버네티스는 파드마다 &quot;여기까지&quot;를 정해 두고 넘은 파드만 멈춘다. 그러려면 둘이 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;파드별로 얼마 쓰는지 센다      누가 넘었는지 알아야 고른다
그 파드만 멈춘다             나머지는 살려야 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;node-exporter로는 둘 다 안 된다.&lt;/b&gt; 노드 전체 합만 내므로 &lt;code&gt;MemAvailable&lt;/code&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;이 둘을 다 하는 기능이 이미 있다. 쿠버네티스가 그것을 가져다 쓴다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. cgroup 층 &amp;mdash; 프로세스 묶음별로 세고 상한을 건다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한&lt;b&gt; 파드를 다루는 리눅스에서의 기능&lt;/b&gt;이&lt;b&gt; cgroup(control group)&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;다만 cgroup은 리눅스 기능이라 파드를 모른다. &lt;b&gt;아는 것은 프로세스뿐이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;프로세스를 묶는다
그 묶음이 쓴 양을 따로 센다
상한을 걸고, 넘으면 그 안의 프로세스를 죽인다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드를 이 위에 얹을 수 있는 이유는&lt;b&gt; &quot;파드도 결국 프로세스 묶음&quot;이기 때문이다.&lt;/b&gt; 컨테이너가 프로세스이고 파드는 그 컨테이너들을 묶은 것이라, cgroup이 원래 하던 일에 그대로 들어맞는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;컨테이너가 곧 cgroup 이다&lt;/b&gt;&lt;/h5&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;cgroup      얼마나 쓸 수 있나
              메모리 &amp;middot; CPU &amp;middot; 디스크 I/O

namespace   무엇이 보이나.  종류별로 따로 있다
              pid   다른 프로세스가 안 보인다.  ps 를 쳐도 자기 것만 나온다
              net   자기 IP &amp;middot; 포트 &amp;middot; 라우팅을 갖는다
              mnt   자기 파일시스템만 보인다.  호스트의 / 가 안 보인다
              uts   호스트명을 따로 갖는다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 쓰는 &lt;b&gt;namespace&lt;/b&gt;는 리눅스 커널 기능이고, &lt;b&gt;쿠버네티스의 Namespace(&lt;code&gt;app&lt;/code&gt;&amp;middot;&lt;code&gt;data&lt;/code&gt;&amp;middot;&lt;code&gt;observability&lt;/code&gt;)와는 이름만 같고 관계가 없다.&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubelet 이 파드 하나를 띄우기로 한다
  &amp;rarr; containerd 에게 그 안의 컨테이너를 만들라고 한다
  &amp;rarr; containerd 가 cgroup 을 만들고 상한을 적는다      queue 컨테이너는 256Mi
  &amp;rarr; namespace 를 만들어 시야를 가린다                 pid &amp;middot; net &amp;middot; mnt &amp;hellip;
  &amp;rarr; 그 안에서 프로세스를 실행한다
  &amp;rarr; 그 프로세스가 곧 컨테이너다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 network namespace 는 컨테이너마다가 아니라 &lt;b&gt;파드에 하나&lt;/b&gt;다. 왜 그런지가 바로 아래 &lt;code&gt;pause&lt;/code&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;쿠버네티스가 새로 만든 격리 장치가 있는 게 아니라 리눅스가 원래 갖고 있던 것을 쓴다.&lt;/b&gt; 파드 스펙에 적는 &lt;code&gt;limits&lt;/code&gt; 가 cgroup 상한으로 그대로 옮겨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;cgroup 은 계층이고, 층마다 상한이 걸린다&lt;/b&gt;&lt;/h5&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAEIBRAE&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-subtree=&quot;aimfl&quot;&gt;&lt;b&gt;cgroup&lt;/b&gt;은 프로세스마다 생기는 폴더가 아니라, &lt;b&gt;개별 프로세스들을 묶어서 관리하는 계층형 폴더 구조&lt;/b&gt;이다.&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAEIBRAH&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;개별 프로세스에 256Mi씩 상한을 건다면 파드가 30개로 늘면 전체 합이 노드 용량을 넘길 수 있기에, 개별 상한만으로는 전체 합을 정할 수 없다.&lt;/span&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAEIBRAK&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;그래서 cgroup은 '&lt;b&gt;묶음 위에 다시 상위 묶음&lt;/b&gt;'을 두는 계층 구조로 되어 있다. 노드에는 '파드 전체 묶음 폴더(&lt;b&gt;kubepods&lt;/b&gt;)'가 맨 위에 있고 그 아래에 개별 파드 폴더들이 들어간다.&lt;/span&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAEIBRAK&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;정리하면 리눅스 cgroup의 &lt;/span&gt;&lt;b&gt;계층형 폴더 구조&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;를 활용하여, 노드 위 파드들의 &lt;/span&gt;&lt;b&gt;자원 사용량을 그룹 단위로 제한한 것.&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/                            루트.  노드의 모든 프로세스
├─ /kubepods.slice           파드 전부
│   ├─ memory.stat             내역.  anon &amp;middot; file &amp;middot; active &amp;middot; inactive
│   ├─ memory.current          지금 쓰는 총량
│   ├─ memory.max              넘을 수 없는 선.  파드에게 내주는 총량
│   └─ 파드 하나               상한 = 그 안 limits 합
│       ├─ pause 컨테이너
│       └─ 앱 컨테이너          상한 = limits.  queue 는 256Mi
└─ /system.slice             systemd 서비스.  상한 없음
    ├─ k3s.service             apiserver &amp;middot; etcd &amp;middot; kubelet &amp;middot; containerd
    │                            &amp;rarr; kube-reserved 2,048 로 떼어 둔 몫
    └─ sshd.service 등          그 외 OS 서비스
                                 &amp;rarr; system-reserved 512 로 떼어 둔 몫&lt;/code&gt;&lt;/pre&gt;
&lt;div data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAE&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;쿠버네티스 클러스터 생성 시에 작성한&lt;b&gt; config.yaml&lt;/b&gt;에 적은 예약 두 줄이 겨냥하는 대상이 바로 이 system.slice 가지다. 다만 예약이 여기 상한으로 직접 걸리는 것은 아니다.&lt;/span&gt;&lt;/span&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-sfc-inited=&quot;2&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-wiz-uids=&quot;BjSFff_14,BjSFff_15,BjSFff_16&quot; data-sfc-root=&quot;ep&quot;&gt;&lt;span data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAE&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EXVxl/dJMcagT9DVl/JoqHEYkw8OLKAKvdWnFtT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EXVxl/dJMcagT9DVl/JoqHEYkw8OLKAKvdWnFtT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EXVxl/dJMcagT9DVl/JoqHEYkw8OLKAKvdWnFtT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEXVxl%2FdJMcagT9DVl%2FJoqHEYkw8OLKAKvdWnFtT0%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;819&quot; height=&quot;374&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAE&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*쿠버네티스 config.yaml&lt;/b&gt; : &quot;이 클러스터는 어떤 규칙과 자원 배정으로 운영하겠다&quot;라고 작성하는 &lt;b&gt;기반 설정 파일&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAH&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;이 자원 제어의 핵심은 모든 계층이 동일한 도구를 쓴다는 점이다. &lt;b&gt;세 파일(memory~)&lt;/b&gt;은 /kubepods.slice(파드를 다루는 층)에만 있는 것이 아니라 모든 층에 똑같이 들어 있다. 최상위 루트 폴더부터 파드 디렉터리, 컨테이너 디렉터리에 이르기까지 전부 존재하며 감시하는 범위만 다르다.&lt;span data-sfc-inited=&quot;2&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-wiz-uids=&quot;BjSFff_1b,BjSFff_1c,BjSFff_1d&quot; data-sfc-root=&quot;ep&quot;&gt;&lt;span data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAK&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;이 구조 덕분에 바깥 층이 안쪽 층의 합을 막을 수 있게 된다. 개별 컨테이너 하나하나가 아무리 얌전하게 자원을 써도, 상위 묶음인 /kubepods.slice의 상한에서 전체가 걸리기 때문이다. 그 상한선 값이 얼마이고 누가 이 파일에 값을 적는지는 이 절 뒤에서 이어진다.&lt;span data-sfc-inited=&quot;2&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-wiz-uids=&quot;BjSFff_1i,BjSFff_1j,BjSFff_1k&quot; data-sfc-root=&quot;ep&quot;&gt;&lt;span data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAN&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;이 /kubepods.slice 아래의 파드 디렉터리를 보면&lt;b&gt; pause라는 컨테이너가 파드마다 자동으로 붙어 있다.&lt;/b&gt; &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAN&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;파드 안 컨테이너들은 network namespace를 공유&lt;/b&gt;해&lt;b&gt; 같은 IP를 쓰고 localhost로 서로를 부른다.&lt;/b&gt; 이때 앱 컨테이너가 죽었다 떠도 파드의 IP가 유지되려면 누군가 그 namespace를 끊임없이 붙들고 있어야 한다. &lt;/span&gt;&lt;/span&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;그 일만 전담하는 것이&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; pause 컨테이너&lt;/span&gt;&lt;/b&gt;이며, 이 역시 하나의 프로세스라 메모리도 몇 MiB 차지한다. 이 미세한 몫이 계산에 미치는 영향은 다시 나온다.&lt;span data-sfc-inited=&quot;2&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-wiz-uids=&quot;BjSFff_1p,BjSFff_1q,BjSFff_1r&quot; data-sfc-root=&quot;ep&quot;&gt;&lt;span data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;여기서 파드 몫과 시스템 몫이 갈린다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;/kubepods.slice&lt;/code&gt;가 파드 전부를 담는 층&lt;/b&gt;이므로, 그 안과 밖이 곧 파드 몫과 시스템 몫이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;파드 몫    /kubepods.slice
시스템 몫   루트 전체 &amp;minus; /kubepods.slice&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;cgroup이 메모리를 새로 분류하는 것은 아니다.&lt;/b&gt; 익명이냐 캐시냐는 그대로 두고, 거기에 &lt;b&gt;누가 쓰느냐&lt;/b&gt;는 축을 하나 더한다. 그래서 두 축이 교차한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                 노드 전체     파드      시스템
익명 메모리         4,264     2,503     1,761
페이지 캐시         3,146     2,046     1,100

세로   익명이냐 페이지 캐시냐       커널이 가른다.  회수 가능 여부가 갈린다
가로   파드가 쓰는 것이냐 아니냐     /kubepods.slice 안이냐 밖이냐&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 페이지 캐시 3,146은 2절에서 본 &lt;code&gt;Cached&lt;/code&gt; 2,949에 &lt;code&gt;Buffers&lt;/code&gt; 197을 더한 값이다. &lt;b&gt;cgroup은 둘을 &lt;code&gt;file&lt;/code&gt; 하나로 센다.&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;b&gt;&lt;code&gt;k3s.service&lt;/code&gt;가 시스템 쪽에 있는 이유&lt;/b&gt;도 여기 있다. &lt;b&gt;파드가 아니라 노드에서 직접 도는 프로세스라 &lt;code&gt;/kubepods.slice&lt;/code&gt; 밖이다.&lt;/b&gt; 시스템 익명 메모리 1,761 중 대부분이 이 프로세스 하나다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;cgroup 이 세는 값 &amp;mdash; 분류는 노드 층과 같다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cgroup 하나가 디렉터리 하나다.&lt;/b&gt; 위의 트리는 개념도가 아니라 &lt;code&gt;/sys/fs/cgroup&lt;/code&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;&lt;code&gt;/proc/meminfo&lt;/code&gt;가 노드 전체를 익명과 캐시로 나눴듯, cgroup도 자기 범위를 같은 항목(익명 / 캐시)으로 나눈다.&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;b&gt;&lt;code&gt;memory.stat&lt;/code&gt;&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ cat /sys/fs/cgroup/kubepods.slice/memory.stat

anon      2624626688      파드 전부의 익명 메모리      2,503 MiB
file      2145509376      파드 전부의 페이지 캐시      2,046
kernel      76140544      커널 자료구조                  73
...                       inactive_file &amp;middot; active_file 이 이어진다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AnonPages&lt;/code&gt;&amp;middot;&lt;code&gt;Cached&lt;/code&gt;가 &lt;code&gt;anon&lt;/code&gt;&amp;middot;&lt;code&gt;file&lt;/code&gt;로 이름만 바뀌었고 &lt;b&gt;범위가 파드 전부로 좁혀진 것&lt;/b&gt;이다. 이 파일이 층마다 있어서, 컨테이너 cgroup에서 읽으면 그 컨테이너 하나의 익명과 캐시가 나온다.&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;code&gt;file&lt;/code&gt;이 그만큼 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;노드 층에 없는 것 &amp;mdash; 상한&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memory.stat&lt;/code&gt;이 내역이라면, &lt;b&gt;총량과 상한은 파일이 따로&lt;/b&gt; 있다. &lt;b&gt;성격이 다르다 &amp;mdash; 하나는 측정이고 하나는 선언이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memory.current   파드 전부가 지금 쓰는 총량      4,622 MiB
                 측정.  커널이 센다.  계속 움직인다
                 = stat 의 합.  anon 2,503 + file 2,046 + kernel 73

memory.max       파드 전부가 넘을 수 없는 선     5,381 MiB
                 선언.  kubelet 이 적는다.  고정
                 = 물리 7,941 &amp;minus; 예약 2,560&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;memory.max&lt;/code&gt;의 뺄셈에 들어가는 예약 2,560&lt;/b&gt;이 &lt;b&gt;노드 &lt;code&gt;config.yaml&lt;/code&gt;에 직접 적은 그 값(= /system.slice)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;config.yaml       kube-reserved=memory=2Gi &amp;middot; system-reserved=memory=512Mi
  &amp;rarr; kubelet 이 부팅하며 읽고 계산한다        7,941 &amp;minus; 2,048 &amp;minus; 512 = 5,381
  &amp;rarr; /sys/fs/cgroup/kubepods.slice/memory.max 에 쓴다
  &amp;rarr; 커널이 그 선을 강제한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YAML에 적은 숫자가 커널 파일까지 내려간 것이다.&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;code&gt;memory.max&lt;/code&gt;가 노드 층과 갈리는 지점이기도 하다.&lt;/b&gt; 노드에는 &quot;여기까지&quot;라는 파일이 없다. 물리 메모리가 곧 한계이고, 다 차면 커널이 알아서 회수하거나 죽인다. cgroup에는 그 선이 파일로 적혀 있고, 넘는 순간 &lt;b&gt;그 cgroup 안에서만&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;컨테이너 cgroup의 &lt;code&gt;memory.max&lt;/code&gt;도 같은 방식이고 적는 쪽만 다르다. &lt;code&gt;/kubepods.slice&lt;/code&gt;는 kubelet이, 컨테이너 것은 containerd가 적는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그런데 커널은 이 둘을 그대로 비교하지 않는다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;memory.current&lt;/code&gt; 4,622&lt;/b&gt;를 &lt;b&gt;&lt;code&gt;memory.max&lt;/code&gt; 5,381&lt;/b&gt;과 그대로 견주면 &lt;b&gt;파드 전부가 상한의 86%까지 찬 것&lt;/b&gt;이 된다. 남은 자리가 759 MiB니 꽤나 용량이 부족한 것처럼 보인다.&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;파드 현재 총 사용량(current, 4,622)&lt;/b&gt; 안에는 &lt;b&gt;파드들의 페이지 캐시(file, 2,046)&lt;/b&gt;가 들어 있다. &lt;b&gt;커널이 언제든 가져갈 수 있는 몫이다.(노드에서처럼)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memory.current 가 memory.max 5,381 에 닿는다
  &amp;rarr; 커널이 그 cgroup 의 페이지 캐시부터 회수한다      2,046 이 줄어든다
  &amp;rarr; 자리가 생기면 파드는 그대로 계속 돈다
  &amp;rarr; 회수해도 안 줄어들 때만 죽인다                   익명 메모리는 못 줄이니까&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;&quot;캐시부터 회수한다&quot;는 것은 그 cgroup의 &lt;code&gt;file&lt;/code&gt;(페이지 캐시)2,046을 가져간다는 뜻&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;b&gt;제일 중요한 건 그 안에서도 순서가 있다.&lt;/b&gt; 노드 전체를 &lt;code&gt;Active&lt;/code&gt;&amp;middot;&lt;code&gt;Inactive&lt;/code&gt;로 갈라 두었듯 &lt;b&gt;cgroup 안에서도 같은 구분이 있으며, inactive_file 즉, 한동안 안 읽힌 페이지 캐시를 커널이 먼저 가져간다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memory.stat 의 file 2,046      페이지 캐시
  ├─ inactive_file   한동안 안 읽힌 것.  커널이 먼저 가져간다
  └─ active_file     최근에 읽힌 것.    그래도 모자랄 때 가져간다

memory.stat 의 anon 2,503      익명 메모리
  ├─ inactive_anon   같은 구분이 여기도 있다
  └─ active_anon     그런데 이 노드에서는 둘 다 못 가져간다
                     버리면 값이 사라지고, 내보낼 자리가 없다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;죽고 사는 것을 가르는 값은 &lt;code&gt;memory.current&lt;/code&gt;가 아니다.&lt;/b&gt; 그 안에 &lt;b&gt;커널이 가져갈 몫(페이지 캐시의 inactive_file 우선)&lt;/b&gt;이 섞여 있는 한, 상한에 닿아도 회수로 넘어간다. &lt;b&gt;&quot;지금 얼마나 위험한가&quot;를 알려면 사라질 것을 미리 뺀 값이 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그 값이 working_set 이다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memory.current&lt;/code&gt;에서 &lt;b&gt;커널이 첫 번째로 가져갈 몫(페이지 캐시 - inactive_file)만 뺀다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;working_set = memory.current &amp;minus; inactive_file
            = 익명 메모리 anon
            + 페이지 캐시 중 active_file
            + 커널 자료 kernel&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;회수했을 때 이 파드가 손해를 보느냐&lt;/b&gt;다.&lt;br /&gt;&lt;br /&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;b&gt;커널이 회수하면&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;working_set 에서&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;inactive_file&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아무 일 없다. 곧 없어질 몫이다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;뺀다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;active_file&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;그 파드가 디스크를 다시 읽는다&lt;/td&gt;
&lt;td&gt;남긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;anon&lt;/code&gt;&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;code&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;working_set&lt;/span&gt;&lt;/code&gt;&lt;/b&gt;&lt;/span&gt;은&lt;span style=&quot;color: #ee2323;&quot;&gt; &lt;b&gt;이 파드가 손해 없이는 못 놓는 양&lt;/b&gt;&lt;/span&gt;이다. 이 기준으로 파드 전부를 다시 재면 3,671이고, 같은 순간의 세 값이 이렇게 벌어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memory.current  4,622  ████████████████████     86%   캐시까지 센 값
working_set     3,671  ██████████████           68%   판정에 쓰는 값
memory.max      5,381  └───────────────────┘          넘을 수 없는 선&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memory.max &amp;minus; memory.current    5,381 &amp;minus; 4,622 =   759
memory.max &amp;minus; working_set       5,381 &amp;minus; 3,671 = 1,710&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앞쪽은 951을 적게 잡는다.&lt;/b&gt; &lt;code&gt;current&lt;/code&gt; 안의 &lt;code&gt;inactive_file&lt;/code&gt;은 파드가 더 요청하면 커널이 버리고 내줄 자리인데, 이미 쓰이는 것처럼 세기 때문이다. &lt;code&gt;memory.current&lt;/code&gt;의 쓰임은 여기까지이고 &lt;b&gt;뒤로는 &lt;code&gt;working_set&lt;/code&gt;만 쓴다.&lt;/b&gt;&lt;/p&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;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;MemAvailable 은 노드 단위, working_set 은 파드 단위&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2절에도 캐시를 다루는 계산이 하나 있었다. &lt;b&gt;&lt;code&gt;MemAvailable&lt;/code&gt;&lt;/b&gt;이다. 둘을 나란히 놓으면 이렇다.&lt;/p&gt;
&lt;table style=&quot;height: 109px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;th style=&quot;width: 174px; height: 25px;&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th style=&quot;width: 155px; height: 25px;&quot;&gt;무엇을 재나&lt;/th&gt;
&lt;th style=&quot;width: 147px; height: 25px;&quot;&gt;범위&lt;/th&gt;
&lt;th style=&quot;width: 378px; height: 25px;&quot;&gt;계산식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 174px; height: 42px;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;code&gt;MemAvailable&lt;/code&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 155px; height: 42px;&quot;&gt;&lt;b&gt;지금 더 줄 수 있는 양&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 147px; height: 42px;&quot;&gt;&lt;b&gt;노드 전체&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 378px; height: 42px;&quot;&gt;&lt;b&gt;&lt;code&gt;MemFree&lt;/code&gt; + &lt;br /&gt;페이지 캐시([&lt;code&gt;Active(file)&lt;/code&gt; + &lt;code&gt;Inactive(file)&lt;/code&gt;] + &lt;br /&gt;&lt;code&gt;SReclaimable&lt;/code&gt; &amp;minus; 안전분&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 174px; height: 42px;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;code&gt;working_set&lt;/code&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 155px; height: 42px;&quot;&gt;&lt;b&gt;지금 실제로 쓰는 양&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 147px; height: 42px;&quot;&gt;&lt;b&gt;&lt;code&gt;/kubepods.slice&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 378px; height: 42px;&quot;&gt;&lt;b&gt;&lt;code&gt;memory.current&lt;/code&gt; &amp;minus; &lt;code&gt;inactive_file(file)&lt;/code&gt;&lt;/b&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;식에 나오는 &lt;code&gt;Inactive(file)&lt;/code&gt;과 &lt;code&gt;inactive_file&lt;/code&gt;은 같은 것을 가리킨다. &lt;b&gt;둘 다 페이지 캐시이고, &lt;code&gt;/proc/meminfo&lt;/code&gt;가 쓰는 표기와 cgroup의 &lt;code&gt;memory.stat&lt;/code&gt;이 쓰는 표기가 다를 뿐&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;b&gt;&lt;code&gt;MemAvailable&lt;/code&gt;은 노드 단위 값이다.&lt;/b&gt; 노드 전체에서 지금 더 내줄 수 있는 양이고, 회수할 수 있는 것은 전부 여유로 센다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;working_set&lt;/code&gt;은 cgroup 단위 값이다.&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;b&gt;묻는 것이 다르다.&lt;/b&gt; 그래서 한 계산식 안에 섞으면 안 되고, 6절에서 고칠 것이 그 자리다.&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;code&gt;working_set&lt;/code&gt; 쪽&lt;/b&gt;이다. 그 값을 어디서 얻는지가 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;이것이 cAdvisor 가 내는 값이다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;working_set&lt;/code&gt;&lt;/b&gt;은 cgroup 파일에 적혀 있지 않다. &lt;b&gt;&lt;code&gt;memory.current&lt;/code&gt;와 &lt;code&gt;memory.stat&lt;/code&gt;을 읽어 계산&lt;/b&gt;해야 나오고, &lt;b&gt;그 계산을 해서 Mimir로 보내는 것이 cAdvisor&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;b&gt;컨테이너별 자원 사용량을 재려고 구글이 만든 것&lt;/b&gt;을 &lt;b&gt;쿠버네티스가 kubelet 안에 넣었다.&lt;/b&gt; 그래서 &lt;b&gt;별도 파드로 뜨지 않고&lt;/b&gt; 노드의 &lt;code&gt;:10250/metrics/cadvisor&lt;/code&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;&lt;code&gt;container_&lt;/code&gt;&lt;/b&gt;가 붙고, &lt;b&gt;읽어 온 cgroup 파일이 그대로 메트릭이 된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;memory.stat 의 anon             &amp;rarr;  container_memory_rss
memory.stat 의 file             &amp;rarr;  container_memory_cache
memory.max                      &amp;rarr;  container_spec_memory_limit_bytes
memory.current &amp;minus; inactive_file  &amp;rarr;  container_memory_working_set_bytes&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 셋은 숫자를 형식만 바꿔 옮긴 것이고, &lt;b&gt;마지막 하나만 계산해서 만든다.&lt;/b&gt; 공짜로 회수될 몫(&lt;code&gt;inactive_file&lt;/code&gt;)을 뺀 값이라 &lt;b&gt;그 cgroup이 손해 없이는 못 놓는 양&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;b&gt;어느 위치의 cgroup 값인지는 라벨이 가른다. &lt;/b&gt;- cgroup은 계층 구조이기 때문에&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;cgroup 하나마다 시계열이 하나씩 나오는데, 이름은 전부 &lt;b&gt;&lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;&lt;/b&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;어느 범위의 working_set&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;&lt;code&gt;id=&quot;/&quot;&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;노드의 모든 프로세스&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;5,906&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;id=&quot;/kubepods.slice&quot;&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파드 전부&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memory.max&lt;/code&gt; 5,381&lt;/td&gt;
&lt;td&gt;3,671&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;pod=&quot;queue-xxxxx&quot;&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파드 하나&lt;/td&gt;
&lt;td&gt;그 안 &lt;code&gt;limits&lt;/code&gt; 합&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;container=&quot;queue&quot;&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;컨테이너 하나&lt;/td&gt;
&lt;td&gt;그 컨테이너의 &lt;code&gt;limits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&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;&lt;code&gt;container&lt;/code&gt; 라벨이 빈 시계열도 같이 나온다. 파드 cgroup 자체를 가리키는 것이라 &lt;code&gt;pause&lt;/code&gt; 몫이 거기 들어 있고, &lt;b&gt;컨테이너만 세려면 &lt;code&gt;container!=&quot;&quot;&lt;/code&gt;로 걸러야 한다.&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;b&gt;루트 &lt;code&gt;id=&quot;/&quot;&lt;/code&gt; 줄만 조립 방식이 다르다.&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;b&gt;루트 cgroup 디렉터리에는 &lt;code&gt;memory.current&lt;/code&gt;도 &lt;code&gt;memory.stat&lt;/code&gt;도 없다&lt;/b&gt; &amp;mdash; 둘 다 비루트 전용 파일이고, &lt;b&gt;노드 전체&lt;/b&gt;는 커널이 이미 &lt;b&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;(node-exporter 역할)&lt;/b&gt;에서 세고 있다. 그래서 cAdvisor가 루트만은 그 파일로 같은 꼴을 조립한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;비루트 cgroup   working_set = memory.current &amp;minus; inactive_file
루트 &quot;/&quot;       working_set = (MemTotal &amp;minus; MemFree) &amp;minus; Inactive(file)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;working set&quot;&lt;/b&gt;을 구하는 것이기에 빼는 항의 정체는 페이지 캐시의 &lt;b&gt;Inactive&lt;/b&gt;로 같다 &amp;mdash; &lt;b&gt;그 범위에서 커널이 공짜로 회수할 몫.&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;b&gt;뺄셈이 그대로 쿼리가 된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;container_memory_working_set_bytes{id=&quot;/&quot;}
  &amp;minus; container_memory_working_set_bytes{id=&quot;/kubepods.slice&quot;}
  = 2,235                                        시스템 몫&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 전체와 파드 전부를 &lt;b&gt;같은 기준으로 잰 값&lt;/b&gt;이라 빼도 어긋나지 않는다. 6절에서 표를 고칠 때 쓰는 것이 이 성질이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;한계 &amp;mdash; 지금 쓰는 양과 지금 걸린 상한까지&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cAdvisor가 읽는 것은 cgroup 파일뿐이라, 거기 적혀 있는 것까지만 답한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;답한다     파드 전부가 지금 얼마 쓰나            3,671
          그 상한이 얼마인가                   5,381
          컨테이너 하나가 자기 limits 를 넘었나

답 못 한다  그 상한이 왜 5,381 인가
          파드를 더 앉힐 자리가 있나            requests 는 cgroup 에 없다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memory.max&lt;/code&gt;가 &lt;code&gt;7,941 &amp;minus; 2,560&lt;/code&gt;이라는 것까지는 봤지만 &lt;b&gt;그 예약을 왜 그만큼 뗐는지, 스케줄러가 보는 선은 왜 또 다른지는 cgroup 밖에서 정해진다.&lt;/b&gt; 그 값을 계산해서 cgroup 파일에 적는 쪽이 4절이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 쿠버네티스 층 &amp;mdash; 노드를 나누고 그 선을 cgroup 으로 옮긴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 두 층은 &lt;b&gt;쿠버네티스가 없어도 있는 것&lt;/b&gt;이었다. 리눅스 서버라면 어디든 &lt;b&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;(node-exporter)&lt;/b&gt;가 있고 &lt;b&gt;cgroup 디렉터리(cAdvisor)&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;b&gt;그중 &lt;code&gt;memory.max&lt;/code&gt; 하나만 커널이 채운 것이 아니다.&lt;/b&gt; 누군가 계산해서 써 넣은 값이다. 그 일을 하는 것이 노드마다 하나씩 도는 &lt;b&gt;kubelet&lt;/b&gt; 이고, 파드를 띄우는 것도 넘은 파드를 내보내는 것도 이 프로세스가 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;cAdvisor 는 kubelet 안에 있는데 층이 다르다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memory.max&lt;/code&gt; 파일 하나를 셋이 건드린다. &lt;b&gt;하나가 쓰고 둘이 읽는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;쓴다    kubelet    이 파일에 5,381 을 적는다
읽는다  커널       적힌 값을 읽어 그 선을 강제한다
읽는다  cAdvisor   적힌 값을 읽어 메트릭으로 내놓는다
                    container_spec_memory_limit_bytes&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cAdvisor는 kubelet 바이너리에 컴파일된 코드라 별도 프로세스가 아니다 &amp;mdash; &lt;code&gt;ps&lt;/code&gt;로 찾아도 안 나온다. 같은 프로세스 안에 있어도 &lt;b&gt;cAdvisor는 읽는 쪽에만 서 있다.&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;b&gt;그 값을 누가 정했느냐&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;code&gt;MemTotal&lt;/code&gt; &amp;middot; &lt;code&gt;MemAvailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;커널&lt;/b&gt;이 센다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory.current&lt;/code&gt; &amp;middot; &lt;code&gt;memory.stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cgroup 파일&lt;/td&gt;
&lt;td&gt;&lt;b&gt;커널&lt;/b&gt;이 센다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;memory.max&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cgroup 파일&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;kubelet 이 계산해 쓴다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;allocatable&lt;/code&gt; &amp;middot; &lt;code&gt;requests&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;노드의 파일에 없다. apiserver 에 있다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;쿠버네티스가 정한다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널이 다루는 위 둘은 초마다 움직인다. &lt;b&gt;아래 둘은 설정을 바꾸고 kubelet을 다시 띄울 때까지 고정이다.&lt;/b&gt; 대시보드에서 한쪽은 출렁이고 한쪽은 평평한 선으로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;뺄셈이 두 단이고, 값을 둘 뽑는다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet은 부팅하며 노드 &lt;code&gt;config.yaml&lt;/code&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;중간에 한 번 cgroup /kubepods.slice의 memory.max로 , &lt;/b&gt;&lt;b&gt;끝에 한 번 allocatable 값을 뽑아 apiserver로 보낸다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;capacity 7,941 MiB (7.76 GiB)   노드의 물리 메모리
  &amp;minus; kube-reserved     2,048     kubelet &amp;middot; containerd 몫
  &amp;minus; system-reserved     512     sshd &amp;middot; systemd 등 OS 몫
  ──────────────────────────
  = 5,381 MiB                   /kubepods.slice 의 memory.max 에 쓴다
  &amp;minus; eviction-hard       300     노드 여유가 이 아래면 파드를 내보낸다
  ──────────────────────────
  = 5,081 MiB (4.96 GiB)        allocatable 로 apiserver 에 보고한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 줄 전부 노드 &lt;code&gt;config.yaml&lt;/code&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;903&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bP0ZdY/dJMcaij7LEw/2ivElZ1mYwRzcQXkOvrF0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bP0ZdY/dJMcaij7LEw/2ivElZ1mYwRzcQXkOvrF0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bP0ZdY/dJMcaij7LEw/2ivElZ1mYwRzcQXkOvrF0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbP0ZdY%2FdJMcaij7LEw%2F2ivElZ1mYwRzcQXkOvrF0K%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;903&quot; height=&quot;224&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;224&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템 쪽을 그 안에 가둔다는 뜻도 아니다.&lt;/b&gt; kubelet이 상한을 적는 cgroup은 &lt;code&gt;/kubepods.slice&lt;/code&gt; 하나라 &lt;code&gt;/system.slice&lt;/code&gt;는 상한 없음 그대로이고, 시스템이 2,560을 넘게 써도 막는 선이 없다. 지금 시스템 몫 2,235가 예약 안에 있는 것은 강제된 결과가 아니라 관측값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;두 값이 다른 곳으로 가는 이유 &amp;mdash; 읽는 쪽이 다른 데 있다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;allocatable&lt;/code&gt;&lt;/b&gt;은 메트릭이 아니다. &lt;b&gt;kubelet&lt;/b&gt;이 &lt;b&gt;Node 오브젝트&lt;/b&gt;의 &lt;b&gt;&lt;code&gt;status.allocatable&lt;/code&gt; 필드에 실어 apiserver로 올리는 값&lt;/b&gt;이고, &lt;code&gt;kubectl describe node&lt;/code&gt;로 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RLS9J/dJMcad366W1/6E2fXKfl9Z9k4DVY1oU3Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RLS9J/dJMcad366W1/6E2fXKfl9Z9k4DVY1oU3Zk/img.png&quot; data-alt=&quot;노드하나에 CPU 4를 할당. 위의 config.yaml을 읽어 kube-reserved / system-reserved의 CPU 500+200 값을 제외하여 Alloctable은 kubelet이 3300으로 할당.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RLS9J/dJMcad366W1/6E2fXKfl9Z9k4DVY1oU3Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRLS9J%2FdJMcad366W1%2F6E2fXKfl9Z9k4DVY1oU3Zk%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;667&quot; height=&quot;125&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;노드하나에 CPU 4를 할당. 위의 config.yaml을 읽어 kube-reserved / system-reserved의 CPU 500+200 값을 제외하여 Alloctable은 kubelet이 3300으로 할당.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;memory.max    같은 노드의 커널이 읽는다.  cgroup 파일 하나로 끝난다
allocatable   스케줄러가 읽는다.          스케줄러는 다른 노드에 앉아 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 클러스터의 컴포넌트인 스케줄러는 &lt;code&gt;k3s-1&lt;/code&gt;의 &lt;code&gt;/sys/fs/cgroup&lt;/code&gt;을 못 읽는다. &lt;b&gt;노드 사정이 클러스터에 올라가 있어야 자리를 판단할 수 있고, 그 통로가 apiserver다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;세 값이 각각 다른 것을 상대한다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드 몫을 가리키는 값이 둘이 되고 300 MiB 어긋나지만, 어느 하나가 틀린 것이 아니다.&lt;/p&gt;
&lt;table style=&quot;height: 86px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;값&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;누가 보나&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;무엇과 비교하나&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;넘으면&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;5,081 &lt;code&gt;allocatable&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;스케줄러&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;이 노드에 앉은 파드의 &lt;code&gt;requests&lt;/code&gt; 합&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;파드를 더 안 앉힌다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;5,381 &lt;code&gt;memory.max&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;커널&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;파드 전부의 &lt;code&gt;working_set&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;그 안에서 OOM 이 난다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;300 &lt;code&gt;eviction-hard&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;kubelet&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;노드 여유&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&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;&lt;b&gt;스케줄러가 보는 축과 커널이 보는 축이 다르다.&lt;/b&gt; 5,081은 스펙에 적힌 값끼리의 덧셈에 걸리는 선이고, 5,381은 지금 실제로 쓰는 양에 걸리는 선이다. 그래서 파드 전체가 5,081을 넘게 써도 커널은 5,381까지 안 죽인다. 그 사이 300 MiB는 &lt;b&gt;kubelet이 먼저 개입하도록 비워 둔 자리&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;노드 한 대를 나누는 선은 여기까지다. 5,381도 5,081도 300도 전부 노드 &lt;code&gt;config.yaml&lt;/code&gt;에서 나온 노드 단위 값이다. &lt;b&gt;그런데 표에 출처가 다른 값이 하나 끼어 있다.&lt;/b&gt; 스케줄러가 5,081과 견주는 &lt;code&gt;requests&lt;/code&gt; 합 &amp;mdash; 노드 설정이 아니라 &lt;b&gt;파드마다 스펙에 적은 값을 더한 것&lt;/b&gt;이다. 그 스펙으로 내려간다.&lt;/p&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;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;파드 하나에 적는 값 &amp;mdash; requests 와 limits, reqeusts와 alloctable&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드 스펙에 메모리 값을 두 개 적는다. 하나가 방금 그 &lt;code&gt;requests&lt;/code&gt;이고, &lt;code&gt;limits&lt;/code&gt;가 그 짝이다. &lt;b&gt;노드 층에서 갈린 두 축이 여기서 하나씩 제 값을 갖는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;requests   스케줄러의 축      노드를 고를 때 allocatable 과 견주는 재료
limits     커널의 축          이 컨테이너 cgroup 의 memory.max 가 된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;requests&lt;/code&gt;는 앉힐 때만 쓰인다.&lt;/b&gt; &lt;b&gt;스케줄러는 새 파드가 오면 노드마다 이미 앉은 파드들의 &lt;code&gt;requests&lt;/code&gt; 합에 새 파드 몫을 더해 &lt;code&gt;allocatable&lt;/code&gt;과 견주고, 통과한 노드 중에서 고른다.&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;b&gt;&lt;code&gt;requests&lt;/code&gt;를 500Mi로 적은 컨테이너가 실제로 얼마를 쓰든 스케줄러는 500으로 더하고, 500Mi를 보장해 주는 것도 아니다.&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;b&gt;&lt;code&gt;limits&lt;/code&gt;는 앉은 뒤를 맡는다.&lt;/b&gt; 컨테이너 cgroup의 &lt;code&gt;memory.max&lt;/code&gt;가 되어 커널이 강제한다. &lt;code&gt;working_set&lt;/code&gt;이 그 값을 넘으면 커널이 회수를 시도하고, 회수해도 못 줄이면 그 cgroup 안의 프로세스를 죽인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;선에 걸리면 벌어지는 일이 셋이다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드가 앉지 못하거나 사라지는 경로가 셋이고, &lt;b&gt;세 선이 하나씩 맡는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;            안 앉힌다              OOMKill                eviction
누가        스케줄러               커널                   kubelet
걸리는 선   allocatable 5,081     memory.max &amp;middot; limits    eviction-hard 300
견주는 값   requests 합           working_set            노드 여유
대상        새로 오는 파드         그 cgroup 안 프로세스  노드 위 파드 중 하나
결과        Pending 으로 대기      컨테이너 재시작         Evicted 로 남는다
이벤트 기록  FailedScheduling      OOMKilling             Evicted&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왼쪽 하나는 선언 축이고, 오른쪽 둘은 실사용 축이다.&lt;/b&gt; 안 앉힘은 실사용과 무관하게 난다 &amp;mdash; 노드가 한가해도 이미 앉은 파드들의 &lt;code&gt;requests&lt;/code&gt; 합이 &lt;code&gt;allocatable&lt;/code&gt;을 채우고 있으면 새 파드는 Pending으로 남는다.&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;OOMKill은 그 컨테이너가 자기 상한을 넘은 것&lt;/b&gt;이라 노드에 메모리가 남아 있어도 난다. &lt;b&gt;eviction은 노드 전체가 모자란 것&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;eviction에서 kubelet이 누구를 고를지는 &lt;code&gt;requests&lt;/code&gt;와 &lt;code&gt;limits&lt;/code&gt;의 관계가 정한다. 그 관계에 붙은 이름이 QoS 등급이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Guaranteed    requests == limits (CPU&amp;middot;메모리 모두)      나중에 내보낸다
Burstable     requests &amp;lt; limits, 또는 하나만 적음
BestEffort    둘 다 안 적음                            먼저 내보낸다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;이것이 kube-state-metrics(KSM)&lt;/b&gt;&lt;b&gt;&amp;nbsp;가 내는 값이다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet이 계산한 &lt;b&gt;&lt;code&gt;allocatable&lt;/code&gt;과 파드 스펙에 적힌 &lt;code&gt;requests&lt;/code&gt;는 apiserver에 오브젝트로 저장&lt;/b&gt;된다. &lt;b&gt;그것을 읽어 Mimir로 보내는 것이 kube-state-metrics(KSM)&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;b&gt;클러스터에 파드 하나로 뜨고, 읽는 곳도 apiserver 하나다.&lt;/b&gt; 노드마다 있을 이유가 없다 &amp;mdash; cgroup도 &lt;code&gt;/proc&lt;/code&gt;도 안 읽는다. 이름 앞에 &lt;code&gt;kube_&lt;/code&gt;가 붙고, 오브젝트의 필드가 그대로 메트릭이 된다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;kube_node_status_capacity        노드가 보고한 물리 메모리
kube_node_status_allocatable     kubelet 이 계산한 allocatable
kube_pod_container_resource_requests   파드 스펙에 적힌 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 값이 필요한 이유&lt;/b&gt;는 스케줄러가 보는 세계가 이쪽이기 때문이다. 파드를 어디 앉힐지는 실사용이 아니라 &lt;code&gt;allocatable&lt;/code&gt;과 &lt;code&gt;requests&lt;/code&gt;로 정해지므로, &quot;이 노드에 더 앉힐 수 있나&quot;는 이 값으로만 답이 나온다.&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;code&gt;requests: 500Mi&lt;/code&gt;라고 적혀 있으면 그 값을 낸다. 그 컨테이너가 실제로 50Mi를 쓰든 490Mi를 쓰든 같은 값이다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;답할 수 있다   파드를 새로 앉힐 자리가 있나       allocatable &amp;minus; requests 합
답 못 한다     지금 실제로 얼마 쓰나&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;apiserver를 거쳐 오는 것도 성질을 하나 만든다. &lt;b&gt;etcd 쿼럼이 깨지면 갱신을 멈추고 마지막 값으로 굳는다.&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;kubelet 자신도 &lt;code&gt;:10250/metrics&lt;/code&gt;로 지표를 내지만(&lt;code&gt;kubelet_*&lt;/code&gt;) 이 대시보드에서는 쓰지 않는다. &lt;b&gt;kubelet은 값을 내는 쪽보다 판정하는 쪽으로 이 글에 나온다&lt;/b&gt; &amp;mdash; &lt;b&gt;&lt;code&gt;allocatable&lt;/code&gt;을 계산&lt;/b&gt;하고, &lt;b&gt;cgroup 상한을 설정&lt;/b&gt;하고, 누구를 내보낼지 고른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;층 셋을 한 표로&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2&amp;middot;3&amp;middot;4절&lt;/b&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;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;리눅스(2절)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;node-exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;node_*&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;노드 전체 합. 캐시는 전부 여유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;cgroup(3절)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cgroup 파일&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cAdvisor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;container_*&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;묶음별. &lt;code&gt;active_file&lt;/code&gt;은 사용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;쿠버네티스(4절)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;apiserver 오브젝트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;kube-state-metrics(KSM)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;kube_*&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;선언값. 실사용과 무관, 고정&lt;/b&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;세 층이 캐시와 실사용을 서로 다르게 세는 것까지 성격 칸에 들어 있다. &lt;b&gt;행1이 쓰는 값은 전부 이 세 줄 어딘가에서 나온다.&lt;/b&gt; 이것을 들고 대시보드로 돌아간다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 층의 값으로 행1 을 짠다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 던진 질문 셋을 다시 놓는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;이 노드에 파드를 더 앉힐 수 있나
어디까지가 파드 몫이고 어디부터가 시스템 몫인가
노드마다 떼어 둔 예약 2,560 을 줄일 수 있나&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;b&gt;첫 질문은 선언 축의 질문이다.&lt;/b&gt; 파드를 앉히는 것은 스케줄러이고, 스케줄러는 &lt;code&gt;requests&lt;/code&gt; 합과 &lt;code&gt;allocatable&lt;/code&gt;만 보므로 다른 값으로는 답이 안 나온다. &lt;b&gt;나머지 둘은 실사용 축의 질문이다.&lt;/b&gt; 지금 실제로 쓰는 양을 파드 몫과 시스템 몫으로 갈라 세야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축이 다른 값을 한 표에 섞으면 열마다 기준을 따져 읽어야 한다. 그래서 행1을 표 두 개로 갈랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;선언 &amp;mdash; 스케줄러가 보는 세계    allocatable &amp;middot; requests 합 &amp;middot; 스케줄 여유
실사용 &amp;mdash; 커널이 보는 세계      물리 &amp;middot; 파드 사용 &amp;middot; 시스템 사용
                             노드 여유 &amp;middot; 파드 여유 &amp;middot; 시스템 여유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSSt8i/dJMcahetUF6/8YQslAYgXXmC0ILdbim7K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSSt8i/dJMcahetUF6/8YQslAYgXXmC0ILdbim7K1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSSt8i/dJMcahetUF6/8YQslAYgXXmC0ILdbim7K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSSt8i%2FdJMcahetUF6%2F8YQslAYgXXmC0ILdbim7K1%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;672&quot; height=&quot;238&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;181&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F1lA9/dJMcad36AqT/nRPnAAIWNi0fX2gy3UNBfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F1lA9/dJMcad36AqT/nRPnAAIWNi0fX2gy3UNBfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F1lA9/dJMcad36AqT/nRPnAAIWNi0fX2gy3UNBfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF1lA9%2FdJMcad36AqT%2FnRPnAAIWNi0fX2gy3UNBfK%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;958&quot; height=&quot;146&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;154&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 data-ke-size=&quot;size16&quot;&gt;첫 질문은 선언 표가 그 자리에서 답한다. &lt;code&gt;스케줄 여유 = allocatable &amp;minus; requests 합&lt;/code&gt;이고, 재료 둘 다 kube-state-metrics가 낸다. &lt;b&gt;남은 두 질문이 실사용 표의 몫이고, 이 표에서 문제가 났다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그라파나가 표를 만드는 방식&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mimir에 저장되는 것은 시계열이다. &lt;b&gt;메트릭 이름 + 라벨 조합 하나가 시계열 하나&lt;/b&gt;이고, 거기에 시각과 값이 짝으로 쌓인다. 그라파나 패널은 PromQL 쿼리를 Mimir에 보내고, 돌아온 시계열을 그린다. 패널 종류가 정하는 것은 얼마나 요청해서 어떻게 그리느냐다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;표 패널     마지막 시각의 값 하나만 요청한다
            시계열마다 행 하나 &amp;mdash; node 라벨 k3s-1&amp;middot;2&amp;middot;3 이 세 행이 된다
            쿼리 하나가 열 하나 &amp;mdash; 여섯 열이면 쿼리 여섯
추이 패널   구간을 요청해 시각순으로 선을 잇는다
            정한 값 allocatable 은 평평한 선, 재는 값은 출렁이는 선&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행이 노드별로 갈리려면 모든 쿼리 결과에 &lt;code&gt;node&lt;/code&gt; 라벨이 있어야 한다. &lt;code&gt;kube_*&lt;/code&gt;에는 있는데 &lt;b&gt;node-exporter가 내는 값에는 없다.&lt;/b&gt; &lt;code&gt;instance=&quot;192.168.0.201:9100&quot;&lt;/code&gt; 형태뿐이라 그대로는 다른 출처와 안 붙는다. 그래서 노드 이름이 든 &lt;code&gt;node_uname_info&lt;/code&gt;와 조인해 이름을 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;node_memory_MemTotal_bytes * on(instance) group_left(nodename) node_uname_info&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행1의 &lt;code&gt;node_*&lt;/code&gt; 쿼리마다 이 조각이 붙어 있는 것이 그래서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;실사용 표의 여섯 열 &amp;mdash; 넷이 뺄셈이다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실사용 쪽 두 질문에 답하려면 열이 여섯 필요하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;가른다     파드 사용 &amp;middot; 시스템 사용
남은 양    파드 여유 &amp;middot; 시스템 여유
기준       물리 &amp;middot; 노드 여유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 넷이 뺄셈으로 나온다. 뺄셈은 빼는 두 값이 같은 기준일 때만 성립한다 &amp;mdash; &lt;b&gt;여기가 이 글이 막혔던 자리다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;시스템 사용 열이 두 층을 섞었다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행1을 처음 짤 때 &lt;code&gt;시스템 사용&lt;/code&gt;을 이렇게 계산했다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;시스템 사용 = (MemTotal &amp;minus; MemAvailable) &amp;minus; &amp;Sigma; working_set{container!=&quot;&quot;}
                  리눅스 층 값                  cgroup 층 값&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;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;k3s.service&lt;/code&gt; 프로세스 하나이므로, 노드에 들어가 그 RSS와 나란히 놓으면 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                      k3s-1    k3s-2    k3s-3     MiB
시스템 사용 열의 값     1,356      663      360
k3s.service RSS       1,708    1,120    1,082&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s-3에서 이 열은 시스템 전체를 360으로 계산하는데, 그 안에서 도는 프로세스 하나가 1,082다. &lt;b&gt;세 노드 모두 프로세스 하나가 시스템 전체로 계산된 값보다 크다.&lt;/b&gt; 식이 틀렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;원인 &amp;mdash; 캐시를 반대로 세는 값끼리 뺐다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 층은 &lt;code&gt;active_file&lt;/code&gt;을 여유로 센다 &amp;mdash; 커널이 &lt;code&gt;MemAvailable&lt;/code&gt;에 더해 둔다. cgroup 층은 사용으로 센다 &amp;mdash; cAdvisor가 &lt;code&gt;working_set&lt;/code&gt;에 남긴다. 두 값을 한 식에 넣으면 k3s-1에서 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;MemTotal &amp;minus; MemAvailable = 4,740     노드 전체의 &quot;사용&quot;
    MemAvailable = MemFree + 페이지 캐시 전부 + SReclaimable &amp;minus; 안전분
    &amp;rarr; 캐시를 여유로 쳤다.  파드의 active_file 이 이 사용에는 없다

&amp;Sigma; working_set           = 3,384     파드 전부의 &quot;사용&quot;
    working_set = memory.current &amp;minus; inactive_file
    &amp;rarr; active_file 을 사용으로 남겼다.  여기에는 들어 있다
                          ─────
                          1,356     앞항에 없는 몫을 뒤항이 들고 있어
                                    그만큼 한 번 더 빠진다&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;code&gt;active_file&lt;/code&gt;이 양쪽에 같은 방식으로 들어 있어 상쇄된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;루트 / 의 working_set             5,906     노드 전체 (파드 + 시스템)
    (MemTotal &amp;minus; MemFree) &amp;minus; Inactive(file)
/kubepods.slice 의 working_set    3,671     파드 전부
    memory.current &amp;minus; inactive_file
                                  ─────
                                  2,235     시스템 몫만 남는다
                                            k3s.service RSS 1,708 을 담는다&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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;              Active_file    섞은 식     cgroup 기준    차이
k3s-1            1,649        1,356        2,235        880
k3s-2            1,566          663        1,550        887
k3s-3            1,980          360        1,548      1,188&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;파드 사용 열도 범위가 어긋난다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 검산에서 하나 더 나왔다. 파드 전부가 쓰는 양을 구하는 방법이 둘 있는데, 행1의 &lt;code&gt;파드 사용&lt;/code&gt; 열은 위쪽을 쓰고 있다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&amp;Sigma; working_set{container!=&quot;&quot;}        3,384   컨테이너 시계열을 전부 더한다
working_set{id=&quot;/kubepods.slice&quot;}   3,671   디렉터리 값을 그대로 읽는다
                                            커널이 memory.max 와 비교하는 값&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;둘 다 cgroup 층이라 캐시 기준은 같은데 287 차이가 난다. &lt;b&gt;더한 값이 작은 이유는, &lt;code&gt;/kubepods.slice&lt;/code&gt; 안에 컨테이너 시계열로 안 잡히는 몫이 있어서다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/kubepods.slice                3,671 = 이 아래 전부의 합
├─ 파드 하나
│   ├─ pause 컨테이너            container 라벨이 없다 &amp;rarr; &amp;Sigma; 에서 빠진다
│   └─ 앱 컨테이너               &amp;Sigma; 에 들어간다
├─ 파드 &amp;hellip;
└─ 컨테이너 밖에 직접 얹힌 몫      죽은 컨테이너가 남긴 캐시는 부모
                                cgroup 으로 이관된다 &amp;rarr; &amp;Sigma; 에서 빠진다&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;커널이 상한 5,381과 비교하는 값은 디렉터리 값 3,671이다. 개별 합 3,384로 여유를 계산하면 &lt;b&gt;표가 파드 여유를 287만큼 크게 잡는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;표가 말하는 파드 여유       5,381 &amp;minus; 3,384 = 1,997
커널이 실제로 허용하는 것    5,381 &amp;minus; 3,671 = 1,710&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하 판에서 &quot;아직 자리 있다&quot;로 읽은 곳에 커널은 287만큼 더 가까이 와 있는 셈이다.&lt;/p&gt;
&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 같은 기준 안에서만 뺀다&lt;/h2&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;&lt;code&gt;MemAvailable&lt;/code&gt;&lt;/b&gt; &amp;mdash; 노드 전체에서 &lt;b&gt;지금 달라면 받을 수 있는 양.&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;MemFree + 파일 캐시 전부 + SReclaimable &amp;minus; 안전분&lt;/code&gt;으로 계산해 두고, node-exporter가 낸다.&lt;/li&gt;
&lt;li&gt;캐시를 여유 쪽에 놓는 회계다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;working_set&lt;/code&gt;&lt;/b&gt; &amp;mdash; 그 cgroup이 &lt;b&gt;놓으면 손해인, 쓰는 중인 양.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cAdvisor가 &lt;code&gt;memory.current &amp;minus; inactive_file&lt;/code&gt;로 계산한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;active_file&lt;/code&gt;을 사용 쪽에 놓는 회계라, 같은 캐시가 &lt;code&gt;MemAvailable&lt;/code&gt;과 반대편에 선다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;allocatable&lt;/code&gt;&lt;/b&gt; &amp;mdash; 노드가 &lt;b&gt;파드에게 내주기로 선언한 총량.&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;code&gt;capacity &amp;minus; 예약 2,560 &amp;minus; eviction 300 = 5,081&lt;/code&gt;로 계산해 apiserver에 보고한다.&lt;/li&gt;
&lt;li&gt;실사용과 무관하게 고정이고, 스케줄러가 &lt;code&gt;requests&lt;/code&gt; 합과 비교할 때만 쓴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여섯 열을 이렇게 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;물리        MemTotal                                  리눅스 층 &amp;middot; 그대로 둔다

파드 사용    &amp;Sigma; container{container!=&quot;&quot;}                cgroup 층
         &amp;rarr;  working_set{id=&quot;/kubepods.slice&quot;}         같은 층 &amp;middot; 범위 교정

시스템 사용  (MemTotal &amp;minus; MemAvailable) &amp;minus; &amp;Sigma; container   리눅스&amp;minus;cgroup 섞임
         &amp;rarr;  working_set{id=&quot;/&quot;} &amp;minus; working_set{id=&quot;/kubepods.slice&quot;}
                                                      cgroup 층끼리

노드 여유    MemAvailable                              리눅스 층
         &amp;rarr;  capacity &amp;minus; working_set{id=&quot;/&quot;}            총량 &amp;minus; cgroup 층

파드 여유    allocatable &amp;minus; &amp;Sigma; container                 선언값&amp;minus;cgroup 섞임
         &amp;rarr;  memory.max 5,381 &amp;minus; working_set{id=&quot;/kubepods.slice&quot;}
                                                      cgroup 층끼리

시스템 여유  예약 2,560 &amp;minus; 시스템 사용                   상수&amp;minus;cgroup &amp;middot; 그대로&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;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;MemTotal      node_memory_MemTotal_bytes            리눅스 층 &amp;middot; node-exporter
working_set   container_memory_working_set_bytes    cgroup 층 &amp;middot; cAdvisor
memory.max    container_spec_memory_limit_bytes     cgroup 층 &amp;middot; cAdvisor
capacity      kube_node_status_capacity             쿠버네티스 층 &amp;middot; KSM
예약 2,560     메트릭이 아니다.  config.yaml 에 적은 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;capacity&lt;/code&gt;는 선언값(KSM)인데 실사용 표에 껴도 어긋나지 않는다 &amp;mdash; &lt;b&gt;안 움직이는 총량이라 캐시를 어떻게 세느냐는 회계가 아예 안 붙는다.&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;code&gt;node_memory_MemAvailable_bytes&lt;/code&gt;가 행1에서 빠진다. 커널 관점의 여유라 cgroup 회계와 섞이지 않고, 실사용 표의 여유 세 칸은 전부 cgroup 기준으로 나온다.&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;code&gt;파드 여유&lt;/code&gt;의 기준은 &lt;code&gt;allocatable&lt;/code&gt; 5,081이 아니라 cgroup 상한 5,381이다. 이 열이 답하는 질문이 &quot;기존 파드가 더 쓸 수 있나&quot;이고, 그 선을 긋는 것은 커널의 &lt;code&gt;memory.max&lt;/code&gt;이기 때문이다. 스케줄러 관점의 여유는 선언 표의 &lt;code&gt;스케줄 여유&lt;/code&gt;가 따로 답하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;CPU 표는 고치지 않는다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실사용 CPU 표에도 같은 형태의 뺄셈이 있어 같은 검산을 했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                        k3s-1   k3s-2   k3s-3   millicore
node-exporter 기준         196     186     164
cgroup 기준               171     160     140
차이                      +25     +26     +24&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 노드가 모두 같은 방향으로 15퍼센트 남짓 크다. 메모리처럼 노드마다 뒤집히지 않는다.&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;CPU에는 페이지 캐시 같은 회계 차이가 없다. 대신 node-exporter가 세는 non-idle 시간에는 cgroup에 귀속되지 않는 것이 들어간다 &amp;mdash; 인터럽트(irq&amp;middot;softirq)와 steal이다. &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;h5&gt;&lt;b&gt;예약 2,560 을 줄일 수 있나&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 질문은 고친 &lt;code&gt;시스템 사용&lt;/code&gt;으로 답이 나온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                                  k3s-1   k3s-2   k3s-3     MiB
시스템 사용 (cgroup 기준)            2,235   1,550   1,548
떼어 둔 예약                        2,560   2,560   2,560
여유                                 325   1,010   1,012&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예약을 2 GiB로 줄이면 k3s-1은 187 MiB가 모자란다. &lt;b&gt;섞어 뺀 값 1,356으로 계산하면 여유가 1,204로 나와 줄여도 된다는 답이 됐을 자리다.&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;여기까지가 행1을 고치는 근거다. 남은 일은 실사용 표의 쿼리 넷을 위 식으로 바꾸는 것이고, &lt;b&gt;표가 같은 기준으로 서 있어야 부하를 걸며 읽는 숫자도 같은 기준으로 읽힌다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/224</guid>
      <comments>https://zed6740.tistory.com/224#entry224comment</comments>
      <pubDate>Sat, 15 Aug 2026 14:42:09 +0900</pubDate>
    </item>
    <item>
      <title>번외 &amp;mdash; 클러스터 안 카프카</title>
      <link>https://zed6740.tistory.com/223</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 막힌 자리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11부에서 data 네임스페이스의 파드(MySQL&amp;middot;Redis&amp;middot;Kafka)에 NetworkPolicy 를 설계하는 중이다. 방식은 하나다. 일단 전부 막고, 들어와야 하는 것만 다시 연다. 그래서 대상마다 같은 질문에 답하게 된다 &amp;mdash; 이 포트에 누가 붙어야 하나.&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;MySQL 3306 은 booking 하나였다. Redis 6379&amp;middot;26379 는 queue 와 booking 이었다. Kafka 9092 에서 답이 나오지 않았다. 브로커가 3대인 것도, 토픽이 5종인 것도, &lt;code&gt;partitions: 4&lt;/code&gt; 와 &lt;code&gt;min.insync.replicas: 2&lt;/code&gt; 가 무엇을 정하는 값인지도 조회해야 알 수 있었다. 그 설정은 AI 도움을 받아 쓴 것이고, 그때 읽고 넘어갔다고 생각했던 것들이다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;여기서 쓰는 쿠버네티스 용어&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파드(Pod)&lt;/b&gt; &amp;mdash; 컨테이너를 실행하는 최소 단위. 한 파드는 한 노드(물리&amp;middot;가상 서버) 위에서 돈다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네임스페이스(Namespace)&lt;/b&gt; &amp;mdash; 클러스터 안을 이름으로 나눈 구획. app(frontend&amp;middot;queue&amp;middot;booking) / data(MySQL&amp;middot;Redis&amp;middot;Kafka) / observability 셋으로 나뉜다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커스텀 리소스(CR)와 오퍼레이터&lt;/b&gt; &amp;mdash; 쿠버네티스가 원래 모르는 종류의 객체를 선언 형식으로 등록한 것이 CR, 그 선언을 읽어 실제 파드&amp;middot;설정을 만들고 유지하는 프로그램이 오퍼레이터. Kafka&amp;middot;KafkaNodePool&amp;middot;KafkaTopic 은 Strimzi 오퍼레이터(1.1.0)가 읽는 CR 이고, 그 결과로 Kafka 4.3.0 브로커 파드가 선다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PV(PersistentVolume)&lt;/b&gt; &amp;mdash; 파드가 재시작해도 남는 디스크 영역. 로컬 PV 는 특정 노드의 디스크를 그대로 쓰는 PV 라, 그 PV 를 쓰는 파드는 그 노드에 고정된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HPA(HorizontalPodAutoscaler)&lt;/b&gt; &amp;mdash; 부하에 따라 파드 수를 min 과 max 사이에서 조절하는 객체.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NetworkPolicy&lt;/b&gt; &amp;mdash; 파드가 주고받는 통신을 상대&amp;middot;포트 단위로 제한하는 객체. 정책이 하나도 없으면 전부 통과한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 요청이 사라지는 지점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 클러스터에 올라간 서비스는 셋이다. frontend(정적 페이지), queue(Go, 대기열과 정원 판정), booking(Java/Spring, 예매).&lt;/li&gt;
&lt;li&gt;queue 가 정원 통과자를 판정하면 booking 이 그 사람에게 좌석 접근 권한을 발급한다.&lt;/li&gt;
&lt;li&gt;이 두 동작을 queue 가 booking 을 직접 HTTP 로 호출해 잇는다고 하면, booking 파드가 재시작 중인 순간의 호출은 실패하고 그 요청은 어디에도 남지 않는다.&lt;/li&gt;
&lt;li&gt;queue 안에서 재시도를 돌려도 queue 파드 자신이 재시작하면 재시도 대상 목록이 함께 사라진다. 보내는 쪽과 받는 쪽 사이에 요청을 적어 두는 자리를 두면 이 구간이 끊기지 않는다. 카프카가 그 자리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;카프카&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어온 레코드를 파일 끝에 덧붙이기만 하고, 읽는 쪽이 나중에 순서대로 가져가게 하는 서버. 이 구조를&lt;b&gt; append-only 로그&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;브로커(broker)&lt;/b&gt; &amp;mdash; 레코드를 받아 디스크에 적고 읽기 요청에 답하는 서버 프로세스. 이 클러스터에서는 파드 하나가 브로커 하나다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로듀서(producer)&lt;/b&gt; &amp;mdash; 레코드를 브로커에 보내는 쪽.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨슈머(consumer)&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;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;사용자가 queue 에 붙는다. queue 가 Redis 에서 Lua 스크립트로 정원을 원자적으로 확인하고, 통과자만 &lt;code&gt;admissions&lt;/code&gt; 에 발행한다.&lt;/li&gt;
&lt;li&gt;booking 이 &lt;code&gt;admissions&lt;/code&gt; 를 읽고 &lt;code&gt;admitted:{movie}:{requestId}&lt;/code&gt; 키를 Redis 에 발급한다(TTL 180초). 좌석 조회&amp;middot;점유&amp;middot;예매 확정은 그 키를 요구한다. 없으면 403.&lt;/li&gt;
&lt;li&gt;예매가 확정되면 booking 이 &lt;code&gt;bookings-completed&lt;/code&gt; 를 발행하고, queue 가 그것을 읽어 자리를 반환해 다음 사람을 들여보낸다.&lt;/li&gt;
&lt;li&gt;세션이 만료되거나 이탈하면 queue 가 &lt;code&gt;admissions-revoked&lt;/code&gt; 를 발행하고, booking 이 인증 키를 지운다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queue 와 booking 은 서로를 호출하지 않는다. 양쪽 다 브로커에만 붙는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 읽고 지우지 않는다 &amp;mdash; 오프셋&lt;/h2&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;카프카는 읽어도 지우지 않는다. 대신 컨슈머 쪽이 &quot;어디까지 읽었는지&quot; 를 숫자로 기억한다. 이 숫자가 오프셋(offset)이다. 로그에 적힌 레코드마다 0부터 1씩 증가하는 정수가 붙고, 컨슈머는 처리를 마친 위치를 커밋한다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;로그   [0][1][2][3][4][5][6]
                 ▲        ▲
      커밋한 위치 3        마지막 기록 6
                 └─ 밀린 레코드 3건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 위치 값을 컨슈머 파드 안에 두면 파드가 재시작할 때 사라진다. 그래서 브로커가 &lt;code&gt;__consumer_offsets&lt;/code&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;안 하면&lt;/b&gt;: 처리에 실패한 레코드를 다시 읽을 수 없고, 컨슈머 파드가 재시작하면 읽던 위치를 잃는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 이름으로 가른다 &amp;mdash; 토픽&lt;/h2&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;토픽(topic)은 레코드를 담는 로그에 붙인 이름이다. 프로듀서는 토픽 이름을 지정해 발행하고, 컨슈머는 토픽 이름을 지정해 구독한다. 이 클러스터는 토픽 5종을 KafkaTopic CR 로 선언해 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;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;&lt;code&gt;admissions&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;&lt;b&gt;booking&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;admissions-revoked&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;&lt;b&gt;booking&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;bookings-completed&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;booking&lt;/td&gt;
&lt;td&gt;&lt;b&gt;queue&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;admissions.DLT&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;booking 내부&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;7일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;admissions-revoked.DLT&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;booking 내부&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;7일&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;&lt;b&gt;DLT(Dead Letter Topic)&lt;/b&gt;는 booking 이 레코드 처리에 반복 실패했을 때 그 레코드를 옮겨 두는 토픽이다. 실패한 레코드가 원래 토픽의 진행을 막지 않게 분리한다.&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;브로커 설정에 따라 토픽은 앱이 처음 붙을 때 자동으로 만들어질 수도 있다. 저장소의 주석은 선언해 두는 이유를 이렇게 적어 두었다 &amp;mdash; &quot;선언하지 않으면 앱이 처음 붙을 때 브로커가 기본값(파티션 1)으로 만들고, 파티션은 줄일 수 없어 그 값이 굳는다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;안 하면&lt;/b&gt;: 종류가 다른 레코드가 한 로그에 섞이고, 자동 생성에 맡기면 파티션 수가 기본값으로 고정된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 한 토픽이 한 브로커에 묶인다 &amp;mdash; 파티션&lt;/h2&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;파티션(partition)은 한 토픽의 로그를 여러 개로 나눈 것이다. 각 파티션이 독립된 append-only 로그 파일이고, &lt;b&gt;파티션마다 그것을 맡는 브로커가 따로 정해진다.&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;b&gt;순서가 보장되는 범위가 파티션 안까지로 줄어든다.&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;code&gt;requestId&lt;/code&gt; 이고, 선언에 적어 둔 이유는 &quot;전역 순서는 포기하고 키 단위 순서만 지킨다&quot; 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;선언해 둔 값&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaTopic CR 로 선언한 토픽 5종 중 앱이 주고받는 3종은 파티션 4다. 격리용 &lt;code&gt;admissions.DLT&lt;/code&gt;&amp;middot;&lt;code&gt;admissions-revoked.DLT&lt;/code&gt; 는 1이다.&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;4로 정한 근거는 선언에 두 줄로 적혀 있다. 파티션은 늘리는 것만 되고 줄이는 것은 안 되므로 나중에 모자라지 않을 값으로 잡았다는 것(&quot;파티션은 늘리기만 가능(줄이기 불가)라 넉넉히 4&quot;), 그리고 앱 HPA 상한과 맞춘 값이라는 것(&quot;partitions 4 = 앱 HPA 상한과 짝 &amp;mdash; 한 그룹의 동시 소비 파드는 파티션 수를 넘지 못한다&quot;)이다.&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;뒤쪽 제약이 어디서 나오는지는 컨슈머 그룹을 다루는 다음 절에 있다. DLT 를 1로 둔 근거는 &quot;소비자가 없고 사후 조사용이라 병렬성이 필요 없다&quot; 로 적혀 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;브로커는 노드가 아니라 파드다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클러스터의 브로커는 파드 &lt;code&gt;cgv-cgv-pool-0/1/2&lt;/code&gt; 셋이다. 노드(k3s-1/2/3)와 수가 같고 노드마다 하나씩 있어서 브로커와 노드가 같은 것으로 읽히지만, 그 배치는 두 가지가 만든 결과다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;podAntiAffinity 를 &lt;code&gt;requiredDuringScheduling&lt;/code&gt;, topologyKey 를 &lt;code&gt;kubernetes.io/hostname&lt;/code&gt; 으로 선언해 한 노드에 브로커 파드를 두 개 두지 않는다&lt;/li&gt;
&lt;li&gt;저장소가 노드마다 하나씩 있는 정적 로컬 PV(&lt;code&gt;local-kafkadata&lt;/code&gt; 10Gi)라, 그 PV 를 쓰는 파드는 그 노드에 고정된다&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;브로커 수를 늘리려면 노드와 PV 를 먼저 늘리게 된다. 조회 출력에 나오는 숫자 0&amp;middot;1&amp;middot;2 는 노드 번호가 아니라 브로커 ID 이고, 파드 이름 끝 숫자가 그 ID 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;사본이 한 벌일 때 브로커 한 대가 내려간다&lt;/b&gt;&lt;/h5&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파티션 4개.&lt;/b&gt; 4개가 브로커 3대에 나뉘므로 한 대가 두 개를 맡는다. 평소 3대가 전부 요청을 받는다. 한 대가 내려가면 그 대가 맡던 파티션만 멈추고 나머지는 계속 받는다. 내려간 대가 무엇을 맡았느냐에 따라 4개 중 1개 또는 2개가 멈춘다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 같은 파티션을 여러 브로커에 둔다 &amp;mdash; 복제&amp;middot;RF&amp;middot;리더&lt;/h2&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;복제(replication)는 같은 파티션의 로그를 여러 브로커에 사본으로 두는 것이다. 사본 개수가 복제 계수(replication factor, RF)이고, 사본이 하나뿐인 구성이 RF 1 이다.&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;리더(leader)&lt;/b&gt;로 정하고 나머지를 &lt;b&gt;팔로워(follower)&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;b&gt;RF 는 브로커 수를 넘을 수 없다.&lt;/b&gt; 같은 파티션의 사본 둘을 한 브로커에 두면 그 브로커가 내려갈 때 둘이 함께 사라져 복제한 것이 되지 않으므로, 사본은 서로 다른 브로커에 배치된다. 브로커 3대인 이 클러스터에서 선언할 수 있는 최대 RF 는 3이고, 토픽 5종 전부 RF 3 으로 선언해 뒀다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h5&gt;&lt;b&gt;사본이 세 벌일 때 브로커 한 대가 내려간다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파티션 1개 &amp;middot; RF 3.&lt;/b&gt; 브로커 3대가 같은 로그를 하나씩 갖는다. 그중 리더는 한 대뿐이다. 평소 요청은 전부 그 한 대로 가고 나머지 두 대는 복사만 받는다. 한 대가 내려갈 때 결과는 그 대가 리더였는지로 갈린다. 팔로워였으면 요청 흐름이 그대로고, 리더였으면 남은 사본 중 하나가 리더가 되어 요청을 이어받는다. &lt;b&gt;어느 쪽이든 레코드는 남고, 어느 쪽이든 그 뒤에도 요청을 받는 것은 한 대다.&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;b&gt;파티션 4개 &amp;middot; RF 3.&lt;/b&gt; 사본이 12개이고 브로커가 3대라 모든 브로커가 4개 파티션의 사본을 전부 갖는다. 갈리는 것은 리더 자리다. 리더가 파티션마다 다른 브로커에 있어서 평소 3대가 모두 요청을 받는다. 한 대가 내려가면 그 대가 리더였던 파티션만 리더가 바뀐다. 멈추는 파티션도 없고 잃는 레코드도 없다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;세 대가 다 갖고 있는데 왜 넷으로 나누나&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 다시 읽을 때 여기서 막혔다. 브로커가 3대이고 RF 3 이면 세 대가 같은 데이터를 전부 갖는다. 그러면 어느 대에 요청해도 답이 나올 텐데, 토픽을 파티션 넷으로 나눌 이유가 무엇인가.&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; 팔로워는 같은 데이터를 갖고도 클라이언트 요청을 받지 않는다. 파티션 1개에 RF 3 이면 브로커 3대가 같은 데이터를 갖지만 리더는 그중 1대뿐이고, 프로듀서와 컨슈머는 그 1대에만 붙는다. 남은 2대는 복제만 한다.&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;b&gt;가 흩어져서&lt;/b&gt;다. 리더는 파티션마다 하나이므로, 리더가 여럿이 되려면 파티션이 여럿이어야 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;RF        사본이 몇 개인지를 정한다      &amp;rarr;  잃지 않게 한다
파티션 수  리더가 몇 개인지를 정한다      &amp;rarr;  일이 갈리게 한다&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;h5&gt;&lt;b&gt;조회 출력에서 이것을 읽는다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;kubectl -n data exec cgv-cgv-pool-0 -- \
  bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic admissions&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Topic: admissions  PartitionCount: 4  ReplicationFactor: 3
  Configs: min.insync.replicas=2, retention.ms=259200000, retention.bytes=2147483648, segment.ms=86400000
   Partition: 0  Leader: 1  Replicas: 1,2,0  Isr: 0,1,2
   Partition: 1  Leader: 2  Replicas: 2,0,1  Isr: 0,1,2
   Partition: 2  Leader: 0  Replicas: 0,1,2  Isr: 0,1,2
   Partition: 3  Leader: 0  Replicas: 0,1,2  Isr: 0,1,2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Replicas&lt;/code&gt; 열부터 본다. 네 줄 모두 0, 1, 2 가 다 들어 있다. 파티션 4개가 각각 브로커 3대 전부에 사본을 두고 있고, 저장된 데이터만 놓고 보면 세 브로커의 내용은 같다.&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;code&gt;Leader&lt;/code&gt; 열이다. 파티션 0의 리더는 브로커 1, 파티션 1은 브로커 2, 파티션 2와 3은 브로커 0이다. 프로듀서가 파티션 0으로 보내는 레코드는 브로커 1로, 파티션 1로 보내는 레코드는 브로커 2로 간다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                브로커0        브로커1        브로커2
                (k3s-1)       (k3s-2)       (k3s-3)

   파티션0          사본          ★리더          사본
   파티션1          사본           사본         ★리더
   파티션2         ★리더           사본          사본
   파티션3         ★리더           사본          사본

   ── 가로로 읽으면 ──  한 파티션이 세 브로커에 다 있다   &amp;rarr;  RF 3.  잃지 않는다
   ── 세로로 읽으면 ──  ★가 세 브로커에 흩어져 있다      &amp;rarr;  파티션 4.  일이 갈린다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파티션을 1로 뒀다면 이 그림에 줄이 하나뿐이고 ★도 하나다.&lt;/b&gt; 사본 칸은 그때도 세 개 그대로다. 데이터가 3대에 있는 것은 RF 가 만들고, ★가 3대에 흩어지는 것은 파티션 수가 만든다.&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;리더가 3등분되지는 않았다. 브로커 0이 파티션 2개, 브로커 1과 2가 각각 1개다. 4를 3으로 나누면 2+1+1 이 최선이다. 이 배치는 조회한 시점의 상태이고, 브로커가 내려갔다 올라오면 리더가 다른 사본으로 옮겨간 상태가 될 수 있다.&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;파티션이 1개인 토픽에서는 이 배치가 한 줄로 끝난다. &lt;code&gt;admissions.DLT&lt;/code&gt; 는 &lt;code&gt;Replicas&lt;/code&gt; 가 1,2,0 이고 리더는 브로커 1 하나다. 브로커 0과 2는 이 토픽의 사본을 갖고 있지만 팔로워라서 클라이언트 요청을 받지 않는다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;ISR 과 min.insync.replicas&lt;/b&gt;&lt;/h5&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;ISR(in-sync replicas)은 리더의 로그를 따라잡은 상태인 사본 목록이다. 위 출력의 &lt;code&gt;Isr&lt;/code&gt; 열이 네 파티션 모두 0,1,2 인 것은 그 시점에 사본 3개가 전부 따라잡았다는 뜻이다. 복제가 늦어지면 그 사본은 ISR 에서 빠지고 숫자가 줄어든다. &lt;b&gt;&lt;code&gt;Replicas&lt;/code&gt; 는 사본이 어디에 배치되어 있는지를, &lt;code&gt;Isr&lt;/code&gt; 은 그중 지금 따라잡고 있는 것이 어느 것인지를 나타낸다.&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;code&gt;min.insync.replicas&lt;/code&gt; 는 쓰기를 성공으로 처리하는 데 필요한 ISR 최소 개수다. 선언해 둔 값은 2다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;RF 3 &amp;middot; min.insync.replicas 2

브로커 3대 정상  &amp;rarr;  ISR 3  &amp;rarr;  쓰기 성공
브로커 1대 다운  &amp;rarr;  ISR 2  &amp;rarr;  쓰기 성공
브로커 2대 다운  &amp;rarr;  ISR 1  &amp;rarr;  쓰기 거부. 읽기는 리더가 살아 있는 파티션에 대해 계속된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언에 적어 둔 &quot;RF 3 = 브로커 하나 죽어도 유지&quot; 가 이 조합이다. 2대가 내려간 상태에서 쓰기를 계속 받으면 사본 없이 한 브로커에만 있는 레코드가 생기므로, 그 상황에서는 받지 않고 멈추는 쪽으로 정해진다.&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;code&gt;acks=all&lt;/code&gt; 로 보낼 때 적용된다. queue 와 booking 의 acks 설정은 조회하지 않았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 누가 무엇을 읽는가 &amp;mdash; 컨슈머 그룹&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 4개는 발행과 소비 요청을 브로커 세 대로 갈라 놓았다. 갈라진 파티션을 누가 읽을지는 그것만으로 정해지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;컨슈머는 파드가 아니다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머는 앱이 코드에서 만드는 카프카 클라이언트 객체다. &quot;이 토픽을 읽겠다&quot; 고 만든 연결 하나가 컨슈머 하나다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// queue-go/kafka/kafka.go:221
r := kafkago.NewReader(kafkago.ReaderConfig{
    ...
    GroupID: &quot;queue&quot;,
})&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// booking/src/main/java/com/cgv/booking/kafka/AdmissionConsumer.java:37
@KafkaListener(topics = &quot;admissions&quot;)

// booking/src/main/java/com/cgv/booking/kafka/AdmissionExpiryConsumer.java:44
@KafkaListener(topics = &quot;${cgv.admission-revoked.topic:admissions-revoked}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queue 는 &lt;code&gt;NewReader&lt;/code&gt; 를 한 번 호출하므로 파드 하나에 컨슈머가 하나다. booking 은 &lt;code&gt;@KafkaListener&lt;/code&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;h5&gt;&lt;b&gt;&lt;code&gt;group.id&lt;/code&gt; &amp;mdash; 카프카가 보는 유일한 이름표&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머는 접속할 때 문자열 하나를 함께 보낸다. 그것이 &lt;code&gt;group.id&lt;/code&gt; 다. 위 코드의 &lt;code&gt;GroupID: &quot;queue&quot;&lt;/code&gt; 가 그 값이고, booking 은 &lt;code&gt;application.yml&lt;/code&gt; 한 곳에 적어 두 리스너가 같이 쓴다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;# booking/src/main/resources/application.yml:53
group-id: booking&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;카프카는 파드가 무엇인지 모른다.&lt;/b&gt; 어느 노드에 있는지, 같은 앱인지도 모른다. 아는 것은 &quot;이 문자열을 달고 접속한 연결이 몇 개인가&quot; 뿐이다. 문자열이 같으면 한 묶음(컨슈머 그룹)으로 보고 파티션을 나눠 주고, 다르면 서로 없는 것처럼 각자 전부 읽게 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;배정 규칙은 한 방향으로만 걸린다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;파티션 하나  &amp;rarr;  컨슈머 하나에게만      (파티션 하나를 둘이 나눠 갖는 것은 안 된다)
컨슈머 하나  &amp;rarr;  파티션 여러 개 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 줄이 없으면 같은 레코드를 둘이 읽어 두 번 처리한다. &lt;code&gt;bookings-completed&lt;/code&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;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;h5&gt;&lt;b&gt;조회한 두 가지 상태&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;kubectl -n data exec cgv-cgv-pool-0 -- \
  bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group queue&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;TOPIC               PARTITION  CONSUMER-ID
bookings-completed  0          queue-...-4826t
bookings-completed  1          queue-...-4826t
bookings-completed  2          queue-...-gtm24
bookings-completed  3          queue-...-gtm24&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queue 는 파드 2대이고 각 파드가 컨슈머를 하나씩 만들었다. 그룹 멤버가 2개라 파티션 4개가 2개씩 나뉘었다. &lt;code&gt;CONSUMER-ID&lt;/code&gt; 끝의 문자열이 파드 이름 뒤쪽과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;... --describe --group booking&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;TOPIC               PARTITION  CONSUMER-ID
admissions          0          consumer-booking-1-5ffe...
admissions          1          consumer-booking-1-5ffe...
admissions          2          consumer-booking-1-5ffe...
admissions          3          consumer-booking-1-5ffe...
admissions-revoked  0          consumer-booking-2-9dfe...
admissions-revoked  1          consumer-booking-2-9dfe...
admissions-revoked  2          consumer-booking-2-9dfe...
admissions-revoked  3          consumer-booking-2-9dfe...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;booking 은 파드 1대인데 &lt;code&gt;CONSUMER-ID&lt;/code&gt; 가 둘로 갈린다. 파드가 2대여서가 아니라 리스너가 둘이라 컨슈머가 둘이다. 배정은 (그룹, 토픽, 파티션) 단위로 계산되므로 구독한 토픽이 다른 두 컨슈머는 서로 겹치지 않고 각자 4파티션을 전부 맡는다.&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;code&gt;admissions&lt;/code&gt; 쪽은 컨슈머 하나가 파티션 4개를 맡은 상태다. 리더가 파티션0&amp;rarr;브로커1, 파티션1&amp;rarr;브로커2, 파티션2&amp;middot;3&amp;rarr;브로커0 에 흩어져 있어 &lt;b&gt;이 컨슈머 하나가 브로커 3대에 붙는다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;컨슈머가 늘거나 줄면 다시 배정한다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹에 컨슈머가 들어오거나 나가면 브로커가 배정을 다시 계산해 전체에 통보한다. 이것을 리밸런스라 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;파티션 4 개는 그대로 두고 컨슈머 수만 바꾸면

컨슈머 1개    컨슈머1 : 파티션 0,1,2,3

컨슈머 2개    컨슈머1 : 파티션 0,1        컨슈머2 : 파티션 2,3      &amp;larr; queue 의 현재 상태

컨슈머 4개    컨슈머1 : 파티션 0     컨슈머2 : 파티션 1
              컨슈머3 : 파티션 2     컨슈머4 : 파티션 3

컨슈머 5개    위와 같고,  컨슈머5 : 배정 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 설정에 적는 것은 &lt;code&gt;group.id&lt;/code&gt; 와 구독할 토픽 이름뿐이다. 어느 파티션을 읽으라는 지정은 코드에 없다. HPA 가 파드를 늘리면 컨슈머가 늘고, 그때마다 리밸런스가 돌아 배정표가 다시 만들어진다.&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;code&gt;bookings-completed&lt;/code&gt; 의 리더는 파티션0&amp;rarr;브로커0, 파티션1&amp;rarr;브로커1, 파티션2&amp;rarr;브로커2, 파티션3&amp;rarr;브로커0 이라, 파티션 0&amp;middot;1 을 맡은 4826t 는 브로커 0&amp;middot;1 에, 파티션 2&amp;middot;3 을 맡은 gtm24 는 브로커 2&amp;middot;0 에 붙는다.&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;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;h5&gt;&lt;b&gt;읽은 위치도 &lt;code&gt;group.id&lt;/code&gt; 로 저장된다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머가 재시작하면 &quot;어디까지 읽었는지&quot; 를 잃는다. 그 값을 컨슈머 안에 두면 새로 뜬 컨슈머는 처음부터 다시 읽거나(중복) 지금 들어오는 것부터 읽는다(유실).&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;code&gt;__consumer_offsets&lt;/code&gt; 에 적고, 키를 &lt;b&gt;(&lt;code&gt;group.id&lt;/code&gt;, 토픽, 파티션)&lt;/b&gt; 으로 잡는다. 파드 이름도 컨슈머 식별자도 키에 안 들어간다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;(&quot;queue&quot;, &quot;bookings-completed&quot;, 0)  &amp;rarr;  80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드가 교체돼도 값은 브로커에 남는다. 리밸런스로 그 파티션을 새로 받은 컨슈머가 같은 &lt;code&gt;group.id&lt;/code&gt; 로 접속하면 80 다음부터 이어 읽는다. 마지막 커밋 이후에 처리한 레코드는 다시 읽는다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
0          80              80              0
1          -               0               -
2          11              11              0
3          2               2               0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CURRENT-OFFSET 은 커밋된 위치, LOG-END-OFFSET 은 다음 레코드가 붙을 위치, LAG 은 그 차이다. 네 파티션 모두 밀린 것이 없다. 파티션1 은 LOG-END-OFFSET 이 0 이라 들어온 레코드가 없고 커밋한 적도 없어 나머지 두 값이 &lt;code&gt;-&lt;/code&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;읽은 양은 파티션0 이 80, 파티션2 가 11, 파티션3 이 2 로 고르지 않다. 배정이 균등한 것과 부하가 균등한 것은 별개다. 발행 키에 따라 특정 파티션으로 몰리면 그 파티션을 맡은 컨슈머가 더 일한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;파티션 수가 상한이 되는 자리&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 하나는 컨슈머 하나에게만 간다. 뒤집으면 &lt;b&gt;동시에 읽을 수 있는 컨슈머 수의 상한이 파티션 수다.&lt;/b&gt; &lt;code&gt;bookings-completed&lt;/code&gt; 는 파티션 4 이므로 queue 파드를 5대로 올리면 다섯 번째 컨슈머는 배정받을 파티션이 없어 그룹에 들어가 있기만 하고 아무것도 읽지 않는다.&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;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;queue 의 HPA 가 min 2 / max 4 인 것이 파티션 4에 맞춘 것이고, 저장소 주석에도 &quot;partitions 4 = 앱 HPA 상한과 짝&quot; 으로 적혀 있다. HPA max 를 파티션 수보다 크게 잡으면 초과분은 자원만 차지한다.&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;booking 은 replicas 1 이고 HPA 를 붙이지 않았다. 스키마 마이그레이션 도구가 없어 여러 대가 같은 스키마를 건드리면 충돌하기 때문이다. 파티션이 4 라서 못 늘리는 것이 아니라, 파티션 쪽은 4대까지 여유가 있는데 앱 쪽 제약으로 1대에 묶여 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 여기까지의 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&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;파티션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;요청이 브로커 한 대에 쏠리는 것, 컨슈머를 여러 대로 늘리는 것&lt;/td&gt;
&lt;td&gt;데이터 유실&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;복제(RF)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;데이터 유실&lt;/td&gt;
&lt;td&gt;요청 쏠림 &amp;mdash; 사본은 클라이언트를 안 받는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;리더&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;그 파티션의 일을 어느 브로커가 할지&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컨슈머 그룹&lt;/b&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;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;&lt;b&gt;낱말이 겹쳐 헷갈리는 다섯 자리&lt;/b&gt;&lt;/h5&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;브로커 = 노드&lt;/td&gt;
&lt;td&gt;브로커는 &lt;b&gt;파드&lt;/b&gt;다. 노드당 하나로 배치했을 뿐이다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨슈머 = 파드&lt;/td&gt;
&lt;td&gt;컨슈머는 &lt;b&gt;앱이 코드에서 만든 객체&lt;/b&gt;다. 파드 하나에 여러 개일 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리더는 브로커의 성질&lt;/td&gt;
&lt;td&gt;&lt;b&gt;파티션의 성질&lt;/b&gt;이다. 같은 브로커가 어떤 파티션의 리더이면서 다른 파티션의 팔로워다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사본을 가지면 요청도 받는다&lt;/td&gt;
&lt;td&gt;팔로워는 같은 데이터를 갖고도 &lt;b&gt;요청을 받지 않는다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨슈머 수 = 파티션 수&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&amp;le; 다.&lt;/b&gt; 적으면 하나가 여럿을 맡고, 넘으면 남는 컨슈머가 논다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;&lt;b&gt;숫자를 정하는 순서&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;잃지 않는 쪽    노드 수  &amp;rarr;  브로커 &amp;le; 노드  &amp;rarr;  RF &amp;le; 브로커
일을 나누는 쪽  그 토픽을 읽을 파드 수  &amp;rarr;  파티션 &amp;ge; 그 수  &amp;rarr;  HPA max &amp;le; 파티션&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 사슬이 따로 시작한다. 브로커 3과 파티션 4가 가까운 숫자인 것은 우연이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 카프카를 세우는 것은 Strimzi 다&lt;/h2&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;그 일을 대신 하도록 만들어 둔 프로그램이 Strimzi 다. 카프카를 쿠버네티스에서 운영하려고 오픈소스로 공개해 둔 것이고, &lt;code&gt;strimzi.io&lt;/code&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;오퍼레이터&lt;/b&gt;라 부른다. 선언을 지켜보다가 실물을 그 선언에 맞춰 놓는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;들여오는 방법 &amp;mdash; Helm 차트&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;# bootstrap/install.sh
helm repo add strimzi https://strimzi.io/charts
helm upgrade --install strimzi strimzi/strimzi-kafka-operator -n data \
  --version 1.1.0 -f strimzi/values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Helm 차트와 오퍼레이터는 층이 다른 말이다. &lt;b&gt;차트는 설치 방법이고 오퍼레이터는 설치된 프로그램의 종류다.&lt;/b&gt; 매니페스트를 직접 &lt;code&gt;kubectl apply&lt;/code&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;설치하면 들어오는 것 둘&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CRD 10종.&lt;/b&gt; 쿠버네티스에 새 객체 종류를 등록한다. &lt;code&gt;kafkas&lt;/code&gt;&amp;middot;&lt;code&gt;kafkanodepools&lt;/code&gt;&amp;middot;&lt;code&gt;kafkatopics&lt;/code&gt;&amp;middot;&lt;code&gt;strimzipodsets&lt;/code&gt; 등이다. 이것이 없으면 &lt;code&gt;kind: Kafka&lt;/code&gt; 라고 적은 파일을 API 서버가 &quot;그런 종류를 모른다&quot; 며 거부한다. &lt;b&gt;CRD 가 그 종류의 문법(어떤 필드가 올 수 있는지)을 정하고, 그 문법대로 값을 채워 내가 쓰는 파일이 CR 이다.&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;b&gt;오퍼레이터 파드 하나.&lt;/b&gt; &lt;code&gt;strimzi-cluster-operator&lt;/code&gt; 다. 감시 범위는 &lt;code&gt;STRIMZI_NAMESPACE=data&lt;/code&gt; 로 &lt;code&gt;data&lt;/code&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;어떻게 동작하나&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;kafka-cluster.yaml  &amp;middot;  kafka-topics.yaml
        │  kubectl apply
        ▼
API 서버에 CR 로 저장된다        &amp;larr; 이 시점에는 아직 브로커가 없다
        │  watch
        ▼
strimzi-cluster-operator 가 알림을 받는다
        │
        ├─ 선언과 실물을 비교한다
        └─ 다르면 메운다 : 파드&amp;middot;Service&amp;middot;ConfigMap&amp;middot;Secret&amp;middot;NetworkPolicy&amp;middot;PVC 를 만들거나 고친다
        │
        └─ 그리고 2분마다 전체를 다시 비교한다
             (STRIMZI_FULL_RECONCILIATION_INTERVAL_MS=120000)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CR 은 주문서일 뿐 그 자체로는 아무것도 안 한다.&lt;/b&gt; 오퍼레이터가 읽고 실물을 만들어야 브로커가 선다. 그래서 손으로 쓴 것은 아래 두 파일이 전부다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;workloads/manifests/kafka/
  kafka-cluster.yaml   Kafka(버전&amp;middot;리스너&amp;middot;브로커 기본값) &amp;middot; KafkaNodePool(브로커 3대, 역할, 저장소)
  kafka-topics.yaml    KafkaTopic 5종(파티션&amp;middot;RF&amp;middot;보존)
        ▼
  브로커 파드 3 &amp;middot; StrimziPodSet &amp;middot; Service 2 &amp;middot; ConfigMap 5
  &amp;middot; Secret 11(인증서) &amp;middot; NetworkPolicy 2 &amp;middot; PVC 3 &amp;middot; entity-operator&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;이 구조가 해 주는 것&lt;/b&gt;&lt;/h5&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; 어느 노드에 어느 볼륨을 물릴지를 오퍼레이터가 들고 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선언을 고치면 실물이 따라온다.&lt;/b&gt; &lt;code&gt;kafka-topics.yaml&lt;/code&gt; 의 파티션을 4에서 6으로 바꾸면 실제 토픽도 6이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인증서를 손으로 안 만든다.&lt;/b&gt; 클라이언트 리스너는 &lt;code&gt;tls: false&lt;/code&gt; 라 평문인데 브로커 간 복제와 오퍼레이터 통로는 TLS 를 쓴다. 그 CA 와 인증서 11종을 Strimzi 가 만들어 배포해 둔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;대가 &amp;mdash; 만든 쪽이 계속 지킨다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 동작이 반대로도 작용한다. &lt;b&gt;Strimzi 가 만든 객체를 손으로 고치면 다음 조정에서 되돌아간다.&lt;/b&gt; 늦어도 2분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 객체 중에 NetworkPolicy 가 있다. &lt;code&gt;cgv-network-policy-kafka&lt;/code&gt; 가 브로커 앞에 서 있고, 이번 작업에서 좁혀야 하는 9092 규칙이 거기 들어 있다. 정책 파일을 새로 쓰는 방식이 카프카에만 안 통하는 이유가 이것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 브로커가 여는 포트 넷&lt;/h2&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;kubectl -n data get pod cgv-cgv-pool-0 \
  -o jsonpath='{range .spec.containers[*].ports[*]}{.name}{&quot;=&quot;}{.containerPort}{&quot;\n&quot;}{end}'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;tcp-kafkaagent=8443
tcp-ctrlplane=9090
tcp-replication=9091
tcp-clients=9092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 하나에 번호 넷이다. 포트는 한 IP 안에서 어느 프로그램이 받을지를 가르는 번호이고, 같은 브로커 프로세스가 용도별로 나눠 열었다.&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;9092&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;발행&amp;middot;소비&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt; 의 queue &amp;middot; booking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;9091&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;브로커 간 복제&lt;/td&gt;
&lt;td&gt;브로커끼리, &lt;code&gt;data&lt;/code&gt; 의 entity-operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;9090&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;컨트롤러 합의(KRaft)&lt;/td&gt;
&lt;td&gt;브로커끼리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;8443&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;브로커 상태 조회&lt;/td&gt;
&lt;td&gt;strimzi-cluster-operator&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;9091 에 entity-operator 가 들어가는 것은 Strimzi 가 만든 정책이 그렇게 적고 있어 확인된다. &lt;code&gt;cgv-network-policy-kafka&lt;/code&gt; 의 9091 규칙 출처에 &lt;code&gt;strimzi.io/name: cgv-entity-operator&lt;/code&gt; 가 들어 있다. KafkaTopic 선언을 실제 토픽으로 반영하려면 이 경로가 필요하다.&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;9092 만 출처가 &lt;code&gt;data&lt;/code&gt; 밖에 있다.&lt;/b&gt; 나머지 셋의 출처는 브로커 자신과 &lt;code&gt;data&lt;/code&gt; 안의 오퍼레이터라, &lt;code&gt;app&lt;/code&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;그리고 9092 는 브로커 3대 전부에 열어야 한다. 클라이언트는 &lt;code&gt;cgv-kafka-bootstrap&lt;/code&gt; 으로 아무 브로커에나 붙어 파티션별 리더 목록을 받은 뒤 &lt;b&gt;각 파티션의 리더에 직접 접속한다.&lt;/b&gt; 부트스트랩 주소로 지정한 브로커가 계속 중계하지 않는다. 리더가 3대에 흩어져 있으므로 한 대만 열면 그 브로커가 리더인 파티션만 쓸 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 규칙을 적는 자리가 파드마다 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11부에서 &lt;code&gt;data&lt;/code&gt; 로 들어오는 접속을 좁히는 작업은 대상마다 적는 자리가 다르다. 같은 네임스페이스 안인데 세 갈래가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준은 하나다 &amp;mdash; &lt;b&gt;그 파드 앞에 이미 정책이 서 있는가, 그리고 그것을 만든 쪽이 계속 지키는가.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;th&gt;좁히는 자리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL &amp;middot; Redis&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mysql&lt;/code&gt; &amp;middot; &lt;code&gt;redis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Helm 차트&lt;/td&gt;
&lt;td&gt;차트 &lt;code&gt;values.yaml&lt;/code&gt; 로 끄고 &lt;b&gt;정책 파일&lt;/b&gt;을 쓴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka 브로커&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cgv-network-policy-kafka&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strimzi 오퍼레이터&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Kafka CR&lt;/b&gt; 의 리스너에 적는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strimzi-cluster-operator&lt;/td&gt;
&lt;td&gt;&lt;b&gt;없다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;정책 파일&lt;/b&gt;을 쓴다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;&lt;b&gt;왜 브로커만 CR 인가&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커 앞의 정책은 오퍼레이터가 만든 것이고, 오퍼레이터는 2분마다 선언과 실물을 다시 맞춘다. 그래서 두 방식이 다 막힌다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;정책 파일을 새로 써서 더한다
   &amp;rarr; 같은 파드를 고르는 정책이 여럿이면 허용이 합쳐진다
   &amp;rarr; 출처가 빈 9092 규칙이 남아 있는 한 그 포트는 열린 채로 있다

Strimzi 정책을 지운다
   &amp;rarr; 다음 조정에서 CR 에 적힌 대로 다시 만들어진다&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;오퍼레이터가 읽는 자리를 고친다.&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;code&gt;networkPolicyPeers&lt;/code&gt; 를 적으면 오퍼레이터가 그것을 읽고 &lt;code&gt;cgv-network-policy-kafka&lt;/code&gt; 의 9092 규칙에 &lt;code&gt;from&lt;/code&gt; 절을 넣어 재생성한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# workloads/manifests/kafka/kafka-cluster.yaml
    listeners:
      - name: plain
        port: 9092
        type: internal
        tls: false
        networkPolicyPeers:
          - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: app}}
            podSelector: {matchLabels: {app.kubernetes.io/name: queue}}
          - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: app}}
            podSelector: {matchLabels: {app.kubernetes.io/name: booking}}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;kafka-cluster.yaml 을 고친다
        │  ArgoCD 가 반영
        ▼
API 서버의 Kafka CR 이 바뀐다
        │  watch
        ▼
strimzi-cluster-operator 가 읽는다
        │
        ▼
cgv-network-policy-kafka 의 9092 규칙에 from 절이 들어간 채로 재생성된다
        │
        ▼
Calico 가 그 정책을 노드 iptables 규칙으로 옮긴다&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;p data-ke-size=&quot;size16&quot;&gt;9090&amp;middot;9091&amp;middot;8443 은 Strimzi 가 이미 출처를 파드 셀렉터로 지정해 두었으므로 이번에 적을 것이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;클러스터 오퍼레이터는 제한하려는 대상이 아니다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커 쪽은 그것으로 끝나는데, 같은 네임스페이스에 정책이 하나도 안 걸린 파드가 하나 남는다. &lt;code&gt;strimzi-cluster-operator&lt;/code&gt; 다. Strimzi 는 자기가 만드는 것들(브로커&amp;middot;entity-operator)에는 정책을 붙였지만 &lt;b&gt;자기 자신에게는 안 붙였다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파드에 정책을 쓰는 것은 좁히려는 것이 아니라 &lt;b&gt;&lt;code&gt;default-deny-ingress&lt;/code&gt; 가 휩쓴 것을 되돌리는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;1  mysql&amp;middot;redis&amp;middot;브로커를 좁히려면 default-deny 가 필요하다
      정책이 안 고른 파드는 전부 열려 있으므로, 일단 다 고르고 필요한 것만 다시 연다
2  default-deny 는 podSelector: {} &amp;mdash; data 의 파드를 전부 고른다
3  아무 정책도 없던 strimzi-cluster-operator 가 여기서 처음 선택된다
4  그 순간 이 파드로 들어오는 것이 전부 막힌다
5  kubelet 이 노드 주소에서 보내는 8080 상태 검사도 막힌다
6  liveness 실패 &amp;rarr; 재시작 &amp;rarr; 또 실패 &amp;rarr; 반복 &amp;rarr; 카프카 조정이 멈춘다&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;h5&gt;&lt;b&gt;되돌릴 것이 8080 하나뿐인 이유&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오퍼레이터는 거의 다 나가는 쪽으로 일한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;나가는 쪽    API 서버를 지켜본다(watch) &amp;middot; 브로커에 8443 으로 상태를 묻는다
             객체를 만들고 고친다

들어오는 쪽   kubelet 의 상태 검사 8080        &amp;larr; 이것 하나뿐&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;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# workloads/manifests/netpol/data-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: strimzi-operator-probe
  namespace: data
spec:
  podSelector:
    matchLabels:
      strimzi.io/kind: cluster-operator     # 이 라벨을 단 파드가 대상
  policyTypes: [Ingress]                    # 나가는 쪽은 손대지 않는다
  ingress:
    - from:                                 # 원소 3개 = OR
        - ipBlock: {cidr: 192.168.0.201/32}
        - ipBlock: {cidr: 192.168.0.202/32}
        - ipBlock: {cidr: 192.168.0.203/32}
      ports:
        - port: 8080                        # from 과 AND&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여는 범위는 &lt;b&gt;&quot;노드 3대에서 8080 으로 오는 것&quot; 하나&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;노드 .201 &amp;rarr; 8080     통과
노드 .202 &amp;rarr; 8080     통과
노드 .203 &amp;rarr; 8080     통과

노드 .201 &amp;rarr; 다른 포트   막힘      포트가 다르다
파드      &amp;rarr; 8080       막힘      출처가 노드가 아니다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/32&lt;/code&gt; 는 그 IP 하나만이라는 뜻이고, kubelet 은 파드가 아니라 노드에서 도는 프로세스라 라벨로 못 고른다. 그래서 노드 주소를 직접 적는다.&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;code&gt;policyTypes&lt;/code&gt; 에 &lt;code&gt;Ingress&lt;/code&gt; 만 적어서 &lt;b&gt;나가는 쪽은 그대로 열려 있다.&lt;/b&gt; 나가는 쪽까지 막았으면 8080 을 열어 줘도 오퍼레이터는 API 서버를 못 보고 브로커에 못 붙어 아무 일도 못 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;여기는 CR 이 아니라 정책 파일인 이유&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커 앞의 정책은 Strimzi 가 만들고 2분마다 다시 맞춘다. 그래서 CR 을 고쳐야 했다. 클러스터 오퍼레이터 앞에는 &lt;b&gt;그 정책을 만든 쪽도, 지키는 쪽도 없다.&lt;/b&gt; 내가 쓴 파일이 되돌아갈 일이 없다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;data-ingress.yaml 을 쓴다
        │  ArgoCD 가 반영
        ▼
API 서버에 NetworkPolicy 객체가 저장된다     &amp;larr; 오퍼레이터는 관여하지 않는다
        │
        ▼
Calico 가 노드 iptables 규칙으로 옮긴다&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&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;/td&gt;
&lt;td&gt;&lt;code&gt;cgv-network-policy-kafka&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strimzi&lt;/td&gt;
&lt;td&gt;Kafka CR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;entity-operator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cgv-entity-operator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strimzi&lt;/td&gt;
&lt;td&gt;이번에 안 건드린다&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;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;&lt;b&gt;두 경로의 차이&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;정책 파일   내가 만든 객체를 Calico 가 집행한다
CR         오퍼레이터가 만든 객체를 Calico 가 집행한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;집행하는 쪽은 둘 다 Calico 로 같다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;좁혀도 남는 것&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너는 &lt;code&gt;tls: false&lt;/code&gt; 에 인증 설정이 없다. 출처를 지정해도 &lt;b&gt;저 두 라벨을 단 파드는 인증 없이 9092 에 붙는다.&lt;/b&gt; 라벨은 누구나 적을 수 있는 문자열이라 이 제한은 출처를 증명하지 않는다. 브로커 인증(SASL&amp;middot;mTLS)이나 메시지 서명은 이번에 하지 않는다.&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/223</guid>
      <comments>https://zed6740.tistory.com/223#entry223comment</comments>
      <pubDate>Tue, 11 Aug 2026 21:55:38 +0900</pubDate>
    </item>
    <item>
      <title>노트북 홈서버 k3s 구축기 - 11. 앱 검증과 시뮬레이터</title>
      <link>https://zed6740.tistory.com/222</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 &quot;노트북 홈서버 k3s 구축기&quot; 11부.&lt;br /&gt;지난 편까지 = 코드를 머지하면 사람 손 없이 3-4분 뒤 파드가 새 이미지로 돈다. 다만 그 파드 안의 앱을 브라우저에서 끝까지 태워본 적이 없다.&lt;br /&gt;11부 = 앱. 어떻게 흐르는지 확인하고(E2E), 남에게 보여줄 시뮬레이터로 다시 만들고, 확인된 동작을 테스트 게이트로 남기고, LAN에 열기 전에 권한&amp;middot;통신을 조인다. 끝에서 이 앱이 돌아가는 판(가상 설정들)을 정리해 부하 실측으로 넘긴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 이어받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈는 노트북 한 대에서 시작했다. 1-5부가 물리 기반이고, 클러스터 인프라는 6부부터 쌓였다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 57px;&quot;&gt;편&lt;/th&gt;
&lt;th style=&quot;width: 340px;&quot;&gt;세운 것&lt;/th&gt;
&lt;th style=&quot;width: 458px;&quot;&gt;남긴 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;1-5&lt;/td&gt;
&lt;td style=&quot;width: 340px;&quot;&gt;&lt;b&gt;Proxmox + VM 3대(k3s-1&amp;middot;2&amp;middot;3) + 네트워크&amp;middot;용도별 디스크&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 458px;&quot;&gt;노드 셋이 클러스터를 받을 준비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 340px;&quot;&gt;&lt;b&gt;k3s 3노드(embedded etcd) + 로컬 PV&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 458px;&quot;&gt;쿠버네티스가 섰다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 340px;&quot;&gt;&lt;b&gt;GitOps 부트스트랩 &amp;mdash; ArgoCD가 App 21개를 git에서 세움&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 458px;&quot;&gt;git에 적힌 대로 클러스터가 맞춰진다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 340px;&quot;&gt;&lt;b&gt;LGTM 관측 스택(분산 모드) 개통 + 관측 스택 자기 계기판&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 458px;&quot;&gt;인프라 신호가 흐르고 담긴다. 인프라 대시보드 본편&amp;middot;알림은 뒤 편으로 미뤄둠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 340px;&quot;&gt;&lt;b&gt;GitLab 자가호스팅 + webhook&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 458px;&quot;&gt;커밋이 3초 만에 클러스터에 반영된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 57px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 340px;&quot;&gt;&lt;b&gt;CI 게이트 10종 + 레지스트리 + image-updater&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 458px;&quot;&gt;머지 &amp;rarr; 3-4분 &amp;rarr; 파드 교체. 사람 손은 머지 클릭까지&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;10부가 끝난 지금을 한 줄로 하면: &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;파드 넷(queue&amp;times;2&amp;middot;booking&amp;middot;frontend)은 Running이고, LAN의 LB 주소까지 뚫려 있다&lt;/li&gt;
&lt;li&gt;그러나 대기열 입장&amp;rarr;좌석 선점&amp;rarr;예매 확정을 클러스터 위에서 끝까지 태워본 적이 없다 &amp;mdash; 이 앱은 로컬 docker-compose에서 curl 왕복까지 확인하고 올라온 상태다&lt;/li&gt;
&lt;li&gt;테스트는 0건이라 CI의 test 단계가 빈칸이고, 앱 관측은 코드에만 들어 있고, 화면은 남에게 보여줄 데모가 아니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 편이 그 간극을 메운다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1  앱은 어떻게 흐르나            아키텍처 &amp;middot; queue &amp;middot; booking &amp;middot; 프론트 &amp;middot; E2E 검증
2  프론트 &amp;mdash; 시뮬레이터로 다시    화면 구성 &amp;middot; 콘솔(투입&amp;middot;오픈&amp;middot;자동예매) &amp;middot; 백엔드 추가 셋
3  테스트                       API 목록 &amp;middot; 맞물리는 방식 &amp;middot; 거기서 나오는 불변식
4  파드 사이 접근 범위 좁히기     NetworkPolicy &amp;mdash; 정원 통제를 건너뛰는 경로를 닫는다

5  대조 &amp;middot; 6  다음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값의 대장(무엇이 가상값인지, 설계 문서와 실물이 어디서 갈라졌는지)은 분량이 편 하나라 번외로 뺐다 &amp;mdash; 12부&amp;middot;13부가 그 대장을 입력으로 쓴다.&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;stg/prd 관점에서 이 편의 자리: 부하 실측(13부)으로 배포 스펙을 산정하려면 측정 대상이 기능적으로 검증돼 있어야 하고, 측정 중 코드를 고칠 때 테스트 게이트가 있어야 한다. 여기서 만드는 시뮬레이터가 그대로 부하 시나리오의 재현 장치가 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 앱은 어떻게 흐르나&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 이 앱인가 &amp;mdash; 사건에서 문제로&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 실제 사건이다. 2024년 프로야구가 사상 처음 천만 관중을 돌파했고, 직관 티켓은 전쟁이 됐다. 한국시리즈 예매에선 접속자 폭주로 &lt;b&gt;예매 버튼이 활성화되지도 않은 채&lt;/b&gt; 대기 인원이 16만 명까지 불어났다(&lt;a href=&quot;https://www.theden.co.kr/news/articleView.html?idxno=3065&quot;&gt;덴 매거진 보도&lt;/a&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;CGV가 KBO와 협약을 맺고 야구 생중계를 극장에서 상영하기 시작했다&lt;/b&gt; &amp;mdash; 그리고 그 극장 티켓팅마저 시작과 동시에 매진됐다. &quot;경기장에 못 간 수요가 극장 예매로 몰린다&quot;는 것은 가정이 아니라 이미 일어난 일이다.&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;이 프로젝트는 그 상황을 모델로 한다. 설정은 이렇다 &amp;mdash; &lt;b&gt;2026년 KBO 올스타전 생중계를 CGV가 열고, 그 예매 시스템을 내가 만든다.&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;시드 데이터가 그 설정 그대로다: 상영물은 &quot;2026 KBO 올스타전 생중계&quot; 하나(18시), 지점 5 &amp;times; 관 4 = 회차 20, 관당 200석 = 좌석 4,000. 한 상영물에 전원이 몰리는 구조 &amp;mdash; 대기열이 필요한 바로 그 모양이다.&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;대비 없이 열면 서버 안에서 벌어질 일은 일반적인 경로로 재구성할 수 있다(실제 예매 서버들의 내부는 공개된 바 없다 &amp;mdash; 추정의 영역이다). 오픈 순간 수만 명이 좌석 조회&amp;middot;선점&amp;middot;결제 API를 동시에 때린다. 이 구간은 잠금(같은 좌석은 한 명만)과 트랜잭션(돈과 좌석의 원자적 기록)이라 &lt;b&gt;동시에 처리할 수 있는 양에 상한이 있다&lt;/b&gt; &amp;mdash; DB 커넥션 수, 잠금 경합, 커밋 디스크 속도가 그 상한을 정한다. 상한을 넘는 요청은 줄을 서는 게 아니라 타임아웃&amp;middot;에러로 튕기고, 그 실패가 재시도를 부르며 부하를 더 키운다. 결과: 전원이 실패하는 서버.&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;처리량 상한은 없앨 수 없다. 그래서 문제를 뒤집는다 &amp;mdash; &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;&lt;b&gt;아키텍처를 쌓아보기 &amp;mdash; 요구가 구조를 정한다&lt;/b&gt;&lt;/h4&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;요구 1 &amp;mdash; 문지기가 필요하다.&lt;/b&gt; 입장 인원을 정원으로 제한하는 서비스, 그게 &lt;b&gt;queue&lt;/b&gt;다. 정원은 &quot;예매 구간이 건강하게 처리할 수 있는 동시 인원&quot;이고, 지금은 데모값 2 &amp;mdash; 실값은 부하 실측으로 정한다.&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;요구 2 &amp;mdash; 대기열의 상태는 파드 밖에 있어야 한다.&lt;/b&gt; &quot;누가 입장했고 누가 몇 번째인가&quot;를 파드 메모리에 두면 두 가지가 무너진다: 파드가 재시작하면 대기열이 증발하고, 파드가 둘이면 대기열도 둘이 된다. 그래서 상태를 &lt;b&gt;Redis 한 곳&lt;/b&gt;에 몰고 파드는 무상태로 둔다 &amp;mdash; 어느 파드가 받아도 같은 명부를 보고, 파드는 언제든 갈아 끼운다. Redis인 이유: 메모리라 폭주 속도를 받아내고, 명령을 한 줄로 세워 직렬 실행하는 성질이 &quot;동시 요청의 경합&quot;을 푸는 재료가 된다(뒤에 나온다).&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;요구 3 &amp;mdash; 예매는 성격이 정반대다.&lt;/b&gt; 대기열은 &quot;빠르고 가볍게, 잃어도 복구되는 상태&quot;이고, 예매는 &quot;느려도 정확하게, 영원히 남는 기록&quot;이다. 한 서비스에 두면 폭주 트래픽이 트랜잭션과 커넥션을 두고 싸운다. 그래서 서비스를 가른다 &amp;mdash; &lt;b&gt;booking&lt;/b&gt;은 돈이 걸린 확정본만 &lt;b&gt;MySQL&lt;/b&gt;에 남기고, 임시 상태(좌석 잠금&amp;middot;입장 인증)는 Redis에 TTL(수명)을 붙여 두어 결제가 끊겨도 저절로 소멸하게 한다.&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;요구 4 &amp;mdash; 갈랐으면 직접 부르지 않는다.&lt;/b&gt; queue가 booking을 HTTP로 부르면, booking이 죽는 순간 queue도 응답을 기다리다 같이 밀린다 &amp;mdash; 가른 의미가 없다. 그래서 둘 사이에 &lt;b&gt;Kafka&lt;/b&gt;를 둔다: 보내는 쪽은 브로커에 메시지를 두고 제 갈 길을 가고, 받는 쪽은 제 속도로 꺼내 읽는다. 죽어 있던 동안의 메시지도 브로커에 남아 재기동 후 이어진다. 토픽 셋이 닫힌 순환을 이룬다 &amp;mdash; 입장 승인(admissions), 입장 회수(admissions-revoked), 예매 완료에 따른 자리 반환(bookings-completed).&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;요구 5 &amp;mdash; 브라우저 진입점.&lt;/b&gt; 화면은 정적 파일 세 개(HTML&amp;middot;JS&amp;middot;CSS)를 주는 &lt;b&gt;frontend&lt;/b&gt;가 맡고, &quot;어떤 경로를 어느 서비스로&quot;는 클러스터의 Ingress(Traefik)가 맡는다.&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;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;        브라우저  http://192.168.0.240   &amp;larr; MetalLB가 준 LB IP (LAN)
            │
        Traefik  (Ingress 3경로, 파드 2)
            │ /api/admission/*        │ 나머지 /api/*         │ /
            ▼                         ▼                      ▼
        ┌────────┐               ┌─────────┐            ┌──────────┐
        │ queue  │               │ booking │            │ frontend │
        │  (Go)  │               │ (Java)  │            │ (정적만)  │
        └─┬────┬─┘               └─┬─────┬─┘            └──────────┘
          │    │                   │     │
       Redis   │                   │   Redis (좌석락&amp;middot;admitted)
    (대기열 전부)│                   │   MySQL (확정 예매)
               │                   │
               └──── Kafka ────────┘
                admissions ─────────▶   (queue &amp;rarr; booking : 입장 승인)
                admissions-revoked ─▶   (queue &amp;rarr; booking : 입장 회수)
                ◀─ bookings-completed   (booking &amp;rarr; queue : 자리 반환)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.240에 닿는 과정은 요청마다 이렇다: 같은 LAN이라 브라우저가 &quot;.240 누구냐&quot;(ARP)를 물으면 &lt;b&gt;MetalLB&lt;/b&gt;가 노드 하나의 MAC으로 답해 패킷을 클러스터까지 실어 오고, &lt;b&gt;Traefik&lt;/b&gt;이 요청의 경로 문자열을 보고 세 규칙 중 하나를 고른다. 세 경로가 한꺼번에 열리는 게 아니라 &lt;b&gt;요청 하나가 가지 하나를 탄다&lt;/b&gt; &amp;mdash; 화면 한 번 로드에도 &lt;code&gt;/&lt;/code&gt;(정적 파일)와 &lt;code&gt;/api/movies&lt;/code&gt;(booking 왕복)가 따로 갈라져 들어간다. 이 배선의 원리(ARP&amp;middot;kube-proxy&amp;middot;Ingress의 층)는 9부에서 다뤘다.&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;b&gt;queue &amp;mdash; 입장 통제&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;enter ──정원 여유──▶ 200 ADMITTED ──────────────┐
  │                                            │ Kafka admissions
  └──정원 참──▶ 202 WAITING (순번)               ▼
        │                            booking이 admitted 키 발급
        │  순번 폴링(1/2/5초)
        ▼
   승격 루프(2초마다) ── 빈자리만큼 앞에서 꺼냄 ──▶ ADMITTED

   회수: 예매 완료 / 이탈(leave) / 세션 60초 초과  &amp;rarr;  빈자리  &amp;rarr;  다음 승격
   대기 이탈: 30초 안에 폴링 없으면 대기열에서 제거&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis에 있는 것은 명부 두 장이다&lt;/b&gt;. &lt;b&gt;입장자 명부(active)와 대기 줄 명부(waiting &amp;mdash; 들어온 순서가 곧 순번).&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;부속으로 대기자별 마지막 폴링 시각, 발행 대기 저널, 승격 누계(속도 계산 재료)가 같이 산다. queue 파드가 하는 일은 이 명부를 읽고 고치는 것뿐이라, 파드는 몇 개든 무상태다.&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;입장 판정은 Redis Lua 한 번이다.&lt;/b&gt; &quot;정원 확인 &amp;rarr; 입장 또는 대기 등록&quot;이 원자(중간에 아무도 못 끼어드는 한 단위)로 끝난다. 확인과 등록을 따로 하면 그 틈에 다른 요청이 껴 정원을 넘긴다 &amp;mdash; 파드가 여럿이라 앱 안의 잠금으로는 못 막고, Redis가 스크립트를 한 줄로 세워 직렬 실행하는 성질을 잠금으로 쓴다(요구 2에서 Redis를 고른 이유가 여기서 회수된다).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폴링이 곧 생존 신호다.&lt;/b&gt; 서버가 대기자에게 먼저 말을 걸 수단이 없다(연결을 안 붙잡는 폴링 설계 &amp;mdash; 연결을 유지하는 방식은 파드 증설&amp;middot;재시작마다 끊긴 연결을 수습해야 한다). 대신 순번 응답을 주면서 마지막 폴링 시각을 찍고, 30초 안에 안 물은 대기자는 지운다 &amp;mdash; 브라우저를 닫고 사라진 사람이 유령으로 남지 않게.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시계가 두 개다 &amp;mdash; 헷갈리기 쉬운 지점.&lt;/b&gt; 대기 중엔 &quot;30초 안에 폴링&quot;(생존 신호가 기준), 입장 후엔 &quot;60초 세션&quot;(입장 시각이 기준 &amp;mdash; 폴링해도 안 늘어난다). 대기열은 성실히 서 있으면 유지되고, 입장석은 시간이 되면 무조건 비워진다 &amp;mdash; 자리를 잡고 뭉개는 사람이 뒤를 막지 않게.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;발행 유실을 저널로 막는다.&lt;/b&gt; 입장&amp;middot;회수 사실은 Kafka로 booking에 가야 하는데, 상태 변경과 발행 사이에서 파드가 죽으면 통보가 유실된다 &amp;mdash; 화면상 입장했는데 booking이 몰라 계속 403인 사용자가 생긴다. 그래서 상태 변경과 같은 Lua 안에서 &quot;알릴 의무&quot;를 발행 대기 저널에 기록하고, 발행이 끝나야 지운다. 스윕 루프(5초)가 남은 항목을 재발행한다. 같은 통보가 두 번 갈 수는 있는데 소비 쪽이 멱등(두 번 받아도 결과가 같음)이라 무해하다 &amp;mdash; 중복은 흘리고 유실만 막는다.&lt;/li&gt;
&lt;/ul&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;b&gt;booking &amp;mdash; 게이트 뒤의 예매&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정원의 주인은 queue 하나다.&lt;/b&gt; booking은 &quot;지금 몇 명까지 받아도 되나&quot;를 스스로 판단하지 않는다 &amp;mdash; 판단 근거를 둘로 나누면 언젠가 서로 어긋난다.&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;booking이 믿는 것은 입장 인증 키(&lt;code&gt;admitted:{movie}:{requestId}&lt;/code&gt;) 하나뿐이고, 그 키의 수명은 queue가 보내는 이벤트가 관리한다: 승격되면 발급(admissions 수신), 자리를 잃으면 삭제(revoked 수신), 예매가 확정되면 소진. 키에 TTL 180초를 붙여 두는 것은 최후 방어다 &amp;mdash; 회수 이벤트가 유실돼도 인증이 영원히 남지는 않게. 게이트를 안 거친 요청은 회차 목록부터 확정까지 어디서도 403이다.&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;pre class=&quot;gams&quot;&gt;&lt;code&gt;빈자리      어느 쪽에도 없음
임시점유    Redis  SET NX, TTL 45초   &amp;larr; 결제 안 끝내면 자동으로 풀림
판매완료    MySQL  booking_seats 행    &amp;larr; 영구. 화면의 taken = 판매완료 &amp;cup; 임시점유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;임시&quot;와 &quot;영구&quot;를 저장소부터 갈라놓은 배치다 &amp;mdash; 좌석을 고르다 떠난 사람의 흔적은 TTL이 지우고, 돈이 오간 확정만 DB에 남는다.&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;code&gt;POST /api/bookings&lt;/code&gt;)은 순서가 고정돼 있다: 멱등키 확인 &amp;rarr; 게이트 &amp;rarr; 좌석 실재&amp;middot;판매 확인 &amp;rarr; 락 연장 &amp;rarr; PG 승인(mock) &amp;rarr; MySQL 트랜잭션 &amp;rarr; 커밋 후 후처리(락 해제&amp;middot;완료 발행&amp;middot;인증 소진). 멱등키는 &quot;이 요청은 아까 보낸 그 요청과 같은 것&quot;이라는 표식이다 &amp;mdash; 더블클릭&amp;middot;응답 유실 재시도가 두 번 결제로 이어지지 않게, 같은 키가 다시 오면 저장해 둔 결과를 그대로 돌려준다.&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; &amp;mdash; 결제까지 끝났는데 응답만 유실된 사용자가 재시도했을 때, 그 사이 세션이 만료됐다는 이유로 403에 막히면 돈은 빠졌는데 예매번호를 영영 못 받는다.&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;락은 필터고 UNIQUE가 보증이다.&lt;/b&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;1차 필터&lt;/td&gt;
&lt;td&gt;Redis 좌석락(SET NX)&lt;/td&gt;
&lt;td&gt;대부분을 입구에서 차단. TTL 만료&amp;middot;장애 전환에 뚫릴 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;판매 확인&lt;/td&gt;
&lt;td&gt;MySQL 조회&lt;/td&gt;
&lt;td&gt;확정 직후엔 락이 없어서, 팔린 좌석은 여기서 걸러짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최종 보증&lt;/td&gt;
&lt;td&gt;MySQL UNIQUE(회차, 좌석)&lt;/td&gt;
&lt;td&gt;위가 다 뚫려도 커밋 순간 한쪽만 이김. 진 쪽은 롤백+환불&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프론트 &amp;mdash; 클라이언트가 계약의 반쪽이다&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[로그인: requestId 발급(localStorage)]
    │
 movies ──클릭&amp;middot;enter──▶ waiting ──ADMITTED 발견──▶ entering ──▶ screenings ──▶ seats ──▶ pay ──▶ done
    ▲                  (순번 폴링)                 (전환 유예)
    └──────── EXPIRED(만료&amp;middot;이탈) / 홈&amp;middot;뒤로 ──────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 사용자에게 먼저 말을 걸지 않고(폴링 &amp;mdash; push 없음), 사용자별 세션도 파드에 들고 있지 않다(무상태). 그 결과 신원 만들기, 물어보는 박자, 떠날 때 뒷정리가 전부 클라이언트로 넘어온다 &amp;mdash; &lt;b&gt;서버 설계의 반쪽이 프론트에 구현돼 있는 셈&lt;/b&gt;이다. 프론트를 다시 만들 때 아래 여섯 줄이 빠지면 백엔드 설계가 반쪽이 된다.&lt;/p&gt;
&lt;table style=&quot;height: 148px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;몫&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;구현&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;신원&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;requestId = 자가발급 UUID(localStorage). 모든 요청에 실린다. 서버는 검증만, 발급 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;폴링&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;순번 뒤일수록 길게(1/2/5초) + jitter &amp;mdash; 동시 폴링 무리를 분산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;403 흡수&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;입장 직후 좌석&amp;middot;회차 403은 승격&amp;harr;인증 발급의 시간차 &amp;mdash; 0.3초 간격 최대 12회 재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;지연 안내&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;503 RETRY_LATER(발행 지연) &amp;rarr; 화면 유지 + 안내. 재시도하면 통과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;뒤로&amp;middot;홈 이동 시 좌석 해제(release)와 자리 반납(leave)을 짝으로 호출 &amp;mdash; 안 하면 TTL까지 유령 점유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;만료 감지&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;입장 후에도 5초 폴링 유지 &amp;mdash; 서버가 자리를 거두면(EXPIRED) 감지해 홈으로&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;E2E &amp;mdash; 실제로 흘러가는가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개통 확인.&lt;/b&gt; 외부 경로는 이미 git에 선언돼 라이브였다 &amp;mdash; 확인만 한다: LB IP 192.168.0.240 배정, 3경로 도달(&lt;code&gt;/&lt;/code&gt; 프론트 HTML &amp;middot; &lt;code&gt;/api/movies&lt;/code&gt; 시드 JSON &amp;middot; &lt;code&gt;/api/admission/*&lt;/code&gt; queue 응답), 지표 비노출(&lt;code&gt;/metrics&lt;/code&gt;&amp;middot;&lt;code&gt;/actuator&lt;/code&gt;는 프론트 폴백만), 공유기 DHCP 대역(2-199)과 MetalLB 풀(240-250) 안 겹침, Kafka 토픽 5종 READY. 이 시점부터 집 안 어느 기기든 브라우저로 데모에 닿는다.&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; 브라우저로 한 벌 &amp;mdash; 화면&amp;middot;폴링&amp;middot;전환이 사람에게 실제로 보이는가. API로 한 벌 &amp;mdash; 프론트가 부르는 것과 같은 API를 직접 불러 응답 계약을 분기까지 확인한다.&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;흐름 한 바퀴 &amp;mdash; 실측 타임라인.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;4명 enter      &amp;rarr; 앞의 2명 즉시 입장(200 ADMITTED), 3&amp;middot;4번째는 대기 1&amp;middot;2번(202 WAITING)
1번이 예매      &amp;rarr; 좌석 잠금(LOCKED) &amp;rarr; 확정(예매번호 발급)
+1초           &amp;rarr; 대기 1번이 ADMITTED &amp;mdash; 확정이 Kafka를 돌아 자리를 회수하고 승격까지 1초
입장자를 방치    &amp;rarr; 60초 세션 만료(EXPIRED) &amp;rarr; 같은 3초 관측창 안에서 다음 대기자 승격&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예매 완료 소비 로그에는 trace_id가 붙어 나온다 &amp;mdash; HTTP에서 Kafka 소비까지 추적이 이어진다는 표시다(실도달 검증은 관측 편에서).&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;API 열 개 전부, 분기 포함 통과.&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&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;enter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;즉시 입장 &amp;middot; 대기(순번&amp;middot;총원) &amp;middot; 재진입=자리 유지 &amp;middot; 대기 재진입=꼬리 밀림 &amp;middot; 형식 위반 400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;position&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;WAITING(순번&amp;middot;뒤 인원&amp;middot;ETA) &amp;middot; ADMITTED &amp;middot; EXPIRED &amp;middot; 형식 위반 400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;leave&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;이탈 &amp;rarr; 즉시 EXPIRED, 뒷사람 순번 전진&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;complete&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자리 반환(removed) &amp;rarr; EXPIRED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;movies&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;시드 응답 &amp;mdash; 게이트 없음(입장 전 진입점, 설계대로)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;screenings&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;입장자 200 &amp;middot; 미입장 403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;seats&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;좌석도 200칸(3상태 합성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;seats/select&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;잠금 &amp;middot; 경합 409 &amp;middot; 다좌석 전부-아니면-전무(실패 시 부분 잠금 없음까지 확인)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;seats/release&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;해제 &amp;rarr; 남이 재선점 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;bookings&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;확정 &amp;middot; 같은 멱등키 재시도=같은 예매번호(재결제 없음) &amp;middot; 같은 키+다른 좌석 409&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;&lt;b&gt;검증이 남긴 발견 셋.&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;b&gt;ETA는 아직 믿을 값이 아니다.&lt;/b&gt; 60초마다 2명 도는 상태에서 4번째 대기자에게 46분이 표시됐다 &amp;mdash; 실제 기대는 약 2분. 승격 속도 평균(EMA)의 기억이 초 단위라, &quot;한 번에 2명 승격 &amp;rarr; 60초 침묵&quot; 패턴을 평균하지 못하고 침묵 구간의 0에 눌린다. 승격 직후엔 십몇 초, 30초 뒤엔 수십 분, 곧 &quot;계산 중&quot;으로 널뛴다. 속도 계산을 공유 카운터 + 회전 주기보다 긴 창으로 바꾸는 재설계의 실물 근거다(결정은 시뮬레이터 절에서).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폴링을 멈춘 대기자는 30초 만에 사라진다.&lt;/b&gt; 대기자를 세워 두고 폴링 없이 예매 절차만 밟자 정확히 설계대로 강퇴됐다. 브라우저는 자동 폴링이라 안 밟는 함정인데, 가짜 대기자를 만드는 생성기는 전원 폴링 루프가 필수라는 제약이 실물로 확인됐다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멀티팟 첫 실증 &amp;mdash; 순번은 일치하고 ETA만 갈라진다.&lt;/b&gt; 폴링이 queue 두 파드에 번갈아 떨어지는데 순번&amp;middot;상태는 항상 같았다(상태가 Redis 하나라서). ETA만 파드마다 다르게 나온다(-1 &amp;harr; 13초 &amp;harr; 115초) &amp;mdash; 속도 평균이 파드 메모리에 살아서다. 로컬에서 검증된 적 없던 다중 replica 동작이 여기서 처음 확인됐다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 안 한 것.&lt;/b&gt; admissions 후커밋 재현(Redis 단절), UNIQUE 최종 방어 실도달(락 만료 창을 인위로 열어야 함), 503 RETRY_LATER(Kafka 발행 실패)는 전부 장애 재현 장치가 필요하다 &amp;mdash; 테스트 절과 장애 편으로 넘긴다. 검증이 만든 예매 3건은 DB 비우기 기능이 생기면 첫 청소 대상이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프론트 &amp;mdash; 시뮬레이터로 다시 만든다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예매 화면만 있던 프론트를 &lt;b&gt;티케팅 대기열 시뮬레이터&lt;/b&gt;로 다시 만든다. 왼쪽은 서비스(스토리&amp;middot;아키텍처 다이어그램&amp;middot;전체 현황&amp;middot;예매 카드), 오른쪽은 시뮬레이터 콘솔이다. 방문자가 콘솔에서 가상 관객을 넣고, 대기열이 도는 것을 지켜보다가, 직접 줄을 서서 예매까지 해보는 구성이다. 노출은 LAN까지 &amp;mdash; 인터넷 공개는 TLS&amp;middot;인증을 갖추는 14부에서 연다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1565&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwuxI1/dJMcagzKQNW/nYYZvmOWdIGSTk5fdRM6c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwuxI1/dJMcagzKQNW/nYYZvmOWdIGSTk5fdRM6c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwuxI1/dJMcagzKQNW/nYYZvmOWdIGSTk5fdRM6c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwuxI1%2FdJMcagzKQNW%2FnYYZvmOWdIGSTk5fdRM6c0%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;1565&quot; height=&quot;890&quot; data-origin-width=&quot;1565&quot; data-origin-height=&quot;890&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;콘솔이 하는 일&lt;/b&gt;&lt;/h4&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; 가상 관객 = 가짜 requestId로 enter하는 클라이언트다. 3초마다 전원이 position을 폴링한다(6명씩 병렬). 이 폴링이 생존 신호이자 상태 관찰이라, 멈추면 대기자는 30초 만에 서버가 회수한다. 투입은 프리셋 드롭다운으로 최대 200명 &amp;mdash; 이 상한은 브라우저 생성기의 한계(동시연결 6개로 생존 창 안에 전원 폴링)지 백엔드 정원이 아니다. 그 이상의 부하는 k6로 잰다(13부).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 예매&lt;/b&gt; &amp;mdash; 켜면 입장한 관객이 좌석을 예매하고 나간다. 예매 완료 이벤트가 Kafka로 queue에 돌아와 자리가 반환되고 다음 대기자가 승격된다 &amp;mdash; 줄이 도는 원리가 화면에 그대로 나온다. 끄면 세션 60초를 다 쓰고 나가 정원이 막힌 상황이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예매 오픈 예약&lt;/b&gt; &amp;mdash; 오픈 시각을 걸면 예매 버튼이 잠기고 시계가 카운트다운한다. 오픈 전 투입한 관객은 서버에 줄을 세우지 않고 대기실에 모아 뒀다가, 오픈 순간 러시 인원과 함께 한꺼번에 줄을 세운다(미리 줄 세우면 오픈에 들어오는 사람이 이미 진 채로 시작하므로). 게이트는 localStorage 프론트 연출이라 브라우저마다 독립이고, 서버는 이 시각을 모른다.&lt;/li&gt;
&lt;/ul&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;b&gt;백엔드에 추가한 것 셋&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;stats API&lt;/b&gt; &amp;mdash; &lt;code&gt;GET /api/admission/stats&lt;/code&gt;. 전체 대기&amp;middot;입장&amp;middot;누적 승격을 Redis 파이프라인 한 왕복으로 집계한다. 화면의 현황 타일이 이걸 3초마다 읽는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ETA 재설계&lt;/b&gt; &amp;mdash; 검증에서 파드마다 예상 시간이 제각각으로 갈라졌다(파드 메모리 EMA가 원인 &amp;mdash; 승격은 60초에 한 번 몰아 도는 주기 신호라 짧은 평활은 빈 구간에서 0으로 수렴한다). 승격 수를 Redis 초 단위 버킷에 기록하고 90초 창 전체를 합산해 평균 내는 방식으로 바꾼다. 값이 Redis에 있어 전 파드가 같은 ETA를 주고, 창이 회전 주기보다 길어 주기 신호에도 버틴다. 실측: 승격 12명 시점 rate 0.133/s, 6번째 대기자 ETA 45초 &amp;mdash; 계산과 일치.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 리셋&lt;/b&gt; &amp;mdash; 예매는 영구 기록이라 좌석이 다 팔리면 데모가 멈춘다. queue(대기열 키 전부)와 booking(예매&amp;middot;좌석락&amp;middot;입장인증 + 방송일을 실행일+7일로 재설정)에 초기화 API를 만든다. 인증은 X-Admin-Token 헤더 &amp;mdash; 토큰을 주입하지 않은 배포에는 라우트 자체가 등록되지 않는다. 화면의 ?ops 도구는 토큰을 입력받아 호출할 뿐, 판정은 서버만 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 테스트 &amp;mdash; API 계약을 게이트로 남긴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 파이프라인 5단(check&amp;middot;test&amp;middot;build&amp;middot;scan&amp;middot;publish) 중 test 칸이 비어 있다. 지금까지 이 앱의 동작 확인은 AI에게 &quot;코드를 훑고 검증해 달라&quot;고 시켜 한 번 돌린 것이고, 그 결과는 그때 화면에만 남았다. 다음 커밋이 같은 동작을 깨도 아무것도 울리지 않는다.&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;API가 서로에게 한 약속&lt;/b&gt;에 있다. 그래서 이 절은 API 목록에서 출발해, 그것들이 어떻게 맞물리는지 따라가고, 거기서 나오는 불변식을 테스트로 옮긴다.&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;b&gt;API 목록 &amp;mdash; 두 서비스가 여는 창구&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queue는 &quot;누가 들어갈 수 있나&quot;를, booking은 &quot;들어온 사람이 무엇을 살 수 있나&quot;를 소유한다. 창구도 그 경계대로 갈린다.&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;queue (Go) &amp;mdash; 입장 통제&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드&amp;middot;경로&lt;/th&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;&lt;code&gt;POST /api/admission/enter&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;정원을 원자로 확인해 입장 또는 대기 등록&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;200 &lt;code&gt;ADMITTED&lt;/code&gt;&amp;middot;&lt;code&gt;ALREADY_ACTIVE&lt;/code&gt; / 202 &lt;code&gt;WAITING&lt;/code&gt;+순번 / 400 / 503 &lt;code&gt;RETRY_LATER&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;GET /api/admission/position&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;내 순번&amp;middot;상태 조회. &lt;b&gt;호출 자체가 생존 신호를 겸한다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;O(생존 시각)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WAITING&lt;/code&gt;(순번&amp;middot;뒤 인원&amp;middot;예상) / &lt;code&gt;ADMITTED&lt;/code&gt; / &lt;code&gt;EXPIRED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;GET /api/admission/stats&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;전체 현황 집계(입장&amp;middot;대기&amp;middot;정원&amp;middot;신규 진입 예상)&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;숫자 묶음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;GET /api/admission/events&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최근 이벤트 피드(커서 &lt;code&gt;after&lt;/code&gt; 이후만)&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;이벤트 배열 + 마지막 id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;POST /api/admission/leave&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자발적 이탈 &amp;mdash; 입장&amp;middot;대기&amp;middot;생존 추적에서 모두 제거&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LEFT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;POST /api/admission/complete&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용자가 부르는 자리 반환 &amp;mdash; 입장에서만 제거&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMPLETED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;POST /api/admission/reset&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;대기열 상태 전부 삭제(운영용, 토큰 필요)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RESET&lt;/code&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;&lt;b&gt;booking (Java) &amp;mdash; 예매&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 202px;&quot;&gt;메서드&amp;middot;경로&lt;/th&gt;
&lt;th style=&quot;width: 305px;&quot;&gt;하는 일&lt;/th&gt;
&lt;th style=&quot;width: 155px;&quot;&gt;입장 인증&lt;/th&gt;
&lt;th style=&quot;width: 191px;&quot;&gt;응답&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;&lt;b&gt;&lt;code&gt;GET /api/movies&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 305px;&quot;&gt;상영 목록&lt;/td&gt;
&lt;td style=&quot;width: 155px;&quot;&gt;불필요&lt;/td&gt;
&lt;td style=&quot;width: 191px;&quot;&gt;목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;&lt;b&gt;&lt;code&gt;GET /api/screenings&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 305px;&quot;&gt;회차 목록 + 잔여 좌석&lt;/td&gt;
&lt;td style=&quot;width: 155px;&quot;&gt;필요&lt;/td&gt;
&lt;td style=&quot;width: 191px;&quot;&gt;목록 / 403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;&lt;b&gt;&lt;code&gt;GET /api/seats&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 305px;&quot;&gt;좌석도(빈자리&amp;middot;임시점유&amp;middot;판매완료를 합친 값)&lt;/td&gt;
&lt;td style=&quot;width: 155px;&quot;&gt;필요&lt;/td&gt;
&lt;td style=&quot;width: 191px;&quot;&gt;목록 / 403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;&lt;b&gt;&lt;code&gt;POST /api/seats/select&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 305px;&quot;&gt;좌석 임시 점유(다중 좌석 전부 성공 또는 전부 실패)&lt;/td&gt;
&lt;td style=&quot;width: 155px;&quot;&gt;필요&lt;/td&gt;
&lt;td style=&quot;width: 191px;&quot;&gt;200 &lt;code&gt;LOCKED&lt;/code&gt; / 409&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;&lt;b&gt;&lt;code&gt;POST /api/seats/release&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 305px;&quot;&gt;점유 해제&lt;/td&gt;
&lt;td style=&quot;width: 155px;&quot;&gt;필요&lt;/td&gt;
&lt;td style=&quot;width: 191px;&quot;&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;&lt;b&gt;&lt;code&gt;POST /api/bookings&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 305px;&quot;&gt;결제&amp;middot;예매 확정&lt;/td&gt;
&lt;td style=&quot;width: 155px;&quot;&gt;필요(멱등 재요청은 예외)&lt;/td&gt;
&lt;td style=&quot;width: 191px;&quot;&gt;200 예매번호 / 400 / 403 / 409&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 202px;&quot;&gt;&lt;b&gt;&lt;code&gt;POST /api/admin/reset&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 305px;&quot;&gt;예매&amp;middot;좌석&amp;middot;인증 삭제 + 방송일 재설정(운영용, 토큰 필요)&lt;/td&gt;
&lt;td style=&quot;width: 155px;&quot;&gt;토큰&lt;/td&gt;
&lt;td style=&quot;width: 191px;&quot;&gt;&lt;code&gt;RESET&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 서비스를 잇는 것은 HTTP가 아니라 Kafka 세 토픽이다. &lt;code&gt;admissions&lt;/code&gt;(queue &amp;rarr; booking, 입장 승인) &amp;middot; &lt;code&gt;admissions-revoked&lt;/code&gt;(queue &amp;rarr; booking, 입장 회수) &amp;middot; &lt;code&gt;bookings-completed&lt;/code&gt;(booking &amp;rarr; queue, 자리 반환). 서로를 직접 호출하지 않으므로 한쪽이 죽어도 다른 쪽이 함께 멈추지 않는다.&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;b&gt;API가 어떻게 맞물리나 &amp;mdash; 한 사람의 여정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 창구를 관통하는 열쇠는 &lt;b&gt;&lt;code&gt;requestId&lt;/code&gt; 하나&lt;/b&gt;다. 화면의 [익명으로 시작]을 누르면 브라우저가 UUID를 만들어 이 값으로 쓰고, 앞자리를 딴 라벨(&lt;code&gt;관객-8e9e&lt;/code&gt;)을 이름 대신 보여준다. 대기열 등록도 좌석 점유도 예매 확정도 전부 이 값으로 이뤄지고, 값은 브라우저에 저장돼 새로고침해도 유지된다 &amp;mdash; 새로고침 뒤에도 대기 순번이 이어지는 이유가 이것이다.&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; 형식만 확인하고(영숫자&amp;middot;&lt;code&gt;_&lt;/code&gt;&amp;middot;&lt;code&gt;-&lt;/code&gt;, 64자 이내), 그 값이 지금 대기 줄에 있는지&amp;middot;정원 안에 있는지&amp;middot;좌석을 잡았는지만 본다. 비밀번호도 서명도 없으므로 아무 값이나 만들어 보내도 받아준다 &amp;mdash; 가상 관객을 만드는 방법이 &lt;code&gt;bot-&lt;/code&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;code&gt;requestId&lt;/code&gt;를 알면 그 사람을 대기열에서 빼거나 좌석 점유를 풀 수 있다. 인터넷에 여는 단계에서 서명된 신원으로 바꾸는 것이 선행 조건이고, 이 편의 노출 범위를 LAN까지로 묶는 근거도 여기 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;클라이언트              queue                   Kafka                booking              저장소
    │
    │ POST /enter
    ├──────────────────▶ 정원 확인(Lua, 원자)
    │                     ├ 여유 있음 &amp;rarr; active 등록 ─── admissions ──▶ 인증 발급 ──▶ Redis(키+TTL)
    │ ◀── 200 ADMITTED    │
    │                     └ 정원 참  &amp;rarr; waiting 등록(진입 순서 score)
    │ ◀── 202 WAITING + 순번
    │
    │ GET /position  (1&amp;middot;2&amp;middot;5초 간격 반복)
    ├──────────────────▶ active에 있나 &amp;rarr; 없으면 waiting 순번 &amp;rarr; 둘 다 없으면 EXPIRED
    │ ◀── 상태 + 순번&amp;middot;예상        └ 이 호출이 '마지막으로 본 시각'을 갱신한다
    │
    │              ┌ 승격 루프(2초): 빈자리만큼 waiting 앞에서 active로 ─ admissions ─▶ 인증 발급
    │   백그라운드 ┤ 세션 타임아웃(10초): 오래된 active 회수 ───── admissions-revoked ─▶ 인증 삭제
    │              ├ 대기 타임아웃(10초): 폴링이 끊긴 waiting 회수
    │              └ 저널 스윕(5초): 발행이 끝나지 못한 이벤트 재발행
    │
    │ GET /screenings &amp;middot; /seats   (requestId 동봉)
    ├───────────────────────────────────────────────────▶ 인증 있나? ─ 없으면 403
    │ ◀── 회차&amp;middot;좌석
    │
    │ POST /seats/select
    ├───────────────────────────────────────────────────▶ 좌석락(Lua) ──▶ Redis(45초 TTL)
    │ ◀── 200 LOCKED / 409 이미 선점
    │
    │ POST /bookings  (idempotencyKey 동봉)
    ├───────────────────────────────────────────────────▶ 확정 8단계 ──▶ MySQL(UNIQUE)
    │ ◀── 200 예매번호                                          │
    │                     ◀───── bookings-completed ────────────┘
    │                     자리 반환(active에서 제거) &amp;rarr; 다음 승격이 그 자리를 채운다&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입장은 두 곳에 기록된다.&lt;/b&gt; queue의 정원 명단(active)과 booking의 입장 인증이다. 사용자가 &quot;들어갔다&quot;고 느끼는 순간과 booking이 그것을 아는 순간 사이에 Kafka 한 구간이 있다. 그래서 입장 직후의 회차 조회가 403으로 튈 수 있고, 화면은 그 틈을 짧은 재시도로 흡수한다. 인증을 booking이 스스로 만들지 않는 이유도 여기 있다 &amp;mdash; 정원 통제의 소유자를 한 곳(queue)으로 두기 위해서다.&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;code&gt;bookings-completed&lt;/code&gt;), 스스로 나가거나(&lt;code&gt;leave&lt;/code&gt;&amp;middot;&lt;code&gt;complete&lt;/code&gt;), 시간이 지나 서버가 회수한다(세션&amp;middot;대기 타임아웃). 어느 길로 나가든 queue의 명단에서 빠지고, 앞의 둘과 셋째는 &lt;code&gt;admissions-revoked&lt;/code&gt;로 booking의 인증도 지운다. 이 통보가 빠지면 자리를 잃은 사용자가 계속 예매 API를 통과한다.&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; 서버가 연결을 붙들지 않으므로 &quot;끊겼다&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;&lt;b&gt;흐름에서 나오는 불변식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림을 따라가면 &quot;무엇이 깨지면 사용자가 피해를 보는가&quot;가 나온다. 테스트가 지킬 대상은 이 목록이다.&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;입장 인원이 정원을 넘지 않는다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;정원 산정이 무의미해지고 booking이 감당 못 하는 부하를 받는다&lt;/td&gt;
&lt;td&gt;enter&amp;middot;승격 Lua의 원자 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;같은 신원의 재진입이 자리를 늘리지 않는다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;새로고침만으로 정원이 늘어난다&lt;/td&gt;
&lt;td&gt;enter의 멱등 분기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;대기 순번은 진입 순서를 지킨다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;늦게 온 사람이 먼저 들어간다&lt;/td&gt;
&lt;td&gt;대기 줄과 생존 추적을 다른 키로 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자리를 잃으면 예매 자격도 사라진다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;정원 밖 사용자가 좌석을 산다&lt;/td&gt;
&lt;td&gt;회수 이벤트 + 인증 TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;같은 좌석이 두 번 팔리지 않는다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;두 사람이 같은 자리 티켓을 받는다&lt;/td&gt;
&lt;td&gt;좌석락(1차) + MySQL 유니크 제약(최종)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;같은 멱등키는 같은 결과를 준다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;응답이 유실된 재시도가 이중 결제가 된다&lt;/td&gt;
&lt;td&gt;멱등키 조회가 확정보다 앞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;좌석 요청이 정규화된다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;중복 좌석이 결제 금액을 부풀리고, 순서 차이가 교착을 만든다&lt;/td&gt;
&lt;td&gt;중복 제거&amp;middot;정렬&amp;middot;개수 상한&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 줄의 &quot;순서 차이&quot;를 덧붙이면 &amp;mdash; 두 요청이 &lt;code&gt;[A1,A2]&lt;/code&gt;와 &lt;code&gt;[A2,A1]&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;&lt;b&gt;테스트를 어떻게 구축하나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트도 코드이고 실행기가 있다. 빌드에 &lt;code&gt;go build&lt;/code&gt;&amp;middot;&lt;code&gt;mvn package&lt;/code&gt;가 있듯 &lt;code&gt;go test&lt;/code&gt;&amp;middot;&lt;code&gt;mvn test&lt;/code&gt;가 있고, CI의 test 단계는 그 명령을 부르는 job이다. 실패하면 종료 코드가 0이 아니라서 파이프라인이 멈춘다.&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;Go(queue)&lt;/b&gt; &amp;mdash; 표준 라이브러리에 들어 있어 설치할 것이 없다. 대상 파일 옆에 &lt;code&gt;_test.go&lt;/code&gt;를 만들고 &lt;code&gt;Test&lt;/code&gt;로 시작하는 함수를 쓰면 &lt;code&gt;go test ./...&lt;/code&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;Java(booking)&lt;/b&gt; &amp;mdash; 관례는 같고 위치와 의존성이 다르다. 테스트는 &lt;code&gt;src/test/java&lt;/code&gt;에 두고, &lt;code&gt;pom.xml&lt;/code&gt;에 test 전용 의존성(&lt;code&gt;spring-boot-starter-test&lt;/code&gt;, &lt;code&gt;scope=test&lt;/code&gt;라 실행 이미지에는 안 들어간다)을 더한 뒤 &lt;code&gt;@Test&lt;/code&gt; 메서드를 쓴다. 이 저장소의 booking에는 &lt;code&gt;src/test&lt;/code&gt; 디렉터리 자체가 없어 그 추가가 첫 삽이다. 이미지 빌드가 &lt;code&gt;-DskipTests&lt;/code&gt;로 테스트를 건너뛰는 것은 의도된 분리다 &amp;mdash; build job은 이미지만 만들고, 테스트는 test job이 따로 돈다.&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;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 102px;&quot;&gt;층&lt;/th&gt;
&lt;th style=&quot;width: 188px;&quot;&gt;이 앱에서&lt;/th&gt;
&lt;th style=&quot;width: 565px;&quot;&gt;필요한 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 102px;&quot;&gt;&lt;b&gt;순수 로직&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 188px;&quot;&gt;좌석 요청 정규화, 식별자 형식 검증&lt;/td&gt;
&lt;td style=&quot;width: 565px;&quot;&gt;없다. 입력을 넣고 출력을 비교한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 102px;&quot;&gt;&lt;b&gt;Redis 위&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 188px;&quot;&gt;정원 확인, 승격, 만료 회수, 발행 저널&lt;/td&gt;
&lt;td style=&quot;width: 565px;&quot;&gt;&lt;b&gt;실제 Redis.&lt;/b&gt; 원자성이 Lua 스크립트에서 나오므로, 가짜로 흉내 내면 흉내 낸 자기 자신을 검증하게 된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 102px;&quot;&gt;&lt;b&gt;MySQL 위&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 188px;&quot;&gt;이중 판매 최종 방어, 멱등키 조회&lt;/td&gt;
&lt;td style=&quot;width: 565px;&quot;&gt;&lt;b&gt;실제 MySQL.&lt;/b&gt; 인메모리 대체는 유니크 위반 시점과 잠금 의미가 달라 통과가 증명이 되지 않는다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째 줄이 이 앱의 특성이다. 정원을 지키는 주체가 Go 코드가 아니라 Redis 스크립트라서, 앱 코드만 단위 테스트하면 정작 지켜야 할 것이 검증 범위 밖에 남는다. 실물을 붙이는 이유는 &quot;더 정확해서&quot;가 아니라 &lt;b&gt;검증 대상이 거기 있어서&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;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;&lt;b&gt;넣은 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 무게중심은 인프라이고 앱은 그 위에서 도는 대상이다. 그래서 앱 테스트는 &lt;b&gt;게이트가 실제로 도는 최소&lt;/b&gt;로 둔다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 137px;&quot;&gt;job&lt;/th&gt;
&lt;th style=&quot;width: 557px;&quot;&gt;대상&lt;/th&gt;
&lt;th style=&quot;width: 161px;&quot;&gt;붙인 실물&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 137px;&quot;&gt;&lt;b&gt;&lt;code&gt;test:queue-go&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;동시 요청 50개에도 입장이 정원을 넘지 않는가 &amp;middot; 같은 신원의 재진입이 자리를 늘리지 않는가&lt;/td&gt;
&lt;td style=&quot;width: 161px;&quot;&gt;Redis(서비스 컨테이너)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 137px;&quot;&gt;&lt;b&gt;&lt;code&gt;test:booking&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;좌석 요청 정규화 &amp;mdash; 중복 제거&amp;middot;순서 고정&amp;middot;개수 상한&amp;middot;빈 요청 거절&lt;/td&gt;
&lt;td style=&quot;width: 161px;&quot;&gt;없음(순수 로직)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;test:queue-go:
  stage: test
  image: golang:1.26
  services:
    - name: redis:7-alpine
      alias: redis
  variables:
    REDIS_ADDR: &quot;redis:6379&quot;
  script: [ cd queue-go, go test ./... ]
  rules: [ changes: [ queue-go/**/*, .gitlab-ci.yml ] ]   # 안 바뀐 서비스의 테스트는 돌지 않는다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 닿지 않으면 실패가 아니라 건너뛰게 했다 &amp;mdash; Redis 없이 &lt;code&gt;go test ./...&lt;/code&gt;를 돌리는 개발 중 실행이 빨간불이 되지 않게. CI에는 서비스 컨테이너가 붙어 있어 항상 실행된다.&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; 통과하는 것만으로는 부족하다 &amp;mdash; 항상 통과하는 테스트는 없는 것과 같다. 사본에서 정원 확인 조건을 &lt;code&gt;activeCount &amp;lt; maxSessions + 10&lt;/code&gt;으로 망가뜨리고 돌리니 이렇게 잡혔다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;--- FAIL: TestEnterKeepsCapacityUnderConcurrency
    입장 인원이 정원과 다르다: 정원 3, 입장 13
    저장된 입장 인원이 정원을 넘었다: 13(정원 3)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답만 세지 않고 저장된 명부(&lt;code&gt;ZCARD active&lt;/code&gt;)까지 확인하는 이유도 여기 있다. 응답이 맞아도 명부가 넘칠 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 단계는 lint&amp;middot;secret&amp;middot;dependency와 같은 축으로 &lt;code&gt;allow_failure&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;&lt;b&gt;넣지 않은 것 &amp;mdash; 앱이 커지면 필요한 순서&lt;/b&gt;&lt;/h4&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; 같은 키의 재시도가 같은 예매번호를 돌려주는지. 다만 예매번호만 비교하는 테스트는 반쪽이다. 같은 번호를 돌려주면서 결제를 한 번 더 했을 수도 있어서, &lt;b&gt;결제 승인 호출이 1회인지&lt;/b&gt;까지 확인해야 이중 결제를 막는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이중 판매 최종 방어&lt;/b&gt; &amp;mdash; 좌석락이 1차에서 걸러내므로 유니크 제약까지 도달시키려면 락을 일부러 우회시켜야 한다. 그러려면 락 계층을 갈아끼울 수 있어야 하고, 지금 구조가 그것을 허용하는지 확인이 먼저다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확정 경로 여덟 갈래 전수&lt;/b&gt; &amp;mdash; 지표 카운터의 결과 라벨 여덟 개(&lt;code&gt;success&lt;/code&gt;&amp;middot;&lt;code&gt;replay&lt;/code&gt;&amp;middot;&lt;code&gt;lock_expired&lt;/code&gt;&amp;middot;&lt;code&gt;seat_conflict&lt;/code&gt;&amp;middot;&lt;code&gt;forbidden&lt;/code&gt;&amp;middot;&lt;code&gt;no_screening&lt;/code&gt;&amp;middot;&lt;code&gt;bad_request&lt;/code&gt;&amp;middot;&lt;code&gt;error&lt;/code&gt;)와 하나씩 맞춘다. 폭이 넓어 한 번에 하면 오래 걸린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 간 계약&lt;/b&gt; &amp;mdash; 입장 인증은 queue가 만들고 Kafka를 건너 booking이 소비한다. 한 서비스 안에서는 이 계약이 검증되지 않는데, 두 서비스와 브로커를 함께 띄우는 테스트는 CI에서 무겁다. 지금은 배포 후 확인으로 남긴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시간에 매달린 항목&lt;/b&gt; &amp;mdash; 좌석락 45초&amp;middot;세션 60초&amp;middot;인증 180초&amp;middot;대기 이탈 30초. 실시간으로 기다리면 테스트가 분 단위가 되므로, 짧은 값을 주입할 수 있는 구조가 먼저 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리(teardown)는 지금부터 규약으로 둔다.&lt;/b&gt; 테스트가 만든 신원과 좌석은 끝에서 회수한다. 남기면 다음 테스트가 정원이 찬 상태에서 시작해 원인 없이 실패하고, 좌석은 영구 기록이라 재고가 줄어든다. 좌석이 4,000석뿐이라 이 규약이 없으면 테스트를 돌릴수록 매진에 가까워진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파드 사이 접근 범위 좁히기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 쪽 작업이 한 바퀴 끝났다. 다음은 통신 범위 좁히기다. 관측 배선과 k6 부하 실측이 다음 편들에서 이어지는데, 수치를 재기 시작한 뒤에 &quot;누가 어디에 붙을 수 있나&quot;를 바꾸면 다시 재야 한다 &amp;mdash; 그래서 재기 전에 먼저 좁힌다.&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;b&gt;파드는 컨테이너 하나 이상을 묶어 IP 하나를 공유하는 배포 단위&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;클러스터는 노드 3대이고, 객체는 네임스페이스로 갈라 둔다. 네임스페이스는 클러스터 안 객체를 이름으로 나누는 칸이다.&lt;/p&gt;
&lt;table style=&quot;height: 71px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;네임스페이스&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;들어 있는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;&lt;b&gt;&lt;code&gt;app&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;frontend(정적 파일 서빙) &amp;middot; queue(대기열 판정) &amp;middot; booking(예매 처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;&lt;code&gt;data&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;MySQL &amp;middot; Redis &amp;middot; Kafka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;&lt;code&gt;observability&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;지표&amp;middot;로그 수집과 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;막고 여는 단위는 파드와 포트 둘&lt;/b&gt;이다. 주소가 두 겹이라 그렇다 &amp;mdash; IP 가 어느 파드인지를 가르고, 포트가 그 파드 안의 어느 프로그램인지를 가른다. 파드 하나가 여러 포트를 열어 두면 그 포트마다 다른 상대가 붙는다.&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;code&gt;data&lt;/code&gt; 의 Redis 파드가 6379(데이터)&amp;middot;26379(마스터 조회)&amp;middot;9121(지표)을 함께 여는 것이 그렇다. 그래서 정책도 포트별로 따로 적는다.&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;code&gt;data&lt;/code&gt; 로 들어오는 접속까지다. &lt;code&gt;app&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;&lt;b&gt;무엇이 열려 있나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 안 어느 파드에서든 &lt;code&gt;data&lt;/code&gt; 의 MySQL 3306, Redis 6379&amp;middot;26379, Kafka 9092 에 도달한다. 정적 파일만 서빙하는 frontend 도, &lt;code&gt;observability&lt;/code&gt;&amp;middot;&lt;code&gt;argocd&lt;/code&gt;&amp;middot;&lt;code&gt;kube-system&lt;/code&gt; 의 파드도 MySQL 에 붙는다.&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;NetworkPolicy&lt;/b&gt; 라 부르고, 하나는 세 부분으로 되어 있다 &amp;mdash; &lt;b&gt;대상 파드&lt;/b&gt;, &lt;b&gt;방향&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kubectl get netpol -A&lt;/code&gt; 를 하면 8건이 나온다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 94px;&quot;&gt;네임스페이스&lt;/th&gt;
&lt;th style=&quot;width: 337px;&quot;&gt;이름&lt;/th&gt;
&lt;th style=&quot;width: 162px;&quot;&gt;만든 주체&lt;/th&gt;
&lt;th style=&quot;width: 177px;&quot;&gt;열린 포트&lt;/th&gt;
&lt;th style=&quot;width: 83px;&quot;&gt;보내는 쪽&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 94px;&quot;&gt;argocd&lt;/td&gt;
&lt;td style=&quot;width: 337px;&quot;&gt;&lt;b&gt;4건 (server&amp;middot;repo-server&amp;middot;application-controller&amp;middot;redis)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 162px;&quot;&gt;argo-cd 차트&lt;/td&gt;
&lt;td style=&quot;width: 177px;&quot;&gt;argocd 안쪽 &amp;mdash; 이번 대상 밖&lt;/td&gt;
&lt;td style=&quot;width: 83px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 94px;&quot;&gt;data&lt;/td&gt;
&lt;td style=&quot;width: 337px;&quot;&gt;&lt;b&gt;mysql&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 162px;&quot;&gt;&lt;code&gt;bitnami/mysql&lt;/code&gt; 14.0.3&lt;/td&gt;
&lt;td style=&quot;width: 177px;&quot;&gt;3306&lt;/td&gt;
&lt;td style=&quot;width: 83px;&quot;&gt;&lt;b&gt;안 적힘&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 94px;&quot;&gt;data&lt;/td&gt;
&lt;td style=&quot;width: 337px;&quot;&gt;&lt;b&gt;redis&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 162px;&quot;&gt;&lt;code&gt;bitnami/redis&lt;/code&gt; 27.0.15&lt;/td&gt;
&lt;td style=&quot;width: 177px;&quot;&gt;6379 &amp;middot; 26379 &amp;middot; 9121&lt;/td&gt;
&lt;td style=&quot;width: 83px;&quot;&gt;&lt;b&gt;안 적힘&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 94px;&quot;&gt;data&lt;/td&gt;
&lt;td style=&quot;width: 337px;&quot;&gt;&lt;b&gt;cgv-network-policy-kafka&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 162px;&quot;&gt;Strimzi 오퍼레이터&lt;/td&gt;
&lt;td style=&quot;width: 177px;&quot;&gt;9090 &amp;middot; 9091 &amp;middot; 8443&lt;/td&gt;
&lt;td style=&quot;width: 83px;&quot;&gt;지정돼 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 94px;&quot;&gt;data&lt;/td&gt;
&lt;td style=&quot;width: 337px;&quot;&gt;&lt;b&gt;cgv-network-policy-kafka&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 162px;&quot;&gt;Strimzi 오퍼레이터&lt;/td&gt;
&lt;td style=&quot;width: 177px;&quot;&gt;9092&lt;/td&gt;
&lt;td style=&quot;width: 83px;&quot;&gt;&lt;b&gt;안 적힘&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 94px;&quot;&gt;data&lt;/td&gt;
&lt;td style=&quot;width: 337px;&quot;&gt;&lt;b&gt;cgv-entity-operator&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 162px;&quot;&gt;Strimzi 오퍼레이터&lt;/td&gt;
&lt;td style=&quot;width: 177px;&quot;&gt;8080 &amp;middot; 8081&lt;/td&gt;
&lt;td style=&quot;width: 83px;&quot;&gt;&lt;b&gt;안 적힘&lt;/b&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;mysql&lt;/code&gt; 정책이 막는 것은 3306 아닌 포트뿐이고, 지켜야 할 포트는 3306 이다.&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;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; Helm 차트는 쿠버네티스 객체를 값에 따라 찍어 내는 템플릿 묶음이고, 그 값은 &lt;code&gt;values.yaml&lt;/code&gt; 에 적는다. MySQL 과 Redis 는 Bitnami 가 배포한 차트를 값만 바꿔 감싼 래퍼 차트(&lt;code&gt;cgv-mysql&lt;/code&gt;&amp;middot;&lt;code&gt;cgv-redis&lt;/code&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;code&gt;networkPolicy.enabled: true&lt;/code&gt;, &lt;code&gt;networkPolicy.allowExternal: true&lt;/code&gt; 다. &lt;code&gt;allowExternal: true&lt;/code&gt; 면 템플릿이 출처를 빼고 정책을 만든다. 손잡이는 래퍼의 &lt;code&gt;values.yaml&lt;/code&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;오퍼레이터가 만든 것.&lt;/b&gt; CRD 는 쿠버네티스에 새 객체 종류를 등록하는 정의이고, 그 정의대로 만든 객체 하나를 CR 이라 부른다. 오퍼레이터는 CR 을 지켜보다가 필요한 객체를 대신 만들어 두는 프로그램이고, Strimzi 는 Kafka 용 오퍼레이터다.&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;Kafka 쪽 정책은 Strimzi 가 Kafka CR 을 읽고 만든다. 9090&amp;middot;9091 은 브로커끼리 쓰는 복제와 제어, 8443 은 오퍼레이터가 쓰는 통로라 Strimzi 가 출처까지 적는다. 클라이언트가 붙는 9092 만 비어 있는데, 그 출처를 적는 자리인 리스너(브로커가 접속을 받는 창구 설정)에 아무것도 안 넣었기 때문이다. 정책을 지워도 오퍼레이터가 다시 만들어 놓으므로 지우는 방식은 안 통한다. 손잡이는 Kafka CR 이다.&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;b&gt;열려 있으면 무엇이 깨지나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스는 좌석 정원만큼만 예매 화면에 들여보내려고 앞에 대기열을 둔다. queue 가 Redis 에서 남은 정원을 확인해 통과한 요청만 Kafka 의 &lt;code&gt;admissions&lt;/code&gt; 토픽에 넣고, booking 이 그 토픽을 읽어 예매 자격을 발급한다. 열려 있는 포트 가운데 9092 는 그 통제가 통하지 않게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;정상   사용자 ─▶ queue ─(Redis 로 정원 확인)─▶ Kafka admissions ─▶ booking
우회   아무 파드 ─────────────────────────▶ Kafka admissions ─▶ booking
                                                                   │
                                admitted:{movie}:{requestId} 발급 = 예매 자격&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 줄의 차이는 queue 를 거치느냐 하나다. booking 은 메시지를 보낸 쪽을 확인하지 않아서, &lt;code&gt;admissions&lt;/code&gt; 에 메시지를 넣는 것이 곧 예매 자격이 된다. 대기열&amp;middot;순번&amp;middot;정원을 건너뛴다. 리스너는 &lt;code&gt;type: internal&lt;/code&gt;, &lt;code&gt;tls: false&lt;/code&gt;, 인증 설정 없음이고 &lt;code&gt;internal&lt;/code&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;막는 층이 셋이다 &amp;mdash; 메시지에 서명을 붙여 앱이 확인하는 것, 브로커가 접속자를 확인하는 것(아이디&amp;middot;비밀번호를 쓰는 SASL, 양쪽 인증서를 쓰는 mTLS), 애초에 도달 자체를 줄이는 것. 이번에 하는 건 세 번째다. 앱과 브로커 설정을 그대로 두고 도달 범위만 줄인다.&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;b&gt;NetworkPolicy &amp;mdash; 무엇을 적는 물건인가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책이 하나도 고르지 않은 파드는 들어오는 것도 나가는 것도 전부 허용된다. 네임스페이스는 이름 공간이지 패킷의 경계가 아니다. &lt;code&gt;app&lt;/code&gt; 의 파드와 &lt;code&gt;data&lt;/code&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;code&gt;data&lt;/code&gt; 의 MySQL 로 들어오는 접속을 &lt;code&gt;app&lt;/code&gt; 의 booking 파드가 3306 으로 보내는 것 하나로 제한한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mysql-ingress
  namespace: data                            # ① 정책이 사는 곳. 대상 파드도 여기서만 고른다
spec:
  podSelector:                               # ② 대상 &amp;mdash; 이 정책을 어느 파드에 붙이나
    matchLabels:
      app.kubernetes.io/name: mysql          #    이 라벨을 단 파드 = mysql-0
  policyTypes:
    - Ingress                                # ③ 방향 &amp;mdash; 들어오는 쪽만 다룬다
  ingress:                                   # ④ 허용 목록. 여기 맞는 것만 통과, 나머지는 폐기
    - from:                                  # ⑤ 출처 &amp;mdash; 누가 보내는 것을 허용하나
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: app     # 쿠버네티스가 모든 네임스페이스에
          podSelector:                             # 자동으로 붙이는 라벨. 네임스페이스도
            matchLabels:                           # 라벨로만 고를 수 있어 이것을 쓴다
              app.kubernetes.io/name: booking
      ports:                                 # ⑥ 통로 &amp;mdash; 어느 포트로 오는 것을 허용하나
        - port: 3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽는 축은 ② 와 ④ 다. &lt;b&gt;정책이 파드를 고르는 순간 그 파드로 들어오는 접속이 전부 막히고, &lt;code&gt;ingress&lt;/code&gt; 에 적은 것만 다시 통과한다.&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;대상은 라벨로만 고른다.&lt;/b&gt; 파드 이름은 &lt;code&gt;booking-5cff7c7fbb-wn4ph&lt;/code&gt; 처럼 배포마다 바뀌고 IP 도 바뀐다. &lt;code&gt;podSelector: {}&lt;/code&gt; 는 그 네임스페이스 전부를 뜻한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대상은 정책이 있는 네임스페이스 안에서만 고르고, 출처는 다른 네임스페이스에서도 고른다.&lt;/b&gt; 그래서 app 에서 data 로 가는 통신을 제한하는 정책은 받는 쪽인 &lt;code&gt;data&lt;/code&gt; 에 둔다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;from&lt;/code&gt; 과 &lt;code&gt;ports&lt;/code&gt; 는 AND 로 읽고, 안 적은 축은 제한하지 않는다.&lt;/b&gt; 차트와 오퍼레이터가 만든 정책에 출처가 비어 있는 것이 이것이다 &amp;mdash; &lt;code&gt;ports&lt;/code&gt; 만 있으면 어디서 왔든 통과한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;ingress: []&lt;/code&gt; 는 전부 차단, &lt;code&gt;ingress: [{}]&lt;/code&gt; 는 전부 허용이다.&lt;/b&gt; 규칙 개수가 뜻이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라벨로 못 고르는 출처는 &lt;code&gt;ipBlock&lt;/code&gt; 으로 적는다.&lt;/b&gt; 파드는 &lt;code&gt;10.42.0.0/16&lt;/code&gt; 대역을 받고 노드 IP 는 그 밖이라, 노드에서 오는 접속은 파드 셀렉터에 안 잡히고 &lt;code&gt;ipBlock&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;&lt;code&gt;from&lt;/code&gt; 안에서는 하이픈 하나가 AND 와 OR 를 가른다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;- from:
    - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: app}}
      podSelector: {matchLabels: {app.kubernetes.io/name: booking}}
      # 원소 하나 안에 셀렉터 둘 = AND &amp;rarr; app 의 booking 파드만

- from:
    - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: app}}
    - podSelector: {matchLabels: {app.kubernetes.io/name: booking}}
      # 원소 둘 = OR &amp;rarr; app 전체, 또는 정책과 같은 네임스페이스의 booking 파드&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;p data-ke-size=&quot;size16&quot;&gt;문법에서 한 가지가 더 따라 나오고, 이것이 작업 순서를 정한다. &lt;b&gt;여러 정책이 같은 파드를 고르면 허용의 합집합이 적용된다.&lt;/b&gt; 우선순위도 거부도 없어서 좁은 정책을 더해도 넓은 정책이 열어 둔 것이 남는다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;정책 A (차트가 생성):  podSelector=mysql,  ingress[{ports:[3306]}]                 # from 없음
정책 B (직접 작성):    podSelector=mysql,  ingress[{from:[booking], ports:[3306]}]

A + B  &amp;rarr;  3306 은 어디서든 통과        B 만  &amp;rarr;  booking 에서만 통과&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좁히려면 B 를 더하는 것으로는 안 되고 A 를 없애야 한다. 차트가 만드는 정책을 꺼야 하는 이유가 이것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 파일에 적는 내용이고, &lt;code&gt;kubectl apply&lt;/code&gt; 를 하면 클러스터의 모든 객체를 받아 두는 API 서버에 저장된다. 저장된 글자가 패킷을 막지는 않는다.&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;b&gt;그 선언이 패킷을 막기까지 &amp;mdash; Calico&lt;/b&gt;&lt;/h4&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;파드에 IP 를 주는 일과 다른 노드의 파드까지 패킷을 보내는 일은 하지 않는다.&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;그 자리는 &quot;프로그램을 하나 꽂아서 시켜라&quot; 로 비워 두었고, 그 프로그램이 지켜야 할 규격이 CNI(Container Network Interface)다. Calico 는 그 자리에 꽂힌 프로그램이고 두 가지를 한다 &amp;mdash; &lt;b&gt;길을 놓는 일&lt;/b&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;h5&gt;&lt;b&gt;길&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드가 뜰 때 CNI 플러그인이 veth 쌍을 만든다. 랜선 하나의 양 끝 같은 것으로, 한쪽 끝은 파드 안에 들어가 &lt;code&gt;eth0&lt;/code&gt; 이 되고 반대쪽 끝은 노드에 남아 &lt;code&gt;cali&lt;/code&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;code&gt;10.42.0.0/16&lt;/code&gt;, 노드는 LAN 주소(192.168.0.201&amp;middot;202&amp;middot;203)다. 대역이 달라 파드 IP 는 LAN 에서 그대로 오갈 수 없어서, 노드를 넘을 때 원본 패킷을 노드 IP 봉투에 넣어 보내고 받는 쪽에서 벗긴다. 이 방식이 VXLAN 이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;파드A ─veth─▶ k3s-1 ─[노드 IP 봉투]─▶ k3s-3 ─[벗김]─▶ veth ─▶ 파드B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;판정&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷이 파드로 들어가려면 마지막에 그 파드의 veth 를 지난다. Calico 는 &lt;b&gt;veth 의 노드 쪽 끝마다 전용 규칙 묶음을 붙인다.&lt;/b&gt; 들어가는 방향과 나가는 방향이 따로 있어 &lt;code&gt;ingress&lt;/code&gt; 와 &lt;code&gt;egress&lt;/code&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;패킷을 통과시키거나 버리는 것은 리눅스 커널이고, 커널이 그 판정에 참조하는 규칙표가 iptables 다. 규칙 묶음을 체인이라 부르며, Calico 가 만드는 체인은 이름이 &lt;code&gt;cali&lt;/code&gt; 로 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;라벨이 IP 로 바뀌는 곳&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iptables 는 IP 와 포트로만 판정한다. 라벨은 문자열이라 커널이 모른다. 그 사이를 ipset 이 메운다 &amp;mdash; IP 를 묶어 두고 규칙에서 한 덩어리로 참조하는 커널 목록이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;정책에 적은 것   app 의 booking 라벨을 단 파드
      ▼          Calico 가 그 라벨을 단 파드의 IP 를 모은다
ipset            { 10.42.207.61 }
      ▼
iptables 규칙    &quot;출발지가 이 묶음에 있으면 통과&quot;&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;h5&gt;&lt;b&gt;선언을 규칙으로 옮기는 것&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;veth 에 체인을 붙이고 ipset 을 채우는 일은 Felix 가 한다. 파일에 적은 정책이 거기까지 가는 경로다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;kubectl apply ─▶ API 서버 ─watch─▶ 각 노드의 Felix ─▶ 그 노드의 iptables &amp;middot; ipset&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Felix 는 노드마다 하나씩 도는 &lt;code&gt;calico-node&lt;/code&gt; 안의 데몬이고, 자기 노드에 있는 파드까지만 규칙을 쓴다. 정책이나 파드가 바뀌면 달라진 부분만 다시 쓴다. &lt;code&gt;calico-system&lt;/code&gt; 의 나머지 파드는 이 경로를 거드는 것들이다 &amp;mdash; &lt;code&gt;calico-typha&lt;/code&gt; 는 Felix 들이 API 서버를 각자 지켜보지 않도록 중계하고, &lt;code&gt;install-cni&lt;/code&gt; 는 노드에 CNI 플러그인을 설치하고, 나머지는 정리 작업과 Calico 전용 API 를 맡는다.&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;규칙을 iptables 에 넣을지 eBPF 로 넣을지는 &lt;code&gt;felixconfiguration&lt;/code&gt; 이 정한다. 지금은 &lt;code&gt;bpfEnabled: false&lt;/code&gt; 라 iptables 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;flannel 과의 차이&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 가 기본으로 설치하는 CNI 플러그인은 flannel 이다. &lt;b&gt;flannel 은 길만 놓는다.&lt;/b&gt; 판정은 k3s 에 내장된 별도 컨트롤러가 맡는다. flannel 을 쓰면 NetworkPolicy 가 적용되지 않는 것이 아니라 두 프로그램이 나눠 맡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;k3s 기본   flannel(길)  +  k3s 내장 컨트롤러(판정)
지금       Calico(길 + 판정)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Calico 에는 거부 규칙과 우선순위가 있는 전용 정책 API 도 딸려 온다. 그것으로 쓰면 Calico 에서만 해석되어 CNI 를 바꿀 때 다시 써야 하므로, 이번에 쓰는 것은 표준 NetworkPolicy 뿐이다.&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;k3s 설치 때 &lt;code&gt;config.yaml&lt;/code&gt; 에 넣은 두 줄이 이 구성을 만든다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;flannel-backend: none          # 길을 놓는 k3s 기본 플러그인을 끈다
disable-network-policy: true   # 판정하는 k3s 내장 컨트롤러를 끈다&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;&lt;b&gt;무엇을 남길지 정한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법과 집행 경로가 정해졌으니 남은 것은 출처에 무엇을 적을지다. &lt;code&gt;data&lt;/code&gt; 로 들어오는 접속을 일단 전부 막고 필요한 것만 다시 여는 것이므로, 지금 들어오는 것을 빠짐없이 세야 한다. &lt;b&gt;목록에서 빠진 것은 적용하는 순간 끊긴다.&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;&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;앱이 쓰는 것&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt; 의 queue &amp;middot; booking&lt;/td&gt;
&lt;td&gt;앱 env 와 차트 값에 적혀 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;data&lt;/code&gt; 안에서 자기들끼리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Redis 복제&amp;middot;Sentinel 투표, Kafka 브로커 간, Strimzi 오퍼레이터에서 브로커로&lt;/td&gt;
&lt;td&gt;구조를 알면 나온다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;클러스터가 보내는 것&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;kubelet 의 상태 검사, Alloy 의 지표 수집&lt;/td&gt;
&lt;td&gt;&lt;b&gt;배선한 적이 없어서 조회해야 나온다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;h5&gt;&lt;b&gt;kubelet 의 상태 검사&lt;/b&gt;&lt;/h5&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;p data-ke-size=&quot;size16&quot;&gt;찌르는 방식에 따라 정책에 걸리는지가 갈린다. &lt;code&gt;exec&lt;/code&gt; 는 컨테이너 안에서 명령을 실행하므로 네트워크를 안 타고, &lt;code&gt;httpGet&lt;/code&gt; 은 노드에서 파드 IP 로 요청을 보내므로 판정 대상이 된다. &lt;code&gt;data&lt;/code&gt; 의 파드를 전부 조회한 결과다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;th&gt;지금 걸린 정책&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MySQL &amp;middot; Redis &amp;middot; Kafka 브로커&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;exec&lt;/td&gt;
&lt;td&gt;안 탄다&lt;/td&gt;
&lt;td&gt;각자 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Redis 파드의 지표 사이드카&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;httpGet 9121&lt;/td&gt;
&lt;td&gt;&lt;b&gt;탄다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;redis (차트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;entity-operator&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;httpGet 8080 &amp;middot; 8081&lt;/td&gt;
&lt;td&gt;&lt;b&gt;탄다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;cgv-entity-operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;strimzi-cluster-operator&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;httpGet 8080&lt;/td&gt;
&lt;td&gt;&lt;b&gt;탄다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;없다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째 줄의 사이드카는 파드 하나에 컨테이너를 둘 이상 담을 수 있어서 생긴 것이다. Redis 파드 안에 지표를 내놓는 컨테이너가 따로 돌고, 그 포트가 9121 이다.&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;kubelet 은 노드 주소에서 접속한다. 출발지가 파드가 아니라 노드라 라벨로 못 고르고 &lt;code&gt;ipBlock&lt;/code&gt; 에 노드 IP 를 적어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;마지막 줄만 지금 어떤 정책에도 걸려 있지 않다.&lt;/b&gt; &lt;code&gt;data&lt;/code&gt; 전체를 고르는 정책을 넣으면 이 파드가 처음으로 선택되면서 8080 이 막히고, 재시작이 반복되면 Kafka 리소스 조정이 멈춘다. 허용 규칙을 같이 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;Alloy 의 지표 수집&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Alloy 는 &lt;code&gt;observability&lt;/code&gt; 에서 도는 지표 수집 에이전트다. 무엇을 긁을지는 ServiceMonitor 객체에 적혀 있고, &lt;code&gt;data&lt;/code&gt; 에 있는 것은 &lt;code&gt;redis&lt;/code&gt; 하나다. 9121 을 긁는다. Kafka 와 MySQL 은 ServiceMonitor 가 없어 긁는 것이 없다.&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;Alloy 도 kubelet 처럼 노드마다 하나씩 뜨지만 자기 파드 네트워크를 쓴다. &lt;b&gt;출발지가 노드 IP 가 아니라 파드 IP 라 라벨로 고른다.&lt;/b&gt; 같은 9121 인데 kubelet 은 &lt;code&gt;ipBlock&lt;/code&gt;, Alloy 는 셀렉터로 적게 되는 이유다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;손댈 곳&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정책 4개를 새로 쓴다&lt;/b&gt; (&lt;code&gt;workloads/manifests/netpol/data-ingress.yaml&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;&lt;code&gt;default-deny-ingress&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data&lt;/code&gt; 전체&lt;/td&gt;
&lt;td&gt;없음. 판정 대상으로 만드는 것이 일이다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;mysql-ingress&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;mysql&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt; 의 booking 에서 3306&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;redis-ingress&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;redis&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt; 의 queue&amp;middot;booking 에서 6379&amp;middot;26379 / Alloy&amp;middot;노드 3대에서 9121&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;strimzi-operator-probe&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 오퍼레이터&lt;/td&gt;
&lt;td&gt;노드 3대에서 8080&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;Kafka 브로커와 entity-operator 는 Strimzi 가 만든 정책이 이미 고르고 있고 허용은 합집합이라 그대로 통과한다. 새로 열 것이 없다.&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;Redis 는 두 포트가 함께 필요하다. 클라이언트가 26379(Sentinel)로 마스터 주소를 받고 그 주소로 6379 에 다시 접속하므로, 26379 만 열면 주소 조회는 되고 다음 명령에서 멈춘다.&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;차트 정책 2건을 끈다&lt;/b&gt; (&lt;code&gt;cgv-mysql&lt;/code&gt;&amp;middot;&lt;code&gt;cgv-redis&lt;/code&gt; 의 &lt;code&gt;values.yaml&lt;/code&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;networkPolicy.enabled: false&lt;/code&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;Kafka CR 에 출처를 적는다&lt;/b&gt; (&lt;code&gt;kafka-cluster.yaml&lt;/code&gt; 리스너의 &lt;code&gt;networkPolicyPeers&lt;/code&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9092 의 출처는 &lt;code&gt;app&lt;/code&gt; 의 queue 와 booking 이다. 리스너에 적으면 오퍼레이터가 정책을 좁게 다시 만든다.&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;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;Kafka 는 무인증 평문 그대로다. 허용된 라벨을 단 파드는 인증 없이 &lt;code&gt;admissions&lt;/code&gt; 에 발행한다&lt;/li&gt;
&lt;li&gt;라벨은 누구나 적을 수 있는 문자열이라 이 제한은 출처를 증명하지 않는다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app&lt;/code&gt; 에서 나가는 방향, 클러스터 밖에서 traefik 을 거쳐 들어오는 경로는 안 건드린다&lt;/li&gt;
&lt;/ul&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;b&gt;결과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;data&lt;/code&gt; 로 들어오는 접속이 이렇게 바뀌었다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&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;MySQL 3306&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 안 아무 파드나&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt; 의 booking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Redis 6379&amp;middot;26379&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;아무 파드나&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt; 의 queue&amp;middot;booking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Redis 9121&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;아무 파드나&lt;/td&gt;
&lt;td&gt;Alloy &amp;middot; 노드 3대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Kafka 9092&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;아무 파드나&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt; 의 queue&amp;middot;booking &amp;middot; 브로커 자신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;data&lt;/code&gt; 의 정책&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8건, 출처 없음 셋&lt;/td&gt;
&lt;td&gt;6건, 출처 전부 지정&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;라벨이 없는 파드와 있는 파드에서 각각 붙어 확인했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;observability, 라벨 없음    mysql 3306 &amp;middot; redis 6379 &amp;middot; kafka 9092   전부 timed out
app, booking 라벨          같은 셋                                 전부 open&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책은 패킷을 폐기하지 거절 응답을 보내지 않아 타임아웃으로 끝난다. 뒤쪽이 통과한다는 것이 &lt;b&gt;라벨 하나로 갈린다&lt;/b&gt;는 뜻이기도 하다 &amp;mdash; 두 번째 파드는 busybox 인데 라벨만 달고 통과했다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;걸린 것 둘&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브로커 안에서 도는 관리 CLI 가 멈췄다.&lt;/b&gt; &lt;code&gt;kafka-topics.sh --describe&lt;/code&gt; 가 타임아웃으로 끝났다. 클라이언트는 접속한 브로커에서 리더 목록을 받은 뒤 각 리더에 9092 로 직접 붙는데, 출처를 앱으로만 두니 브로커에서 브로커로 가는 9092 가 막혔다. 서비스 경로는 영향이 없다 &amp;mdash; 복제는 9091, 컨트롤러는 9090, 오퍼레이터는 8443 을 쓴다. 9092 출처에 브로커 자신을 더해 풀었다. 브로커는 이미 그 데이터를 보관하는 주체라 열리는 범위가 늘지 않는다.&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;mysql 차트 정책이 안 지워졌다.&lt;/b&gt; 같은 변경인데 redis 는 반영되고 mysql 은 &lt;code&gt;OutOfSync&lt;/code&gt; 로 남았다. 두 Application 의 &lt;code&gt;prune&lt;/code&gt; 설정이 다르다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;prune: true    git 에서 없앤 객체를 클러스터에서도 지운다     redis
prune: false   git 에서 없어져도 클러스터에는 남겨 둔다      mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mysql&lt;/code&gt; 은 StatefulSet 과 PVC 가 실수로 빠질 때 볼륨이 지워지는 것을 막으려고 &lt;code&gt;prune&lt;/code&gt; 을 꺼 두었고, 그 설정이 정책 삭제까지 함께 막았다. &lt;code&gt;selfHeal: true&lt;/code&gt; 는 둘 다 켜져 있지만 그것은 git 에 있는 객체를 git 값으로 되돌리는 장치라 여기에 관여하지 않는다. 남은 정책은 손으로 지웠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 대조 &amp;mdash; 이은 것과 빌린 것&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;간극은 메워졌나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10부 끝의 상태는 &quot;코드가 클러스터까지 가는 길은 전부 자동인데, 그 길 끝에서 도는 앱은 검증된 적이 없다&quot;였다. 지금은 대기 입장에서 예매 확정까지가 클러스터 위에서 실제로 돌았고, 그 흐름이 시뮬레이터로 화면에 보이고, 흐름의 약속은 테스트가 되어 CI 의 test 칸을 채웠고, &lt;code&gt;data&lt;/code&gt; 로 들어오는 접속은 출처가 지정된 것만 남았다. 검증 안 된 앱은 없어졌다 &amp;mdash; 남은 것은 검증 안 된 값들이고, 그 대장은 번외로 넘겼다.&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;b&gt;이 편 안에서 닫은 것&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;E2E 왕복            대기 입장 &amp;rarr; 좌석 선점 &amp;rarr; 예매 확정이 LAN 에서 끝까지 돌았다
결함 4건            추적 등록 원자화 &amp;middot; 추적 해제 신설 &amp;middot; 입력 검증 &amp;middot; revoked DLT 토픽
ETA                파드마다 제각각이던 예상 시간 &amp;rarr; Redis 버킷 90초 창 합산으로 재설계
초기화 API          ADMIN_TOKEN 이 인증이자 존재 스위치 &amp;mdash; 미주입 배포에는 라우트 자체가 없다
CI test 칸          queue 정원 불변식 &amp;middot; booking 좌석 정규화 + Redis 미도달 시 실패 가드
data 접속           정책 8건(출처 없음 셋) &amp;rarr; 6건(출처 전부 지정)
README 정정         토픽 &quot;둘&quot;&amp;rarr;5종 &amp;middot; compose 검증 단계 서술 &amp;middot; VIRTUAL_THREADS 잔재&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;&lt;b&gt;무엇을 빌렸나 &amp;mdash; 부채 장부&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Kafka 무인증 평문       허용 라벨을 단 파드는 인증 없이 발행한다 &amp;mdash; 인증&amp;middot;TLS 는 인터넷 공개 편
발행 저널 지표 없음      저널 크기를 읽는 함수는 있는데 호출부가 0곳 &amp;mdash; 12부
queue 결과 카운터 없음   회수가 세션 만료인지 대기 이탈인지 수치로 못 가른다 &amp;mdash; 12부
마이그레이션 도구 없음    booking replica 가 1 에 묶여 파드 증설 실험이 막혀 있다 &amp;mdash; 13부 블로커
시계 계약이 git 밖       좌석락 45초&amp;middot;승격 배치 100&amp;middot;이탈 판정 30초는 코드 기본값에만 있다
처리율 창 90초          설계 규칙(세션 미만)과 정면 충돌 &amp;mdash; 13부 예상 시간 채점으로 판정
커밋 메시지&amp;ne;내용 2건     스쿼시 머지가 낡은 MR 제목을 커밋 메시지로 남겼다 &amp;mdash; dev 역사에 그대로&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;&lt;b&gt;실무&amp;middot;보안 되짚기&lt;/b&gt;&lt;/h4&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; NetworkPolicy 로 출처를 좁혔지만 판정 근거는 라벨이고, 라벨은 누구나 적을 수 있는 문자열이다 &amp;mdash; 라벨만 단 busybox 가 통과하는 것을 실측으로 확인했다. 이 제한은 실수와 우회 경로를 막는 층이지 신원 증명이 아니다. 신원은 워크로드 인증(mTLS&amp;middot;SASL)의 몫이고, 트리거는 인터넷 공개다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파괴적 API 는 존재부터 조건부로 한다.&lt;/b&gt; 초기화 API 는 토큰 인증에 더해, 토큰이 주입되지 않은 배포에서는 라우트 자체가 등록되지 않는다. &quot;관리용 API 가 실수로 프로덕션에 열려 있었다&quot;는 사고 유형을 설정 실수로 막는 게 아니라 구조로 막는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조용히 건너뛰는 테스트는 게이트가 아니다.&lt;/b&gt; 테스트가 Redis 에 못 닿으면 로컬 실행은 건너뛰고 CI 는 실패한다. 이 구분이 없으면 서비스 컨테이너 배선이 깨진 날부터 게이트가 아무것도 지키지 않으면서 통과로 보인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스쿼시 머지는 MR 제목을 커밋 메시지로 만든다.&lt;/b&gt; 이 편에서 내용과 메시지가 다른 커밋이 dev 에 두 번 남았다 &amp;mdash; 브랜치가 바뀌었는데 MR 제목이 낡은 채로 머지 버튼을 눌러서다. 머지 다이얼로그의 커밋 메시지 칸을 확인하는 것이 절차다. 커밋 역사는 나중에 감사하는 쪽이 읽는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 다음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값의 대장 &amp;mdash; 무엇이 가상이고, 설계 문서와 실물이 어디서 갈라졌고, 관측이 무엇을 보고 못 보는지 &amp;mdash; 는 번외(설정 대장)에 있다. 12부와 13부가 그것을 입력으로 쓴다.&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;12부는 관측의 빈 곳을 채운다. 코드에만 있는 계측이 화면까지 실제로 닿는지 확인하고(트레이스&amp;middot;로그), 없는 신호를 배선하고(Kafka&amp;middot;MySQL 지표, 빠진 카운터), 그 위에 앱 대시보드와 클러스터 대시보드를 세운다. 서류에 필요한 것이 여기서 완성된다. 13부는 그 계기판으로 잰다 &amp;mdash; 파드 1대의 한계, 파드를 늘렸을 때의 곱, 정원&amp;middot;리소스 확정, 장애 주입, 그리고 stg/prd 스펙 산정.&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/222</guid>
      <comments>https://zed6740.tistory.com/222#entry222comment</comments>
      <pubDate>Tue, 11 Aug 2026 21:52:31 +0900</pubDate>
    </item>
    <item>
      <title>노트북 홈서버 k3s 구축기 - 10. CI 와 CD</title>
      <link>https://zed6740.tistory.com/221</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 &quot;노트북 홈서버 k3s 구축기&quot; 10부.&lt;br /&gt;지난 편까지 = GitOps 원본이 데스크탑 GitLab이고, push하면 3초 만에 클러스터에 반영된다. 다만 배포되는 것은 인프라뿐이고 앱 세 종은 이미지가 없어 뜨지 않는다.&lt;br /&gt;10부 = CI와 CD. 앞 토막(CI)은 push 한 번으로 검증된 이미지가 레지스트리에 서게 하고, 뒤 토막(CD)은 그 이미지가 노드에서 돌게 하고 무엇이 도는지 git이 적고 있게 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 이어받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9부에서 GitOps 원본을 데스크탑 GitLab으로 옮겼다. &lt;code&gt;cgv-infra&lt;/code&gt;에 push하면 webhook을 타고 3초 만에 클러스터에 반영된다.&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;code&gt;queue&lt;/code&gt;&amp;middot;&lt;code&gt;booking&lt;/code&gt;&amp;middot;&lt;code&gt;frontend&lt;/code&gt;)은 Deployment&amp;middot;Service&amp;middot;Ingress까지 다 서 있는데 &lt;code&gt;ImagePullBackOff&lt;/code&gt;다 &amp;mdash; ArgoCD 화면에서 &lt;code&gt;Synced&lt;/code&gt;이면서 &lt;code&gt;Degraded&lt;/code&gt;. &lt;b&gt;GitOps가 보장하는 것은 &quot;git에 적힌 대로 반영&quot;까지고, 그것이 실행 가능한지는 다른 문제다.&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;pre class=&quot;dts&quot;&gt;&lt;code&gt;# cgv-infra &amp;mdash; charts/apps/queue/values.yaml
image:
  repository: registry.cgv.local/queue-go   # 실재하지 않는 주소
  tag: dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;registry.cgv.local&lt;/code&gt;은 7부에서 일부러 비워 둔 자리 표시다. 주소가 정해지지 않았는데 먼저 적으면 두 번 고치게 되기 때문이다. 이 편에서 그 자리를 채운다.&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;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;CI   cgv-onprem 에 push  &amp;rarr;  검증  &amp;rarr;  이미지  &amp;rarr;  레지스트리
CD   레지스트리의 이미지  &amp;rarr;  노드가 받아서  &amp;rarr;  파드로 돈다
     그리고 무엇이 도는지를 git 이 적고 있게 한다&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;   ── CI ──
1  CI &amp;mdash; 검증을 통과한 것만 이미지가 된다      개념 &amp;middot; 표준 8단계 &amp;middot; 게이트
2  어떻게 도는가                           GitLab &amp;middot; 러너 &amp;middot; job &amp;middot; stage
3  세운다                                 러너 &amp;middot; 레지스트리 &amp;middot; 손으로 한 번
4  파이프라인                              태그 &amp;middot; 규칙 &amp;middot; 검사 &amp;middot; 실측

   ── CD ──
5  CD &amp;mdash; 배포 엔진은 이미 있다               끊긴 곳 둘
6  노드가 받아오게 한다                     자격 &amp;middot; 배선 &amp;middot; 파드 첫 기동
7  태그를 git 에 되쓴다                     argocd-image-updater

8  대조 &amp;middot; 9  다음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9부는 &quot;클러스터 밖 누구도 클러스터를 건드리지 않는다&quot;를 지키는 편이었다. 10부도 그 성질을 유지한 채로 간다 &amp;mdash; 다만 끝에 가면 GitOps 저장소에 &lt;b&gt;쓰는&lt;/b&gt; 쪽이 하나 생긴다. 그걸 누구에게 어디까지 줄지가 이 편의 마지막 결정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CI &amp;mdash; 검증을 통과한 것만 이미지가 된다&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;손으로 하면 안 되나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지는 세 줄이면 올라간다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;docker login  &amp;rarr;  docker build  &amp;rarr;  docker push&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;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;서비스마다 세 줄씩 쳐야 한다      셋이면 아홉 줄.  하나를 잊으면 그 서비스만 옛것
태그를 내가 정해야 한다          어제 올린 게 뭐였는지 내 기억이 장부다
검증이 없다                    깨진 코드도 컴파일만 되면 이미지가 된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI는 이 셋을 없앤다 &amp;mdash; push가 트리거고, 태그는 규칙이 정하고, 검증을 통과한 것만 이미지가 된다.&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;b&gt;CI 의 뜻&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI(Continuous Integration)는 &quot;지속적 통합&quot; &amp;mdash; &lt;b&gt;자주 합치고, 합칠 때마다 자동으로 검증한다.&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;b&gt;git과 CI가 보는 것이 다르다.&lt;/b&gt; git은 텍스트를 합칠 뿐이라 &quot;충돌 없음&quot;까지만 보장한다. A가 함수 이름을 바꾸고 B가 옛 이름으로 호출하는 코드를 쓰면, 파일이 달라 충돌 없이 합쳐지는데 결과는 컴파일이 안 된다. &lt;b&gt;합쳐진 결과가 실제로 도는지는 CI가 본다.&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;혼자 하는 프로젝트에도 값이 있다. 사람 대신 시간이 벌린다 &amp;mdash; 어제의 나와 오늘의 내가 서로를 모른다. 그리고 CI는 매번 빈 컨테이너에서 clone부터 다시 하므로, 로컬에 우연히 깔린 것에 기대던 빌드가 거기서 드러난다. &quot;내 PC에서는 됐는데&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;&lt;b&gt;표준 8단계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI가 검증하고 배달하는 것을 펼치면 여덟이다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1  lint             내가 쓴 코드의 정적 결함.  컴파일은 되는데 이상하게 도는 자리
2  build            컴파일이 되는가
3  test             단위 테스트
4  secret scan      커밋에 자격(토큰&amp;middot;키&amp;middot;비밀번호)이 섞였는가
5  dependency scan  가져다 쓰는 라이브러리에 알려진 취약점이 있는가
6  package          이미지로 만다
7  image scan       만든 이미지 안에 취약점이 있는가
8  publish          레지스트리에 올린다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손으로 하던 세 줄은 이 중 package(이미지 생성)과 publish(레지스트리 푸시)이다. &lt;b&gt;나머지가 검증이고, 검증이 빠지면 CI가 아니라 빌드 자동화다.&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;3(test)은 이 편에서 못 넣는다. 저장소에 돌릴 테스트가 0개다 &amp;mdash; &lt;code&gt;queue-go&lt;/code&gt;에 &lt;code&gt;*_test.go&lt;/code&gt;가 없고 &lt;code&gt;booking&lt;/code&gt;은 &lt;code&gt;src/test&lt;/code&gt;가 없어 &lt;code&gt;-DskipTests&lt;/code&gt;로 빌드한다. 코드만 읽고 테스트를 쓰면 껍데기가 되기 쉬워서, 앱을 실제로 띄우고 동작을 보면서 쓰는 11부로 미룬다. 이 편에서는 들어올 자리만 만들어 둔다.&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;b&gt;실패하면 막는가 &amp;mdash; 게이트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 단계(Stage)가 실패했을 때&lt;/b&gt; publish를 막을 것인가가 파이프라인의 설계다. 안 막으면 검사는 로그일 뿐이고, 로그는 다음 주면 아무도 안 연다.&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;lint &amp;middot; test &amp;middot; secret scan   내가 쓴 코드다.  실패하면 내가 고친다        &amp;rarr;  조건 없이 막는다
dependency scan             내가 고른 라이브러리.  버전을 올리면 고쳐진다   &amp;rarr;  조건 없이 막는다
image scan                  베이스 이미지의 OS 패키지가 대부분이다         &amp;rarr;  조건이 필요하다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;eclipse-temurin:21-jre&lt;/code&gt;나 &lt;code&gt;nginx&lt;/code&gt;를 훑으면 CVE가 수십 개 나오고 상당수는 아직 패치가 없다. 손쓸 수 없는 것으로 막으면 파이프라인이 영구히 빨간불이 되고, 결국 게이트를 끄게 된다 &amp;mdash; 게이트가 죽는 방식이 이것이다. 그래서 이미지 스캔만 조건을 붙인다: &lt;b&gt;수정 가능한 HIGH 이상이면 막는다.&lt;/b&gt; 나머지는 기록만 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트가 가장 강해야 하는 자리는 secret scan이다. 자격이 한 번 커밋에 들어가면 파일에서 지워도 히스토리에 그대로 남아 되돌릴 수 없다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 어떻게 도는가 &amp;mdash; GitLab 은 목록을 만들고 러너가 실행한다&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt; &amp;mdash; job 목록을 적는 파일&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 전체가 저장소 루트의 파일 한 장으로 정의된다. GitLab은 그 파일 하나만 읽고, 서비스가 몇이든 job을 그 안에 적는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;job은 코드가 아니라 &quot;할 일&quot; 한 덩어리다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;build-queue:                 # job 이름.  아무거나 된다
  script:                    # 돌릴 명령.  이게 본체다
    - docker build ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일에 적혀 있는 동안은 글자다. &lt;b&gt;push가 그 글자에 대상 커밋을 붙여 실체로 만든다.&lt;/b&gt; 한 push로 생긴 job 묶음을 파이프라인이라 부른다.&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;b&gt;stage &amp;mdash; 순서 칸&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job이 여럿이면 순서가 필요하다. stage는 종류가 아니라 &lt;b&gt;순서 칸&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;같은 stage 의 job      순서가 없다.  동시에 돌 수 있는 후보
stage 와 stage 사이    앞 stage 가 전부 성공해야 다음 stage 가 열린다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나누는 기준은 한 줄이다 &amp;mdash; &lt;b&gt;앞것이 실패했을 때 뒷것을 돌릴 이유가 없으면 stage를 나눈다.&lt;/b&gt; 20초짜리 검사가 깨졌는데 3분짜리 빌드를 돌릴 이유가 없으니 그 사이에 벽을 세우고, 서로 무관한 검사끼리는 같은 stage에 나란히 둔다.&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;b&gt;러너 &amp;mdash; 가져가서 실행하는 쪽&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab은 파일을 읽고 job 목록을 만드는 데서 멈춘다. 실행까지 하면 저장소 서버가 빌드마다 눌리기 때문에, 실행은 밖으로 뺐다. 그 실행을 맡는 상주 프로그램이 러너다.&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; 러너가 GitLab에 &quot;내 몫 일 있냐&quot;고 주기적으로 묻고, GitLab은 러너를 부르지 못한다. 그래서 러너는 GitLab에서 보이는 자리에 있을 필요가 없고 &amp;mdash; 실제로 러너 컨테이너에는 열린 포트가 하나도 없다 &amp;mdash; 밖으로 나갈 수만 있으면 어디서든 돈다.&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; GitLab이 파일을 해석해서 job 하나치 명세(이미지 &amp;middot; script &amp;middot; 변수 &amp;middot; 저장소 주소와 커밋 &amp;middot; 그 job 전용 토큰)만 넘긴다. &lt;code&gt;stages&lt;/code&gt;도 &lt;code&gt;extends&lt;/code&gt;도 러너에게는 안 간다 &amp;mdash; 이미 펼쳐진 결과만 온다. 그래서 러너는 프로젝트가 뭔지, 언어가 뭔지 모르고, 몰라도 된다. 언어는 job의 &lt;code&gt;image:&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;&lt;b&gt;한 바퀴&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;①  git push                커밋이 GitLab 에 도착한다
②  GitLab 이 파일을 읽는다   job 을 만든다.  첫 stage 는 pending &amp;middot; 뒤 stage 는 created
③  러너가 물어서 가져간다     pending 인 것만.  created 는 후보에 안 오른다
④  명세를 받는다            이미지 &amp;middot; script &amp;middot; 변수 &amp;middot; 저장소 &amp;middot; 커밋 &amp;middot; 토큰
⑤  실행한다                컨테이너를 띄우고 &amp;rarr; clone &amp;rarr; script 한 줄씩 &amp;rarr; 로그를 실시간 전송
⑥  보고하고 치운다          종료 코드 0 이면 success &amp;rarr; 컨테이너 삭제 &amp;rarr; 다시 ③
⑦  stage 가 넘어간다        앞 stage 가 전부 success 면 다음 stage 가 created &amp;rarr; pending
                          하나라도 failed 면 뒤 stage 는 skipped&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성패는 로그가 아니라 &lt;b&gt;종료 코드&lt;/b&gt;로 정해진다. script는 위에서부터 돌다가 0이 아닌 코드가 나오면 그 자리에서 멈춘다 &amp;mdash; push 앞줄에 검사를 두면 통과 못 한 이미지는 올라가지 않는다. 게이트는 별도 장치가 아니라 이 순서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 job마다 새로 뜨고 끝나면 지워진다. 그 안의 소스&amp;middot;임시 파일&amp;middot;로그인 자격이 전부 사라지고, GitLab에는 로그와 종료 코드만 남는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 세운다 &amp;mdash; 러너 &amp;middot; 레지스트리 &amp;middot; 손으로 한 번&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;러너를 어디에 &amp;mdash; 데스크탑&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;클러스터 안       파드로 뜬다.  빌드가 노드 자원과 디스크를 쓴다
데스크탑 Docker   GitLab 옆에 컨테이너로                       &amp;larr; 고른 것&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 셋이 USB SSD 한 장 위에 있고, 13부에서 그 클러스터에 부하를 걸어 stg&amp;middot;prd 스펙을 산정한다. 빌드가 같은 디스크를 쓰면 그때 나올 숫자가 흔들린다. 데스크탑도 자원이 남아서 고른 게 아니라(이 시점 RAM 16GB에 WSL VM 상한 6GB, 그중 GitLab이 3GB &amp;mdash; 파이프라인이 자리 잡은 뒤 RAM 20GB&amp;middot;상한 8GB로 늘린다), 눌리는 곳이 나중에 재야 할 곳이 아니라서다.&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;b&gt;누구 일을 받나 &amp;mdash; &lt;code&gt;cgv&lt;/code&gt; 그룹&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;프로젝트 러너    한 저장소의 job 만
그룹 러너        그룹 안 모든 저장소의 job                     &amp;larr; 고른 것
인스턴스 러너    이 GitLab 전체.  관리자만 만든다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 만드는 저장소는 &lt;code&gt;cgv-onprem&lt;/code&gt; 하나지만, &lt;code&gt;cgv-infra&lt;/code&gt;에도 CI가 붙는다 &amp;mdash; 이미지 빌드가 아니라 매니페스트 검사(렌더가 깨지는지, 스키마가 맞는지, 봉인본이 빠졌는지)다. 러너 하나로 둘 다 받게 그룹에 둔다. 대가는 &lt;code&gt;cgv-infra&lt;/code&gt;의 job도 이 러너의 권한을 그대로 물려받는 것 &amp;mdash; 두 저장소를 나 혼자 고치는 동안은 두고, 사람이나 저장소가 늘면 프로젝트 러너로 가른다.&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;b&gt;job 을 어떻게 돌리나 &amp;mdash; &lt;code&gt;docker&lt;/code&gt; executor&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;shell     러너 안에서 직접 명령을 돈다.  도구를 미리 깔아 둬야 하고 그 안이 쌓인다
docker    job 마다 컨테이너를 새로 띄운다                      &amp;larr; 고른 것&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 셋의 빌드 환경이 Go&amp;middot;Maven&amp;middot;nginx로 다르다. job마다 필요한 이미지로 새로 뜨는 쪽이 맞다.&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;b&gt;이미지 빌드는 어떻게 &amp;mdash; 소켓 마운트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker&lt;/code&gt; executor를 고르면 바로 걸리는 게 있다. &lt;b&gt;job 컨테이너 안에는 Docker가 없다.&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;Docker는 실제로 일하는 데몬(&lt;code&gt;dockerd&lt;/code&gt;, root로 돈다)과 부탁하는 명령어(&lt;code&gt;docker&lt;/code&gt;)로 나뉘어 있고, &lt;b&gt;둘이 대화하는 통로가 &lt;code&gt;/var/run/docker.sock&lt;/code&gt;이라는 파일&lt;/b&gt;이다. 크기 0짜리 소켓 파일이고 오가는 것은 HTTP다. &lt;code&gt;docker build&lt;/code&gt;를 쳐도 그 명령이 빌드하는 게 아니라 &amp;mdash; 소켓 너머 데몬에게 부탁하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;docker build  ──▶  /var/run/docker.sock  ──▶  dockerd 가 실제로 빌드한다&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;dind&lt;/code&gt;(컨테이너 안에 데몬을 또 띄우는 것)는 격리되는 대신 &lt;code&gt;privileged&lt;/code&gt;와 메모리가 들고, job마다 데몬이 새것이라 빌드 캐시가 매번 사라진다. 띄울 RAM이 없어 소켓으로 간다.&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;대가는 권한이다. 소켓으로는 빌드만 시킬 수 있는 게 아니라 Docker API 전부를 부를 수 있다 &amp;mdash; 데스크탑의 &lt;code&gt;/&lt;/code&gt;를 통째로 마운트한 컨테이너를 띄우라는 요청도 그중 하나고, 데몬이 root로 도니 &lt;b&gt;소켓을 쥔 쪽은 사실상 데스크탑 root다.&lt;/b&gt; 거기에 GitLab 컨테이너와 레지스트리 볼륨이 있다. RAM에 여유가 생기면 &lt;code&gt;dind&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;&lt;b&gt;세우고 등록한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;러너 설정은 두 군데로 갈려 있다. &lt;b&gt;범위&amp;middot;태그&amp;middot;Protected는 GitLab이 들고 있고&lt;/b&gt;, executor&amp;middot;이미지&amp;middot;볼륨은 러너 쪽 파일이 들고 있다. 그래서 GitLab에서 먼저 만든다.&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;code&gt;그룹 cgv &amp;rarr; Build &amp;rarr; Runners &amp;rarr; Create group runner&lt;/code&gt;. 태그 칸에 &lt;code&gt;docker&lt;/code&gt;를 적는다 &amp;mdash; 태그는 러너가 여럿일 때 job을 어디로 보낼지 고르는 이름표고, GitLab은 그 글자의 뜻을 모르고 같은지만 본다. &lt;code&gt;Protected&lt;/code&gt;(보호 브랜치 job만 받기)는 아직 안 켠다 &amp;mdash; 돌릴 job도 브랜치 규칙도 없는데 켜면 첫 파이프라인이 안 도는 것부터 겪는다. 저장하면 &lt;code&gt;glrt-&lt;/code&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;러너 프로세스는 GitLab을 띄우고 있는 데스크탑 &lt;code&gt;docker-compose.yml&lt;/code&gt;에 서비스로 하나 더 넣는다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 데스크탑의 docker-compose.yml &amp;mdash; 서비스 하나 추가
gitlab-runner:
  image: gitlab/gitlab-runner:v19.2.1     # GitLab CE 와 같은 버전
  volumes:
    - gitlab-runner-config:/etc/gitlab-runner
    - /var/run/docker.sock:/var/run/docker.sock   # 러너가 job 컨테이너를 띄우는 통로&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;뜬 것만으로는 아무 GitLab도 모르는 빈 일꾼이다. 아까 받은 토큰으로 도커컴포즈로 생성한 깃랩에 접속(exex)해서 등록한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;docker exec -it gitlab-runner gitlab-runner register \
  --non-interactive \
  --url http://192.168.0.167:8929 \
  --token &amp;lt;glrt- 토큰&amp;gt; \
  --executor docker \
  --docker-image alpine:3.20 \
  --docker-volumes /var/run/docker.sock:/var/run/docker.sock&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;--url &amp;middot; --token       어느 GitLab 의 어느 범위 job 을 받을지.  판단 재료는 GitLab 쪽에 있다
--executor docker     job 마다 컨테이너를 새로
--docker-image        job 이 image: 를 안 적었을 때의 기본값.  실제 job 은 전부 적는다
--docker-volumes      job 컨테이너 안에도 소켓을 넣는다 &amp;mdash; script 의 docker build 가 데몬에 닿게&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소켓은 두 번 나온더. docker-compose.yml에 그리고, exec로 접속해서 깃랩에 설정 등록할때. 이는 이유가 갈린다.&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;compose&lt;/b&gt;의 것은 러너가 job 컨테이너를 만들려고(러너 자신도 컨테이너라 안에 데몬이 없다),&lt;/li&gt;
&lt;li&gt;&lt;b&gt;register&lt;/b&gt;의 것은 job 안의 docker build가 데몬에 닿으려고다.&lt;/li&gt;
&lt;li&gt;그리고 러너가 부탁해 만들어진 job 컨테이너는 러너 안이 아니라 러너 옆에 생긴다 &amp;mdash; 만든 쪽이 데스크탑 데몬이기 때문이다. 파이프라인이 도는 동안 docker ps를 치면 gitlab&amp;middot;gitlab-runner 옆에 runner-xxxx-concurrent-0이 잠깐 보인다.&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;code&gt;register&lt;/code&gt;가 한 일은 &lt;code&gt;/etc/gitlab-runner/config.toml&lt;/code&gt;에 항목 하나를 적은 것이다. 러너는 이후로 그 파일만 읽고 돈다 &amp;mdash; 파일 안의 토큰으로 GitLab에 &quot;내 몫 job 있냐&quot;고 몇 초마다 묻고, 받으면 파일 안의 설정(executor&amp;middot;기본 이미지&amp;middot;소켓)대로 job 컨테이너를 띄워 실행하고, 끝나면 보고하고 다시 묻는다. 그 경로를 볼륨으로 빼 둔 이유가 이것이고, 컨테이너를 다시 만들어도 등록이 남는다. 등록이 끝나면 그룹 러너 목록에 &lt;code&gt;Online&lt;/code&gt;&amp;middot;&lt;code&gt;Idle&lt;/code&gt;로 뜬다. &lt;code&gt;Idle&lt;/code&gt;은 붙어서 묻고는 있는데 받아 갈 job이 없다는 뜻이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1175&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BBkpT/dJMcac5aaUf/oVmP4WzUt4FeBwJDf3hNKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BBkpT/dJMcac5aaUf/oVmP4WzUt4FeBwJDf3hNKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BBkpT/dJMcac5aaUf/oVmP4WzUt4FeBwJDf3hNKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBBkpT%2FdJMcac5aaUf%2FoVmP4WzUt4FeBwJDf3hNKk%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;1175&quot; height=&quot;432&quot; data-origin-width=&quot;1175&quot; data-origin-height=&quot;432&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레지스트리 &amp;mdash; 이미지를 둘 곳&lt;/b&gt;&lt;/h4&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;p data-ke-size=&quot;size16&quot;&gt;따로 세우지 않고 GitLab 내장 Container Registry를 쓴다 &amp;mdash; 이미 있는 것을 쓰면 자격과 백업이 한 자리에 모인다. 켜는 것은 데스크탑 &lt;code&gt;docker-compose.yml&lt;/code&gt;의 두 줄이다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;GITLAB_OMNIBUS_CONFIG: |
  external_url 'http://192.168.0.167:8929'
  registry_external_url 'http://192.168.0.167:5050'    # 더한 줄 &amp;mdash; 레지스트리 프로세스가 뜬다
ports:
  - '8929:8929'
  - '5050:5050'                                        # 더한 줄&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;git은 git 프로토콜로, 레지스트리는 Docker Registry API로 대화&lt;/b&gt;하고, &lt;code&gt;docker push&lt;/code&gt;는 항상 &lt;code&gt;호스트[:포트]/경로:태그&lt;/code&gt; 형태로만 부르기 때문에 한 포트에서 경로로 가를 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 형태가 곧 이미지 이름이다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;192.168.0.167:5050  /  cgv/cgv-onprem  /  queue-go  :  dev
└─ 어느 서버 ────┘     └─ 어느 프로젝트 ┘   └ 이름 ┘   └ 태그 ┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가운데가 GitLab의 그룹/프로젝트 경로 그대로다. 레지스트리 프로세스와 저장 공간은 하나고, 프로젝트마다 갈리는 것은 경로와 권한이다 &amp;mdash; 그 프로젝트에 권한이 있어야 그 경로를 읽고 쓴다.&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;code&gt;http://&lt;/code&gt;로 연 것은 평문으로 열겠다는 뜻이다. TLS를 붙이려면 자체 서명 인증서를 만들어 노드 셋과 데스크탑 Docker에 CA를 심어야 하는데, 14부에서 전 구간 TLS를 할 때 어차피 다시 한다 &amp;mdash; 두 번 할 일이라 지금은 안 한다. 대가는 같은 망에서 들여다보면 이미지도 로그인 토큰도 읽힌다는 것이고, 닫는 시점은 14부다.&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;pre class=&quot;groovy&quot;&gt;&lt;code&gt;$ curl -i http://192.168.0.167:5050/v2/
HTTP/1.1 401 Unauthorized
Www-Authenticate: Bearer realm=&quot;http://192.168.0.167:8929/jwt/auth&quot;,service=&quot;container_registry&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;401&lt;/code&gt;이 맞는 응답이다. &quot;인증하고 오라&quot;는 뜻이고, 인증 서버로 GitLab의 8929를 가리킨다 &amp;mdash; 둘이 제대로 물려 있다는 확인이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데스크탑 Docker는 레지스트리를 HTTPS로 가정하므로, 평문 주소를 예외로 등록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// Docker Desktop &amp;rarr; Settings &amp;rarr; Docker Engine
{ &quot;insecure-registries&quot;: [&quot;192.168.0.167:5050&quot;] }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 해제가 아니라 이 주소 하나만 인증서 검증을 건너뛰고 평문으로 내려간다. 주소는 이미지 이름 앞부분과 글자까지 같아야 걸린다. 데몬 설정이라 적용하면 데몬이 재시작하고, 그 데몬이 물고 있는 GitLab&amp;middot;러너 컨테이너도 같이 내려갔다 올라온다.&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;b&gt;손으로 한 번 올린다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI를 붙이기 전에 경로부터 확인한다. 여기서 안 되면 나중에 CI 문제인지 레지스트리 문제인지 갈리지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자격은 personal access token으로 만든다. scope는 &lt;code&gt;read_registry&lt;/code&gt;&amp;middot;&lt;code&gt;write_registry&lt;/code&gt; 둘 &amp;mdash; git을 읽는 &lt;code&gt;read_repository&lt;/code&gt;와는 다른 칸이고, 올리는 데는 읽기&amp;middot;쓰기가 둘 다 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker login 192.168.0.167:5050
docker build -t 192.168.0.167:5050/cgv/cgv-onprem/queue-go:test ./queue-go
docker push  192.168.0.167:5050/cgv/cgv-onprem/queue-go:test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-t&lt;/code&gt;에 적은 이름이 곧 보낼 주소다. Go 빌드가 128초, push가 끝나면 GitLab의 &lt;code&gt;Deploy &amp;rarr; Container Registry&lt;/code&gt;에 &lt;code&gt;cgv-onprem/queue-go&lt;/code&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;여기서 셋이 한 번에 확인된다 &amp;mdash; Dockerfile이 로컬 compose 밖에서도 돌고, 이미지 경로가 맞고, 자격이 통한다. 그리고 그 화면에 하나 더 적혀 있다: &lt;code&gt;Cleanup is not scheduled&lt;/code&gt;. 올라온 태그를 지우는 규칙이 없다는 뜻인데, 태그를 늘리기 전에 걸어야 하는 것이라 파이프라인을 붙이면서 함께 정리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파이프라인 &amp;mdash; 손으로 한 것을 규칙으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손으로 하던 login&amp;middot;build&amp;middot;push를 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;로 옮긴다. 파일을 쓰기 전에 정할 것이 둘 있다 &amp;mdash; 이미지에 붙일 이름과, 쌓일 이름을 지우는 규칙이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;태그부터 정한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker build -t ...:&amp;lt;여기&amp;gt;&lt;/code&gt;에 무엇을 넣을지가 배포 방식을 통째로 가른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 차트는 &lt;code&gt;tag: dev&lt;/code&gt; 고정에 &lt;code&gt;pullPolicy: IfNotPresent&lt;/code&gt;다. &lt;code&gt;IfNotPresent&lt;/code&gt;는 &quot;노드에 그 이름이 이미 있으면 안 받아온다&quot;는 뜻이라, &lt;code&gt;:dev&lt;/code&gt;를 새로 빌드해 올려도 노드는 예전에 받아 둔 &lt;code&gt;:dev&lt;/code&gt;를 그대로 쓴다. 배포했는데 안 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;A  태그를 고정한다     :dev 를 계속 덮어쓴다.  매니페스트는 안 고쳐도 된다
B  태그를 매번 바꾼다   커밋마다 다른 이름.  매니페스트에 그 이름을 적어야 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A는 배포를 시킬 방법이 없다. 매니페스트가 안 변하니 ArgoCD는 할 일이 없고, 파드를 다시 띄우려면 CI가 클러스터에 &lt;code&gt;kubectl rollout restart&lt;/code&gt;를 걸어야 한다 &amp;mdash; 클러스터 접근 자격이 밖으로 나가고, 9부에서 지킨 &quot;밖이 안을 안 건드린다&quot;가 깨진다. 이전 &lt;code&gt;:dev&lt;/code&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;B로 간다.&lt;/b&gt; 매니페스트의 &lt;code&gt;tag&lt;/code&gt; 한 줄이 바뀌면 ArgoCD가 sync하고 파드가 교체된다 &amp;mdash; 트리거가 git 커밋이 되어 9부에 세운 길을 그대로 탄다. 롤백은 git에서 이전 태그로 되돌리는 일이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름에는 셋을 담는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;dev-42-a1b2c3d
 │   │    └ 커밋 해시.  이미지와 커밋이 1:1 &amp;mdash; 장애 때 &quot;지금 도는 게 어느 코드냐&quot; 가 태그로 나온다
 │   └ 파이프라인 번호.  1 씩 늘어 순서가 눈에 보인다
 └ 브랜치.  stg 브랜치가 생기면 목록에서 갈린다&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;GitLab이 job에 넣어 주는 변수를 그대로 쓴다 &amp;nbsp;&lt;code&gt;$CI_COMMIT_REF_SLUG&lt;/code&gt;&amp;middot;&lt;code&gt;$CI_PIPELINE_IID&lt;/code&gt;&amp;middot;&lt;code&gt;$CI_COMMIT_SHORT_SHA&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;&lt;b&gt;늘리기 전에 지우는 쪽을 먼저 건다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인이 붙으면 커밋마다 서비스 셋씩 태그가 쌓인다. &lt;code&gt;cgv-onprem&lt;/code&gt;의 &lt;code&gt;Settings &amp;rarr; Packages and registries &amp;rarr; Container registry cleanup policies&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Enable cleanup policy    켠다
Run cleanup              Every day
Keep the most recent     10 tags per image name
Remove tags older than   7 days        기본값 90 일에서 줄였다
Remove tags matching     .*            필수 칸.  전부를 후보로 올린다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab은 &lt;code&gt;Remove tags matching&lt;/code&gt;으로 후보를 모은 뒤 &lt;b&gt;최근 10개와 7일 안쪽을 후보에서 뺀 나머지&lt;/b&gt;를 지운다. 하루에 몇 번을 올리든 최근 10개는 항상 남고, 롤백할 여지가 그만큼이다. 90일을 7일로 줄인 것은 &amp;mdash; 최근 10개 유지가 이미 롤백을 보장하는데 90일을 얹으면 처음 석 달 동안 아무것도 안 지워지기 때문이다.&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;code&gt;gitlab-ctl registry-garbage-collect&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;&lt;b&gt;파일의 뼈대 &amp;mdash; 설정 &amp;middot; job &amp;middot; 틀&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;의 최상위 이름은 세 부류다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;설정    workflow &amp;middot; stages &amp;middot; default &amp;middot; variables     GitLab 이 정해 둔 예약어
job     그 밖의 이름 전부.  실제로 도는 것
틀      점(.)으로 시작하는 이름.  실행되지 않고 extends 로 가져다 쓴다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;default&lt;/code&gt;는 적는 순간 모든 job에 자동으로 붙는 것이다. 셋을 둔다.&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; 지목해야 그 러너가 가져간다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interruptible: true&lt;/code&gt; &amp;mdash; 같은 브랜치에 새 커밋이 오면 돌던 파이프라인을 취소한다. 옛 커밋의 결과는 볼 이유가 없는데 실행 슬롯을 계속 잡는다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeout: 30m&lt;/code&gt; &amp;mdash; 프로젝트 기본값 1시간이면 멈춘 job이 그동안 슬롯을 잡는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;variables&lt;/code&gt;에는 정해 둔 &lt;code&gt;TAG&lt;/code&gt;가 앉는다. 틀은 &lt;code&gt;extends&lt;/code&gt;한 job만 받는다. 이제 파일을 위에서부터 짚는다.&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;b&gt;workflow &amp;mdash; 같은 커밋이 두 번 돌지 않게&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 맨 위는 &quot;이 push로 파이프라인을 만들까 말까&quot;다. 이 층이 필요한 것은 &lt;b&gt;push 하나가 GitLab 안에서 사건 둘이 되기 때문이다&lt;/b&gt; &amp;mdash; &quot;브랜치에 새 커밋이 왔다&quot;와 &quot;열린 MR의 소스 브랜치가 바뀌었다&quot;. 사건마다 따로 판단하므로 MR이 열린 브랜치에 push하면 같은 커밋이 두 번 돈다. job에 조건(&lt;code&gt;rules&lt;/code&gt;)을 적기 시작하면 &quot;브랜치 파이프라인만 만든다&quot;는 GitLab 기본 동작이 꺼져서, 이 위층에서 명시해야 한다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;workflow:
  rules:
    # MR 사건이면 만든다.  &quot;머지해도 되나&quot; 를 판정하는 쪽이라 이걸 남긴다
    - if: $CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;
    # 브랜치 push 인데 그 브랜치에 열린 MR 이 있으면 만들지 않는다.  위에서 이미 만들었다
    - if: $CI_COMMIT_BRANCH &amp;amp;&amp;amp; $CI_OPEN_MERGE_REQUESTS
      when: never
    # 브랜치 파이프라인은 dev(머지 결과 검증)만.  일반 브랜치까지 열어 두면 push 를 먼저 하고
    # MR 을 나중에 만들었을 때 push 사건과 MR 생성 사건이 각각 파이프라인을 만든다 &amp;mdash;
    # 위의 차단은 &quot;push 시점에 MR 이 이미 열린&quot; 경우만 걸러서 그 반대 순서를 못 막는다
    - if: $CI_COMMIT_BRANCH == &quot;dev&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙은 위에서부터 보다가 처음 맞는 줄에서 멈추고, 아무 줄에도 안 걸리면 안 만든다. 일반 브랜치는 MR을 열어야 CI가 돌고, 그 대신 파이프라인은 언제나 하나다.&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;b&gt;stage 다섯, 그리고 job 지도&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stage는 다섯을 선언한다. 벽마다 근거가 있다 &amp;mdash; 검사가 깨졌는데 분 단위 빌드를 돌릴 이유가 없고, 스캔의 대상은 build가 만든 이미지고, 스캔에 걸린 이미지는 레지스트리에 안 올린다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;stages:
  - check      # 코드를 훑는다.  여기서 걸리면 뒤로 안 간다
  - test       # 단위 테스트.  11부에서 채운다
  - build      # 이미지를 만든다.  올리지는 않는다
  - scan       # 방금 만든 이미지의 취약점을 훑는다
  - publish    # 스캔까지 통과한 것을 레지스트리로.  dev 에서만 생긴다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build&amp;middot;scan&amp;middot;publish가 다른 job인데도 이미지가 이어지는 것은 소켓 마운트 덕이다.&lt;/b&gt; build가 만든 이미지는 job 컨테이너가 아니라 데스크탑 데몬에 남고, 다음 stage의 job이 같은 이름(&lt;code&gt;$SVC:$TAG&lt;/code&gt;)으로 그것을 본다. dind였으면 job마다 데몬이 새것이라 이미지를 파일로 저장해 넘겨야 했고, 이 분리 자체가 성립하지 않았다. 갈라서 얻는 것은 둘이다 &amp;mdash; 실패가 job 단위로 화면에 갈리고(&lt;code&gt;scan:booking&lt;/code&gt; 빨강이면 클릭 없이 원인이 보인다), 스캔 결과를 MR 화면에 붙일 자리가 생긴다.&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;이 다섯을 채우는 job이 열셋이다.&lt;/p&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;stage      job                                  이미지           하는 일
check      secret-scan                          gitleaks        저장소 전체.  과거 커밋까지
           dep-scan:queue-go                    trivy           go.sum 의 의존성 취약점
           lint:queue-go                        golang:1.26     go vet
           lint:booking                         maven           컴파일 + SpotBugs
test       (비어 있다)                                           11부에서 채운다
build      build:{queue-go&amp;middot;booking&amp;middot;frontend}    docker:27-cli   이미지를 만든다
scan       scan:{...}                           trivy           만든 이미지를 훑는다
publish    publish:{...}                        docker:27-cli   dev 에서만 생긴다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lint와 dep을 나눠 두는 것은 &lt;b&gt;실패의 원인과 대응이 다르기 때문이다.&lt;/b&gt; lint는 내가 코드를 건드려야 새로 실패하고 고치는 법도 코드 수정이다. dep은 코드가 그대로여도 새 CVE가 발표되면 어제 통과한 커밋이 오늘 실패하고, 고치는 법은 버전 올리기다. 합쳐 두면 &quot;내가 뭘 잘못했나&quot;부터 뒤지게 된다.&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;frontend는 정적 파일뿐이라 check&amp;middot;test에 없다 &amp;mdash; build&amp;middot;scan&amp;middot;publish만 타고, scan이 그 서비스의 유일한 검사가 된다. booking의 의존성 검사가 check에 따로 없는 것도 같은 자리에서 해결된다 &amp;mdash; scan:booking이 이미지 안의 &lt;code&gt;app.jar&lt;/code&gt;를 직접 읽으므로, 선언(pom.xml)이 아니라 실제로 담긴 것을 판정한다.&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;b&gt;check &amp;mdash; 항상 도는 것과 바뀔 때만 도는 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;secret scan&lt;/b&gt;은 조건 없이 모든 커밋에서 돈다. 어느 파일이 바뀌든 자격이 섞였을 수 있어서다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;secret-scan:
  stage: check
  image:
    name: zricethezav/gitleaks:latest
    entrypoint: [&quot;&quot;]   # 이미지가 gitleaks 를 바로 실행하게 돼 있어, script 를 넣으려면 비운다
  variables:
    GIT_DEPTH: 0       # 기본은 얕은 clone &amp;mdash; 과거 커밋이 안 온다.  히스토리 전체를 받는다
  script:
    - gitleaks detect --source . --no-banner --redact   # redact = 찾은 값을 로그에 안 찍는다&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;dep 검사&lt;/b&gt;도 조건이 없다 &amp;mdash; 코드가 안 바뀌어도 새 CVE 하나로 결과가 바뀌는 검사라, &quot;바뀐 파일&quot;을 조건으로 걸 이유가 없다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;dep-scan:queue-go:
  stage: check
  image:
    name: aquasec/trivy:latest
    entrypoint: [&quot;&quot;]
  variables:
    TRIVY_NO_PROGRESS: &quot;true&quot;                        # DB 내려받기 진행바를 로그에 안 찍는다
    TRIVY_CACHE_DIR: /root/.cache/trivy/$CI_JOB_NAME
  script:
    - trivy fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 ./queue-go&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대상이 &lt;code&gt;./queue-go&lt;/code&gt; 하나다. &lt;code&gt;go.sum&lt;/code&gt;은 간접 의존까지 버전이 전부 파일에 적혀 있어 원격 조회 없이 판정된다. &lt;code&gt;pom.xml&lt;/code&gt;은 반대다 &amp;mdash; 버전을 부모 POM이 정하므로 읽으려면 Maven Central에 물어야 하고, 같은 IP에서 빌드가 판마다 의존성을 수백 개 받고 있으면 그 조회가 429(Too Many Requests)로 차단된다. Java 의존성은 scan:booking이 &lt;code&gt;app.jar&lt;/code&gt;를 직접 읽는 것으로 판정한다 &amp;mdash; 잃는 것은 build 전에 잡는 이점 하나다.&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;서비스별 lint&lt;/b&gt;는 다르다. 저장소 하나에 서비스 셋이 들어 있어서(모노레포), 조건이 없으면 &lt;code&gt;booking&lt;/code&gt; 한 줄을 고쳐도 세 서비스의 job이 다 돈다. &lt;b&gt;&lt;code&gt;rules: changes&lt;/code&gt;&lt;/b&gt;가 커밋에서 바뀐 파일을 보고 &lt;b&gt;그 job을 만들지 말지&lt;/b&gt;를 정한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;lint:queue-go:
  stage: check
  image: golang:1.26
  script:
    - cd queue-go
    - go vet ./...          # 컴파일은 되는데 의도와 다르게 도는 자리를 잡는다
  rules:
    - changes: [ &quot;queue-go/**/*&quot;, &quot;.gitlab-ci.yml&quot; ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 바뀐 서비스의 job은 건너뛰는 게 아니라 화면에 아예 없다. &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;을 목록에 넣는 것은 &amp;mdash; CI 파일만 고친 커밋에서 job이 하나도 안 생기면 고친 게 도는지 확인할 방법이 없어서다.&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;booking의 lint는 SpotBugs&lt;/b&gt;다. 소스가 아니라 컴파일된 바이트코드를 훑는 도구라 compile이 먼저 온다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;lint:booking:
  stage: check
  image: maven:3.9-eclipse-temurin-21
  variables:
    MAVEN_OPTS: &quot;-Xmx768m&quot;
  script:
    - cd booking
    - mvn -B compile spotbugs:check
  artifacts:
    when: always               # 실패했을 때야말로 받아 봐야 한다
    expire_in: 1 week
    paths:
      - booking/target/spotbugsXml.xml
  rules:
    - changes: [ &quot;booking/**/*&quot;, &quot;.gitlab-ci.yml&quot; ]&lt;/code&gt;&lt;/pre&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;code&gt;MAVEN_OPTS&lt;/code&gt; &amp;mdash; JVM은 힙 상한을 안 주면 컨테이너가 보는 메모리의 25%를 잡는다. job 컨테이너에 메모리 제한이 없어 VM 전체(7.8GB)를 보고 혼자 2GB 가까이 쓴다. 동시에 도는 job 수만큼 곱해지므로 빌드 스테이지와 같은 값으로 고정한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;artifacts&lt;/code&gt; &amp;mdash; 콘솔에는 결함 개수만 남고 어느 파일 몇 줄인지는 &lt;code&gt;spotbugsXml.xml&lt;/code&gt;에 있다. job이 끝나면 컨테이너째 사라지므로 리포트를 GitLab에 올려 둬야 실패를 추적할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검사 규칙은 &lt;code&gt;booking/pom.xml&lt;/code&gt;(플러그인 선언)과 &lt;code&gt;booking/spotbugs-exclude.xml&lt;/code&gt;(제외 필터)에 있다. 생성자 주입으로 받은 스프링 빈을 필드에 저장하는 자리가 전부 EI_EXPOSE_REP 계열로 걸리는데, 컨테이너가 관리하는 싱글턴이라 방어적 복사가 성립하지 않는다. 그 두 패턴만 제외하고 나머지 Medium 이상은 그대로 실패로 본다.&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;b&gt;build &amp;middot; scan &amp;middot; publish &amp;mdash; 서비스마다 셋, 올리는 건 dev 만&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스마다 build&amp;middot;scan&amp;middot;publish job이 하나씩 서고, &lt;b&gt;같은 서비스의 셋이 같은 경로 규칙을 공유한다&lt;/b&gt; &amp;mdash; 안 바뀐 서비스는 셋이 함께 안 생긴다. publish에만 조건이 하나 더 붙는다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;publish:queue-go:
  extends: .publish
  variables:
    SVC: queue-go
  rules:
    - if: $CI_COMMIT_BRANCH == &quot;dev&quot;    # dev 가 아니면 이 job 은 만들어지지 않는다
      changes: [ &quot;queue-go/**/*&quot;, &quot;.gitlab-ci.yml&quot; ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build와 scan은 브랜치에서도 돌린다 &amp;mdash; 컴파일이 되는지, 취약점이 있는지는 머지 전에 걸러야 한다. 올리는 것은 다르다. 아무 브랜치에서나 push되면 리뷰를 통과하지 않은 이미지가 배포 후보와 같은 자리에 쌓인다.&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;브랜치&amp;middot;MR 파이프라인은 check &amp;rarr; build &amp;rarr; scan에서 끝나고, publish job은 화면에 아예 없다.&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;부수 효과로 태그가 정리된다 &amp;mdash; 실험 브랜치 이름이 섞인 태그가 레지스트리에 안 생기고 &lt;code&gt;dev-*&lt;/code&gt;만 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브랜치에서도 build는 도니 레이어 캐시는 데스크탑에 그대로 쌓이고, 머지 뒤의 build가 그 캐시로 빨리 끝나고 publish가 올린다&lt;/b&gt;. 서비스마다 태그가 다른 상태가 정상이 된다 &amp;mdash; 안 바뀐 서비스는 예전 이미지가 그대로 맞는 것이다.&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;b&gt;틀 &amp;mdash; 절차는 한 번만 적는다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build 이후의 절차는 서비스 셋이 똑같다. &lt;b&gt;언어별 차이가 CI가 아니라 각 서비스의 Dockerfile 안에 있어서다&lt;/b&gt; &amp;mdash; CI는 &quot;그 디렉터리에서 &lt;code&gt;docker build&lt;/code&gt; 해라&quot;만 알고, 그 안에서 Go를 컴파일하는지(queue-go) Maven이 도는지(booking) 복사만 하는지(frontend)는 멀티스테이지 Dockerfile이 품고 있다. 나온 이미지를 훑고(scan) 올리는(publish) 데는 애초에 언어 구분이 없다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 절차를 틀에 한 번만 적고, 각 job은 &lt;code&gt;SVC&lt;/code&gt; 값 하나만 바꾼다. &lt;code&gt;SVC&lt;/code&gt;는 서비스 디렉터리 이름이고, 그 한 값이 빌드 컨텍스트(&lt;code&gt;./$SVC&lt;/code&gt;)와 이미지 이름(&lt;code&gt;.../$SVC:$TAG&lt;/code&gt;)을 함께 정한다 &amp;mdash; 디렉터리 이름과 이미지 이름을 같게 지어 둔 것이 여기서 값을 한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;.build:
  stage: build
  image: docker:27-cli        # 등록 기본값 alpine 엔 docker 명령이 없다.  CLI 만 든 이미지
  dependencies: []
  script:
    # --pull: 같은 태그라도 업스트림은 패치를 넣어 다시 굽는다.  로컬 캐시가 있으면 낡은 것으로 빌드된다
    - docker build --pull -t &quot;$CI_REGISTRY_IMAGE/$SVC:$TAG&quot; &quot;./$SVC&quot;

.scan:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [&quot;&quot;]
  dependencies: []
  variables:
    TRIVY_NO_PROGRESS: &quot;true&quot;
    TRIVY_CACHE_DIR: /root/.cache/trivy/$CI_JOB_NAME
    TRIVY_TIMEOUT: 20m
  script:
    # 수정 가능한 HIGH 이상이면 종료 코드 1 &amp;rarr; job 실패 &amp;rarr; publish 가 안 열린다
    - trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 &quot;$CI_REGISTRY_IMAGE/$SVC:$TAG&quot;

.publish:
  stage: publish
  image: docker:27-cli
  dependencies: []
  interruptible: false        # push 중간에 끊기면 레이어가 일부만 올라간 상태로 남는다
  before_script:
    # job 마다 발급되고 끝나면 폐기되는 토큰.  만들 것도 보관할 것도 없다
    # -p 로 주면 비밀번호가 명령줄 인자에 남으므로 파이프로 넣는다
    - echo &quot;$CI_REGISTRY_PASSWORD&quot; | docker login -u &quot;$CI_REGISTRY_USER&quot; --password-stdin &quot;$CI_REGISTRY&quot;
  script:
    - docker push &quot;$CI_REGISTRY_IMAGE/$SVC:$TAG&quot;

build:queue-go:
  extends: .build
  variables:
    SVC: queue-go             # booking &amp;middot; frontend 도 이 값 하나만 다르다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인이 &lt;code&gt;.publish&lt;/code&gt;에만 있다 &amp;mdash; build는 공개 베이스 이미지만 받아오고, scan은 데몬에 있는 로컬 이미지를 보므로 레지스트리 자격이 필요 없다. 주소와 자격도 파일에 안 적는다: &lt;code&gt;$CI_REGISTRY_IMAGE&lt;/code&gt;가 &quot;레지스트리 주소 + 프로젝트 경로&quot;를 담고 있어 레지스트리가 옮겨 가도 이 파일은 안 고친다 &amp;mdash; 하드코딩된 IP가 하나 안 늘었다.&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;code&gt;dependencies: []&lt;/code&gt;는 앞 stage의 아티팩트를 안 받는 선언이다. 기본값이 &quot;앞 stage 전부에서 받는다&quot;라, 이게 없으면 lint:booking이 남긴 SpotBugs 리포트를 build&amp;middot;scan&amp;middot;publish가 job마다 내려받고 풀고 지운다 &amp;mdash; 쓰지도 않는 파일에 시간을 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.scan&lt;/code&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;TRIVY_NO_PROGRESS&lt;/code&gt; &amp;mdash; 진행바 갱신이 로그 줄로 쌓여 한 번의 내려받기가 수백 줄이 된다. 결과 표가 그 뒤로 밀린다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TRIVY_CACHE_DIR&lt;/code&gt; &amp;mdash; trivy는 캐시 디렉터리에 배타 잠금을 건다. scan job 셋이 한 디렉터리를 공유하면 먼저 잡은 하나만 돌고 나머지는 잠금 대기로 죽는다. job 이름으로 하위 폴더를 갈라 잠금이 안 겹치게 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TRIVY_TIMEOUT&lt;/code&gt; &amp;mdash; 기본 제한 5분 안에 DB 내려받기까지 들어간다. Java DB가 903MB라 캐시가 비었거나 만료(3일)된 판은 내려받기만으로 제한을 넘기고, 취약점 유무와 무관하게 job이 죽는다.&lt;/li&gt;
&lt;/ul&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;b&gt;반복해서 받지 않게 &amp;mdash; 캐시는 두 층이다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job 컨테이너는 끝나면 지워진다. 안에서 내려받은 것도 같이 사라져, 손대지 않으면 매 판이 처음이다 &amp;mdash; trivy 취약점 DB 103MB에 Java DB 903MB, Maven 의존성, Go 모듈을 판마다 다시 받는다. 해법은 하나다: &lt;b&gt;죽는 곳에서 받은 것을 안 죽는 곳에 둔다.&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;그런데 이 파이프라인에서는 &quot;죽는 곳&quot;이 두 군데다. 같은 Maven 다운로드가 job에 따라 &lt;b&gt;다른 방&lt;/b&gt;에서 일어난다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;lint:booking     job 컨테이너 안에서 mvn 이 직접 돈다
                 받은 jar 는 그 컨테이너 안에 쌓인다 &amp;rarr; job 끝 = 컨테이너 삭제 = 같이 삭제

build:booking    job 컨테이너는 &quot;docker build 해라&quot; 명령만 소켓으로 보낸다
                 실제 빌드는 데스크탑 데몬의 세계에서 돈다 &amp;mdash; 거기서 mvn 이 또 받는다
                 job 컨테이너와는 다른 방이다&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;층 하나 &amp;mdash; job 컨테이너의 방: 러너 볼륨.&lt;/b&gt; 컨테이너 안의 경로 자체를 바깥 창고(이름 붙인 도커 볼륨)로 연결한다. 도구는 평소 쓰던 경로에 쓸 뿐인데 그 문이 밖으로 나 있어서, 컨테이너가 죽어도 내용물은 창고에 남고 다음 job이 그대로 이어받는다 &amp;mdash; 러너를 세울 때 소켓 파일 하나를 꽂아 줬던 것과 같은 기술이고, 이번엔 디렉터리다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 러너 config.toml &amp;mdash; job 컨테이너마다 이 볼륨들이 꽂힌다
volumes = [&quot;/var/run/docker.sock:/var/run/docker.sock&quot;, &quot;/cache&quot;,
           &quot;maven-cache:/root/.m2&quot;,                # lint:booking 의 의존성
           &quot;trivy-cache:/root/.cache/trivy&quot;,       # 취약점 DB.  안에서 job 별 폴더로 갈린다
           &quot;gomod-cache:/go/pkg/mod&quot;,              # Go 모듈
           &quot;gobuild-cache:/root/.cache/go-build&quot;]  # Go 컴파일 결과.  go vet 도 컴파일을 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 볼륨이 둘째 방에는 안 닿는다 &amp;mdash; 볼륨은 job 컨테이너에 꽂는 것인데, build의 다운로드는 그 컨테이너 밖(데몬)에서 일어난다. 옆방에 꽂은 콘센트다.&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;층 둘 &amp;mdash; 데몬의 방: 레이어 캐시, 그리고 그 틈.&lt;/b&gt; 데몬에는 선반이 기본으로 하나 있다. 레이어 캐시 &amp;mdash; &lt;code&gt;COPY pom.xml&lt;/code&gt;의 내용이 지난번과 같으면 그 아래 &lt;code&gt;RUN&lt;/code&gt;(의존성 다운로드)의 결과를 통째로 재사용한다. 평소 build가 십몇 초로 끝나는 이유다. 약점은 조건부다: 그 파일이 &lt;b&gt;한 글자라도 바뀌면&lt;/b&gt; &quot;재료가 달라졌다&quot;로 보고 아래를 전부 무효로 만들어 수백 개를 처음부터 다시 받는다.&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;pom.xml이 바뀌는 커밋이 언제인가 &amp;mdash; 의존성을 올린 커밋, &quot;이거 올려도 되나&quot;를 CI로 제일 급히 확인하고 싶은 바로 그 순간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 틈을 메우는 것이 &lt;b&gt;BuildKit의 캐시 마운트&lt;/b&gt;다. BuildKit은 도커가 이미지를 굽는 빌드 엔진인데, &lt;code&gt;RUN&lt;/code&gt; 한 줄에 &lt;b&gt;레이어 바깥의 전용 선반을 꽂아 주는&lt;/b&gt; 기능이 있다. 레이어가 무효가 돼 &lt;code&gt;RUN&lt;/code&gt;이 다시 돌아도 그 선반은 무효화와 무관하게 남아 있으니, Maven이 이미 있는 것은 건너뛰고 바뀐 것만 받는다 &amp;mdash; 층 하나의 볼륨과 똑같은 발상을 데몬의 세계 안에서 한 번 더 한 것이다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# syntax=docker/dockerfile:1                       &amp;larr; 이 기능을 쓰는 문법 버전 선언
RUN --mount=type=cache,target=/root/.m2 mvn -q -B dependency:go-offline    # booking
RUN --mount=type=cache,target=/go/pkg/mod go mod download                  # queue-go&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;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;방                    죽는 것              선반                      쓰는 job
job 컨테이너           컨테이너 자체         러너 볼륨 4종              lint &amp;middot; scan &amp;middot; dep-scan
데몬 (docker build)    무효화된 레이어       레이어 캐시(기본)           build 셋
                                          + BuildKit 캐시 마운트(보강)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시의 대가는 디스크다. trivy 캐시는 job별 폴더라 DB가 네 벌로 앉아 8GB대가 된다. 그리고 러너 볼륨은 job이 돌 때만 컨테이너에 붙어 있어 &lt;code&gt;docker volume prune&lt;/code&gt;에는 &quot;안 쓰는 볼륨&quot;으로 보인다 &amp;mdash; 정리 명령을 돌리면 캐시가 통째로 지워진다.&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;b&gt;concurrent &amp;mdash; 동시에 몇 개. needs 는 보류&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;concurrent&lt;/code&gt;는 러너 설정(&lt;code&gt;config.toml&lt;/code&gt; 맨 위)이다. 러너가 GitLab에 묻는 창구 수라서, 1이면 job을 하나 끝내야 다음을 받고 2면 둘을 동시에 든다. 러너 개수와는 별개다. &lt;b&gt;2로 둔다.&lt;/b&gt; 더 올려도 한 stage의 시간은 그 안에서 제일 오래 걸리는 job이 정한다 &amp;mdash; check 넷이 0:14 &amp;middot; 0:15 &amp;middot; 0:15 &amp;middot; 0:36일 때 슬롯 2개면 0:44, 4개면 0:36으로 차이가 초 단위다. 슬롯을 늘리는 값은 메모리와 캐시 동시 접근으로 치른다.&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;code&gt;needs&lt;/code&gt;는 job이 기다릴 대상을 앞 stage 전체가 아니라 특정 job으로 좁히는 것이다 &amp;mdash; &lt;code&gt;build:queue-go&lt;/code&gt;가 &lt;code&gt;check:booking&lt;/code&gt;을 안 기다리게 된다. 다만 걸어 둔 job은 나중에 stage를 추가해도 그것을 조용히 건너뛴다. 11부에 test stage를 채울 예정이라, 기본은 stage로 두고 특정 job이 유독 길어 나머지가 그것만 기다리는 게 보일 때만 건다. &lt;code&gt;rules: changes&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;&lt;b&gt;돌아간 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시가 채워진 상태의 MR 파이프라인 &amp;mdash; job 10개, 전체 1분 54초. publish 셋은 dev가 아니라 화면에 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;check     secret-scan 0:15 &amp;middot; dep-scan:queue-go 0:15 &amp;middot; lint:queue-go 0:14 &amp;middot; lint:booking 0:36
build     queue-go 0:16 &amp;middot; booking 0:15 &amp;middot; frontend 0:11
scan      queue-go 0:12 &amp;middot; booking 0:32 &amp;middot; frontend 0:16&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시가 빈 첫 판은 다르다 &amp;mdash; trivy DB와 Java DB, Maven 의존성, Go 모듈을 처음 한 번 받으므로 job 몇 개가 분 단위로 걸리고, 그다음 판부터 위 숫자가 된다.&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;MR을 스쿼시로 머지하면 dev에 커밋 하나가 생기고, 브랜치 파이프라인이 돈다. 이번엔 &lt;code&gt;$CI_COMMIT_BRANCH == &quot;dev&quot;&lt;/code&gt;가 참이라 publish 셋까지 13개다. 전체 2분 35초. publish는 셋 다 20초 안쪽 &amp;mdash; 레지스트리가 같은 데스크탑에 있어 push가 바깥 회선을 안 탄다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;booking     dev-15-17dcc495     277.5 MiB     JRE 가 들어 있어 셋 중 제일 크다
queue-go    dev-15-17dcc495      46.5 MiB     distroless
frontend    dev-15-17dcc495      42.0 MiB     nginx alpine&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록에는 지나온 흔적도 그대로 있다 &amp;mdash; 손으로 올려 본 &lt;code&gt;test&lt;/code&gt;, push를 dev 머지 뒤로 묶기 전에 올라간 &lt;code&gt;ci-pipeline-1&amp;middot;2&lt;/code&gt;. cleanup 규칙이 7일이 지난 것부터 걷어 간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VKp3E/dJMcagzI6xQ/nusnff096QrkzvUi3K3UWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VKp3E/dJMcagzI6xQ/nusnff096QrkzvUi3K3UWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VKp3E/dJMcagzI6xQ/nusnff096QrkzvUi3K3UWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVKp3E%2FdJMcagzI6xQ%2Fnusnff096QrkzvUi3K3UWk%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;1010&quot; height=&quot;193&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dt1tJu/dJMcajwvg1Y/TDdaoRc4vwlGyOWuT0S6bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dt1tJu/dJMcajwvg1Y/TDdaoRc4vwlGyOWuT0S6bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dt1tJu/dJMcajwvg1Y/TDdaoRc4vwlGyOWuT0S6bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdt1tJu%2FdJMcajwvg1Y%2FTDdaoRc4vwlGyOWuT0S6bk%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;1006&quot; height=&quot;437&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;437&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기까지가 CI다.&lt;/b&gt; push 한 번이면 &amp;mdash; 자격이 섞였는지 훑고, 코드를 검사하고, 이미지를 만들어 취약점까지 확인한 뒤, dev에 머지된 것만 레지스트리에 오른다. 검증을 통과한 이미지가 &lt;code&gt;dev-15-17dcc495&lt;/code&gt;라는 이름으로 레지스트리에 서 있다. 남은 것은 그 이미지를 클러스터가 받아서 돌게 하는 일 &amp;mdash; CD다.&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;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CD &amp;mdash; 배포 엔진은 이미 있다&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;CD 의 뜻&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD는 두 뜻으로 쓰인다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Continuous Delivery     언제든 배포할 수 있는 상태를 유지한다.  버튼은 사람이 누른다
Continuous Deployment   검증을 통과하면 사람 손 없이 배포까지 나간다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 마지막 한 걸음에 사람이 있느냐뿐이다. 환경마다 갈라 쓴다 &amp;mdash; dev는 자동으로 나가고(Deployment), stg&amp;middot;prd는 사람이 승인한다(Delivery). &lt;code&gt;cgv-infra&lt;/code&gt;가 환경을 디렉터리로 갈라 둔 것(&lt;code&gt;environments/dev/&lt;/code&gt;)이 여기서 값을 한다: dev 파일만 자동으로 움직이고 stg&amp;middot;prd 파일은 사람이 고치는 형태가 그대로 성립한다.&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;b&gt;Push 냐 Pull 이냐&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD를 거는 방식이 둘로 갈린다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;Push    CI 가 끝나면 CI 가 클러스터에 직접 명령한다 (kubectl &amp;middot; helm)
        &amp;rarr; 클러스터 접근 자격이 클러스터 밖 CI 에 있어야 한다
Pull    클러스터 안의 도구가 git 을 지켜보다 스스로 당겨 온다
        &amp;rarr; 밖에서 클러스터를 건드리지 않는다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pull 쪽은 이미 서 있다.&lt;/b&gt; 9부의 ArgoCD가 그것이다 &amp;mdash; &lt;code&gt;cgv-infra&lt;/code&gt;를 읽고, 커밋이 오면 webhook을 받아 3초 안에 반영한다. 그래서 CD를 새로 세우는 게 아니라, &lt;b&gt;CI가 만든 이미지가 그 길을 타게 끊긴 곳만 이으면 된다.&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;&lt;b&gt;끊긴 곳이 둘이다&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;①  노드가 레지스트리에서 이미지를 못 받아온다      자격도 없고, 평문 레지스트리를 믿지도 않는다
②  git 이 무엇이 도는지 모른다                  레지스트리엔 dev-42-&amp;hellip; 가 서는데
                                             매니페스트의 tag 는 옛 글자 그대로다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;①을 이어야 파드가 뜨고, ②를 이어야 push 한 번이 파드 교체까지 간다. 순서대로 간다 &amp;mdash; 받을 수 있게 만들어 놓고 자동화한다. 반대로 하면 git은 맞는데 파드만 계속 죽어서, 태그가 틀린 건지 자격이 없는 건지 갈리지 않는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 노드가 받아오게 한다&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;받아오는 주체가 노드다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 이미지를 자기 디스크에 가진 쪽에서만 만들어진다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ArgoCD 가 Deployment 를 만든다
  &amp;darr;
스케줄러가 파드를 노드 하나에 배정한다        셋 중 어디가 될지는 그때 정해진다
  &amp;darr;
그 노드의 kubelet 이 containerd 에 시킨다
  &amp;darr;
containerd 가 디스크에 그 이미지가 없으면 레지스트리에서 받아온다
  &amp;darr;
못 받으면 ImagePullBackOff&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 private이라 받아오는 데도 자격이 필요하다. 내가 손으로 쓴 자격은 내 PC의 &lt;code&gt;~/.docker/config.json&lt;/code&gt;에 있고 러너의 토큰은 job이 끝나면 사라진다 &amp;mdash; &lt;b&gt;노드는 둘 다 볼 수 없다.&lt;/b&gt; 노드는 클러스터 안의 Secret에서만 자격을 읽고, 어느 노드에 뜰지 모르니 셋 다 받아올 수 있어야 한다.&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;b&gt;자격 &amp;mdash; 노드 몫을 좁혀 만든다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;personal access token을 그대로 넣지 않는다. 그건 내 계정에 딸린 열쇠라 내가 닿는 모든 프로젝트에 통한다. 노드에게 필요한 것은 &lt;code&gt;cgv-onprem&lt;/code&gt;의 이미지를 받아오는 권한 하나뿐이다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;personal access token    내 계정.  내가 닿는 전부         손으로 올릴 때 썼다
deploy token             프로젝트 하나.  고른 권한만       노드 몫.  read_registry 만 준다&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; Secret(Deploy token)&lt;/b&gt;은 지금까지의 12종과 타입이 다르다. &lt;code&gt;Opaque&lt;/code&gt;(키&amp;middot;값을 그냥 담는 것)가 아니라 &lt;code&gt;kubernetes.io/dockerconfigjson&lt;/code&gt; &amp;mdash; kubelet이 읽는 정해진 구조다. 그래서 낱개 봉인 스크립트(generic 전용)로는 못 만들고, &lt;code&gt;kubectl&lt;/code&gt;이 만들어 주는 것을 봉인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl create secret docker-registry gitlab-registry -n app \
  --docker-server=192.168.0.167:5050 \
  --docker-username='&amp;lt;deploy token 사용자명&amp;gt;' \
  --docker-password='&amp;lt;토큰&amp;gt;' \
  --dry-run=client -o yaml \
| kubeseal --format yaml \
    --controller-name sealed-secrets --controller-namespace kube-system \
&amp;gt; gitlab-registry.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--docker-server&lt;/code&gt;는 차트의 &lt;code&gt;image.repository&lt;/code&gt; 앞부분과 &lt;b&gt;글자까지 같아야 한다&lt;/b&gt; &amp;mdash; kubelet이 그 문자열 비교로 &quot;이 이미지에는 이 자격&quot;을 매칭한다. 이걸로 봉인본이 13종이 됐다. 재구축 스크립트의 개수 검사와 계약 문서도 같이 움직인다 &amp;mdash; 이 편 뒤쪽에서 image updater 몫이 더해져 최종 14종이 된다.&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;b&gt;배선 &amp;mdash; 차트와 노드가 그것을 보게 한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드가 이미지를 받는 데는 셋이 다 맞아야 한다. 하나만 빠져도 결과는 똑같이 &lt;code&gt;ImagePullBackOff&lt;/code&gt;인데, 사유가 달라 어디가 빠졌는지는 갈린다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;어느 이미지냐      매니페스트의 image          빠지면  no such host
어느 자격으로냐    파드의 imagePullSecrets     빠지면  401 Unauthorized
그 주소를 믿느냐   노드 containerd 설정        빠지면  http: server gave HTTP response to HTTPS client&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;앞의 둘은 차트라 git으로 간다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# charts/apps/cgv-app/values.yaml &amp;mdash; 세 인스턴스가 쓰는 공통 차트라 한 번만
imagePullSecrets:
  - name: gitlab-registry

# charts/apps/queue/values.yaml &amp;mdash; booking &amp;middot; frontend 도 같은 꼴
image:
  repository: 192.168.0.167:5050/cgv/cgv-onprem/queue-go&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;셋째는 노드 파일이다. containerd도 레지스트리를 HTTPS로 가정하므로 평문 주소를 예외로 적는다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# /etc/rancher/k3s/registries.yaml &amp;mdash; 노드 3대 공통
mirrors:
  &quot;192.168.0.167:5050&quot;:
    endpoint:
      - &quot;http://192.168.0.167:5050&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 이 파일에 안 적는다 &amp;mdash; 적으면 노드 디스크에 자격이 평문으로 남는다. 그 몫은 봉인본이 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 노드에 놓고 k3s를 재시작해야 읽힌다. &lt;b&gt;한 대씩 한다.&lt;/b&gt; 노드 셋이 etcd 쿼럼 3이라 한 대씩이면 클러스터가 살고, 둘을 동시에 내리면 쿼럼이 깨져 전체가 멈춘다. 7부에서 미룬 &lt;code&gt;kube-reserved&lt;/code&gt; 2Gi 반영도 같은 재시작에 얹는다 &amp;mdash; 이것도 재시작이 필요한 변경이라, 두 번 내릴 일을 한 번으로 줄인다.&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;b&gt;한 대씩 재시작한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 둘을 scp로 노드 홈에 올려 두고, 한 대씩 들어가 제자리에 놓고 재시작한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 노드마다 (k3s-3 &amp;rarr; k3s-2 &amp;rarr; k3s-1 순)
sudo cp /etc/rancher/k3s/config.yaml /etc/rancher/k3s/config.yaml.bak-1gi
sudo mv ~/config.yaml /etc/rancher/k3s/config.yaml
sudo mv ~/registries.yaml /etc/rancher/k3s/registries.yaml
sudo systemctl restart k3s&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;재시작이라고 워크로드가 끊기지는 않는다. 컨테이너는 k3s가 아니라 별도 프로세스(shim)가 붙들고 있어, k3s가 내려간 몇십 초 동안도 계속 돈다 &amp;mdash; 멈추는 것은 제어면 일(스케줄&amp;middot;kubectl 응답)뿐이다. drain도 걸지 않는다. 로컬 PV에 묶인 파드(ingester WAL 등)는 다른 노드로 못 가는 것들이라, 쫓아내 봐야 Pending으로 떠 있다 제자리로 돌아올 뿐이다.&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;allocatable    6105Mi &amp;rarr; 5081Mi (세 노드 동일)      kube-reserved 2Gi 반영
앱 파드 오류    http: server gave HTTP response to HTTPS client
               &amp;rarr;  frontend:dev: not found&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 바뀐 것이 배선의 증명이다. 앞엣것은 문전 거절 &amp;mdash; 레지스트리에 닿지도 못했다. 뒤엣것은 http로 접속했고 자격 검사까지 통과한 뒤 &quot;그런 태그는 없다&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;&lt;b&gt;태그 한 줄이 첫 배포다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;environments/dev&lt;/code&gt;의 값 파일 셋에서 &lt;code&gt;tag&lt;/code&gt;를 CI가 올린 실물로 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;image:
  tag: dev-15-17dcc495     # &amp;larr; dev 였던 자리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 커밋이 MR로 main에 머지되면 &lt;b&gt;&amp;mdash; webhook이 ArgoCD를 깨우고(cgv-infra gitops)&lt;/b&gt;, sync가 Deployment의 이미지를 바꾸고, 노드가 방금 연 길로 받아온다. 머지 몇 분 뒤 세 서비스가 처음으로 Running이 됐다. 배선을 만든 이래 줄곧 ImagePullBackOff로 서 있던 파드들이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VTjuF/dJMcaazzBTi/ZAQyNN1REQEtP9hgTF2Lpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VTjuF/dJMcaazzBTi/ZAQyNN1REQEtP9hgTF2Lpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VTjuF/dJMcaazzBTi/ZAQyNN1REQEtP9hgTF2Lpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVTjuF%2FdJMcaazzBTi%2FZAQyNN1REQEtP9hgTF2Lpk%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;1183&quot; height=&quot;741&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;741&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;줄어든 살림의 청구서 &amp;mdash; request 를 실측으로 조인다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-reserved 2Gi는 노드당 할당 가능량 1Gi 감소로 돌아온다. 재시작 직후 &lt;code&gt;mimir-ingester-1&lt;/code&gt;이 Pending에 걸렸다 &amp;mdash; WAL이 로컬 PV라 k3s-2에만 갈 수 있는 파드인데, 그 노드의 request 합이 92%라 1Gi짜리가 앉을 자리가 없다.&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;이때 기준은 실측이다. request가 실사용보다 크게 잡힌 컴포넌트를 &lt;code&gt;kubectl top&lt;/code&gt;으로 골라 조인다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                  request     실사용(실측)      조정
mimir-ingester    1Gi         410-510Mi       768Mi     만재(시리즈 30만, 약 1.2Gi) 접근 시 재상향
mimir-compactor   512Mi       54Mi 미만       192Mi     병합 스파이크는 limit 1Gi 가 받는다
tempo             256Mi       54Mi 미만       128Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;limit은 안 건드린다 &amp;mdash; limit은 스케줄링에 쓰이지 않아 자리를 잡아먹지 않는다. 커밋이 머지되자 세 컴포넌트가 새 request로 돌고 ingester-1이 앉았다. 최종 request 비율은 k3s-1 81% &amp;middot; k3s-2 94% &amp;middot; k3s-3 74% &amp;mdash; k3s-2는 남은 자리가 263Mi라, 다음 증설은 다른 노드 몫이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 태그를 git 에 되쓴다&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마지막 한 줄이 남는다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지로 이미지는 &lt;code&gt;dev-*-abcdefg&lt;/code&gt;로 서고 노드는 그것을 받아올 수 있다. 그런데 매니페스트의 &lt;code&gt;tag&lt;/code&gt;는 아직 옛 글자다. 그 한 줄을 누가 고치나.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사람이 매번 손으로     자동화한 의미가 없다.  잊으면 그만인 상태로 되돌아간다
CI 가 고친다          GitOps 저장소 쓰기 자격이 클러스터 밖으로 나간다.  Push 방식으로 회귀
클러스터 안의 도구     9부의 성질이 유지된다                              &amp;larr; 고른 것&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;&lt;b&gt;argocd-image-updater&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터 안에서 레지스트리를 지켜보다 새 이미지가 오르면 git에 되쓰는 도구&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CI 가 push        레지스트리에 dev-43-e5f6a7 이 선다
  &amp;darr;
image-updater     레지스트리를 폴링하다 새 태그를 본다
  &amp;darr;
git 커밋          cgv-infra 의 environments/dev/values-&amp;lt;서비스&amp;gt;.yaml 의 tag: 한 줄
  &amp;darr;
webhook           9부에 걸어 둔 그 길.  3초 안에 ArgoCD 가 refresh
  &amp;darr;
ArgoCD sync       파드가 새 이미지로 교체된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포는 여전히 ArgoCD가 한다.&lt;b&gt; image-updater는 파드를 만지지 않고 커밋 한 줄을 쓸 뿐&lt;/b&gt;이다 &amp;mdash; CI와 CD 사이의 다리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용도 있다. 파드가 하나 늘고, 레지스트리를 뒤지는 폴링이 하나 생기고, &lt;b&gt;git에 쓰는 자격이 새로 필요하다.&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;1003&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QVqQ6/dJMcadJHAuk/gPLonKpHfMBO2BTqCtXbT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QVqQ6/dJMcadJHAuk/gPLonKpHfMBO2BTqCtXbT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QVqQ6/dJMcadJHAuk/gPLonKpHfMBO2BTqCtXbT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQVqQ6%2FdJMcadJHAuk%2FgPLonKpHfMBO2BTqCtXbT1%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;1003&quot; height=&quot;372&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;372&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 data-ke-size=&quot;size16&quot;&gt;이 편에서 자격을 쥔 쪽이 ArgoCD(저장소 읽기) &amp;middot; 나(손 push) &amp;middot; 노드(pull)에서 늘어나는 마지막 자리가 여기다. 되쓰는 대상을 &lt;code&gt;environments/dev/&lt;/code&gt;로 한정하므로 dev만 자동으로 움직이고 stg&amp;middot;prd는 사람이 고치는 구도가 유지된다.&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;b&gt;설치 &amp;mdash; GitOps 로, 단 자리는 가려서&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;updater 자체도 GitOps로 배달한다 &amp;mdash; argo-helm 차트를 App 하나로 선언하고, 값은 이 저장소에서 읽는다. ArgoCD가 이미 떠 있으니 배포 주체의 순환이 없다(본체와 다른 점 &amp;mdash; 본체는 빈 클러스터에 배포해 줄 주체가 없어 최초 한 번을 손으로 설치했다).&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;자리는 골라야 했다. metallb&amp;middot;traefik이 타는 platform 틀은 대상 네임스페이스에 PodSecurity 라벨을 무조건 거는데, updater의 대상은 이미 존재하는 &lt;code&gt;argocd&lt;/code&gt; 네임스페이스다 &amp;mdash; 라벨이 걸리면 ArgoCD 본체까지 enforce 대상이 된다. 그래서 root 직속 App으로 세워 네임스페이스를 건드리지 않는다.&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;버전 하나가 동작 방식을 갈랐다. 핀한 1.2 계열은 &lt;b&gt;컨트롤러 방식&lt;/b&gt;이다 &amp;mdash; &lt;code&gt;ImageUpdater&lt;/code&gt;라는 CR이 있어야 일을 시작하고, 없으면 Application에 annotation이 붙어 있어도 아무것도 하지 않는다(기동 로그: &quot;No ImageUpdater CRs to process&quot;). CR은 처리 대상만 고르고, 무엇을 어떻게 갱신할지는 &lt;code&gt;useAnnotations&lt;/code&gt;로 각 App의 annotation에서 읽게 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 차트 extraObjects 로 같은 App 이 배달
kind: ImageUpdater
spec:
  applicationRefs:
    - namePattern: queue
      useAnnotations: true
    # booking &amp;middot; frontend 동일&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;&lt;b&gt;자격 둘 &amp;mdash; 읽는 손과 쓰는 손&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;updater가 손대는 곳이 저장소 둘&lt;/b&gt;이라, 자격도 저장소마다 하나씩이다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;레지스트리 폴링    cgv-onprem 의 deploy token (read_registry)
                  &amp;mdash; 레지스트리가 그 프로젝트에 딸려 있다.  노드 pull 자격과 권한이 같지만
                    주체(노드/봇)가 달라 가른다 &amp;mdash; 새면 그것만 폐기
git 커밋          cgv-infra 의 project access token (write_repository, role Maintainer)
                  &amp;mdash; 태그를 적는 값 파일이 그 저장소에 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 토큰의 role이 Maintainer인 것은 protected main 때문이다. main은 직접 push가 막혀 있고(사람은 MR로만 간다), 무료판(CE)은 push 예외를 유저 단위가 아니라 &lt;b&gt;role 단위로만&lt;/b&gt; 열 수 있다 &amp;mdash; &quot;Allowed to push = Maintainers&quot;로 열고, 토큰을 그 role로 만들어 봇만 문을 통과하게 한다. 토큰을 만들면 GitLab이 봇 유저(&lt;code&gt;project_N_bot_&amp;hellip;&lt;/code&gt;)를 같이 만드는데, 이후 자동 커밋의 작성자가 그 봇으로 찍힌다 &amp;mdash; git 이력에서 사람과 자동화가 작성자로 갈린다.&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;b&gt;어느 태그를 &quot;새것&quot;으로 보나&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;update-strategy: newest-build     빌드 시각 기준.  태그(dev-&amp;lt;번호&amp;gt;-&amp;lt;해시&amp;gt;)가 사전순 정렬이
                                  안 되는 형태라(dev-9 &amp;gt; dev-15) 시각으로 고른다
allow-tags: regexp ^dev-&amp;hellip;$        dev 계열만 후보.  MR 파이프라인의 ci-pipeline-* 은 배포 후보가 아니다
write-back-target: helmvalues     이 App 이 읽는 바로 그 값 파일(environments/dev/values-&amp;lt;앱&amp;gt;.yaml)의
                                  image.tag 에 쓴다&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;&lt;b&gt;평문의 마지막 청구서 &amp;mdash; 자격 경로를 바꾼다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 실행에서 updater가 새 태그를 발견하고 커밋하려다 죽었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;could not get creds for repo 'http://&amp;hellip;/cgv-infra.git': unknown repository type&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전용 시크릿으로 git 자격을 주는 경로(&lt;code&gt;git:secret:&lt;/code&gt;)는 &lt;b&gt;코드가 ssh와 https 저장소만 받는다.&lt;/b&gt; 이 GitLab은 평문 http라, 설정으로 열 수 없는 벽이다 &amp;mdash; 이 편에서 평문이 만든 네 번째 비용이고, 유일하게 예외 등록으로 못 넘는 것이었다.&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;우회는 같은 코드 안에 있다. ArgoCD의 저장소 자격을 빌리는 경로(repocreds)는 스킴을 검사하지 않는다. 그래서 저장소 자격(&lt;code&gt;argocd-repo-cgv-infra&lt;/code&gt;)에 쓰기 토큰을 넣어 &lt;b&gt;읽기와 쓰기를 한 자격으로 통합&lt;/b&gt;했다. 원칙의 후퇴다 &amp;mdash; 다만 노출 반경은 안 변한다: 쓰기 토큰은 어차피 같은 네임스페이스의 봉인본에 있었고, 그 네임스페이스를 뚫은 쪽은 어느 구성이든 그것을 얻는다. TLS를 도입하는 날 read 전용 토큰 + 전용 쓰기 시크릿으로 되돌린다 &amp;mdash; 계약 문서에 되돌리기 항목으로 적어 뒀다.&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;b&gt;한 바퀴 &amp;mdash; 사람 손 없이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;frontend에 한 줄짜리 커밋을 만들어 경로 전체를 태웠다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;사람      코드 커밋 &amp;rarr; MR 머지                    &amp;larr; 마지막 개입
54초      CI 5개 job &amp;mdash; 바뀐 서비스 것만 생겼다     dev-18-767410b6 이 레지스트리에
2분 내    봇 커밋                                 author: argocd-image-updater
                                                 &quot;updates image &amp;hellip; 'dev-15-&amp;hellip;' to 'dev-18-&amp;hellip;'&quot;
수십 초   webhook &amp;rarr; ArgoCD sync &amp;rarr; 파드 교체&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;폴링 주기는 2분이다. ArgoCD처럼 webhook으로 당기지 않는 것은 보내는 쪽이 없어서다 &amp;mdash; GitLab 무료판의 컨테이너 레지스트리는 &quot;이미지 올라옴&quot; 이벤트를 쏘는 기능이 없다. 폴링 비용은 내 서버&amp;middot;같은 망&amp;middot;이미지 세 개의 태그 목록 조회라 0에 가깝다. 폴링이 기본이고 webhook은 보내는 쪽이 있을 때의 가속이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;git에 적으면 배포된다&quot;가 &quot;머지하면 배포된다&quot;가 됐다.&lt;/b&gt; 사람은 코드와 승인만 남고, 이미지 이름이 오가는 길은 전부 기계가 맡는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 대조 &amp;mdash; 이은 것과 빌린 것&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;끊긴 곳은 이어졌나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끊긴 곳은 둘이었다 &amp;mdash; &lt;b&gt;노드가 이미지를 못 받는 것&lt;/b&gt;, &lt;b&gt;새 태그를 git이 모르는 것&lt;/b&gt;. 둘 다 이어졌고, 그 증명이 위의 한 바퀴다. 코드 한 줄이 머지 클릭 하나를 지나 3-4분 안에 파드가 된다. 이 편의 목적이었던 문장이 실물이 됐다.&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;b&gt;이 편 안에서 닫은 것&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;손 push 토큰(docker-push-manual) &amp;middot; 옛 ArgoCD 읽기 토큰    revoke &amp;mdash; CI job 토큰과 새 자격이 대체
Pipelines must succeed                                  켬 &amp;mdash; 빨간불이면 머지 버튼이 잠긴다
러너 allowed_images                                      6종 화이트리스트 &amp;mdash; 소켓을 쥔 러너가
                                                        아무 이미지나 못 띄우게
같은 커밋이 두 번 돌던 workflow 구멍                       브랜치 파이프라인을 dev 로 한정
백업                                                    sealed-secrets 개인키 &amp;middot; GitLab 본체&amp;middot;설정을
                                                        클러스터 밖으로 반출&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;&lt;b&gt;무엇을 빌렸나 &amp;mdash; 부채 장부&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;평문 http 청구서 4건       ① 데스크탑 insecure-registries  ② 노드 registries.yaml
                          ③ updater 레지스트리 insecure    ④ git 자격 경로 통합(코드 벽)
                          TLS 는 이름(DNS)이 생기는 네트워크 재구성 편에서 넷을 함께 걷는다.
                          ④의 자격 통합은 그때 원상복구.
                          트립와이어: 코드 벽이 한 번 더 나오면 그 자리에서 TLS 를 당긴다

러너 소켓 마운트           호스트 도커 전권.  Protected 러너는 안 켰다 &amp;mdash;
                          MR 파이프라인이 비보호 브랜치에서 돌아야 해서 켜면 CI 가 굶는다

러너 config.toml 이 git 밖  concurrent&amp;middot;캐시 볼륨 설정이 도커 볼륨 안에만 있다.
                          데스크탑을 갈아엎으면 사라진다 (GitLab 백업이 유일한 사본)

CI 이미지의 데몬 축적       판마다 이미지 셋이 데스크탑 데몬에 쌓인다.  레지스트리 cleanup 은
                          걸었는데 데몬 쪽 지우는 규칙이 없다

캐시의 디스크              trivy 캐시 8.5GB(잠금 회피로 DB 네 벌).  도커 VM 디스크(VHDX)는
                          안에서 지워도 저절로 안 줄고, 캐시 볼륨은 volume prune 에 지워진다

GitLab 무인 운영 불가      Docker Desktop 이 로그인 세션 앱이라 재부팅 후 로그인 전엔 안 뜬다
미러 토큰 평문             GitHub PAT 가 GitLab 미러 설정 안에 산다
ingester request           만재(시리즈 30만) 접근 시 재상향 &amp;mdash; 부하 실측 편에서 시리즈 수와 함께&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;&lt;b&gt;실무&amp;middot;보안 되짚기&lt;/b&gt;&lt;/h4&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;middot;로테이션 계획이 없다. deploy token은 무기한으로 만들었고, PAT 만료가 오면 봇 커밋이 조용히 죽는다 &amp;mdash; 만료 캘린더든 만료 알림이든, 죽기 전에 아는 장치가 실무의 기본이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 출처 증명이 없다.&lt;/b&gt; 레지스트리의 이미지가 &quot;이 파이프라인이 만든 것&quot;이라는 증거는 push 자격뿐이다. 서명(cosign)&amp;middot;SBOM이 없어, 자격이 새면 위조 이미지가 정품과 구분되지 않는다. 게이트 다음 단계의 공급망 방어다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;게이트는 기계뿐이다.&lt;/b&gt; 파이프라인 초록이 머지 조건이 됐지만 사람 리뷰(approval)는 0명 설정이다 &amp;mdash; 혼자라서 그런 것이고, 사람이 늘면 가장 먼저 켤 항목.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백업이 반출까지고 검증 전이다.&lt;/b&gt; 개인키&amp;middot;GitLab을 클러스터 밖에 뒀지만 &quot;그걸로 실제로 복원되는가&quot;는 안 해 봤다. 복원 리허설 없는 백업은 희망사항이다 &amp;mdash; VM 백업(vzdump)&amp;middot;etcd 스냅샷 반출과 함께 남은 몫.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 다음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드는 돌지만 아직 앱이 잘되는지 체크해보지는 않았다 &amp;mdash; 트래픽 0, 테스트 0. 11부에서 앱을 실제로 태운다: 대기열 입장부터 좌석 선택, 예매 확정까지 흐름을 돌려 보고, 그 동작을 근거로 테스트를 써서 이 편에 자리만 만들어 둔 test 칸을 채운다. 지킬 대상이 실물이 됐으니 RBAC과 NetworkPolicy 최소 한 벌도 그때 함께 붙인다.&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/221</guid>
      <comments>https://zed6740.tistory.com/221#entry221comment</comments>
      <pubDate>Sun, 9 Aug 2026 17:06:41 +0900</pubDate>
    </item>
    <item>
      <title>번외 &amp;mdash; CI 파이프라인 기록</title>
      <link>https://zed6740.tistory.com/220</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;10부 CI는 완성본만 설명 했다. 이 글은 순서를 뒤집는다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;최종본 해부&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; stage마다, job마다 무엇이고 왜 그 자리에 있는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여정&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; job 실타래마다, 뭐에 막혔고 무슨 작업으로 풀었는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;판정&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 그래서 이게 맞는 CI인지&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;최종본 &amp;mdash; 지금 무엇이 돌고 있나&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;push 한 번에 이렇게 흐른다.&lt;/p&gt;
&lt;pre class=&quot;smali&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;push ─&amp;rarr; check ─&amp;rarr; test ─&amp;rarr; build ─&amp;rarr; scan ─&amp;rarr; publish
        4 job    (빈칸)   3 job    3 job    3 job (dev 에서만)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;stage는 벽이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;한 칸에서 실패가 하나라도 나오면 다음 칸은 실행되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;job은 &quot;명령 목록 + 그걸 돌릴 이미지&quot;다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;러너가 컨테이너를 띄워 돌리고, 끝나면 버린다. 그래서 모든 job은 두 질문으로 읽힌다 &amp;mdash; 무엇을 보는가, 실패가 무엇을 뜻하는가.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;check &amp;mdash; 코드 층의 게이트 넷&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이미지를 만들기 전에, 소스만 보고 판정할 수 있는 것들. 넷은 서로 독립이라 한 칸에서 동시에 돈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;secret-scan &amp;mdash; 자격이 섞였나 (gitleaks)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무엇을 보나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 커밋에 토큰&amp;middot;키&amp;middot;비밀번호가 들어갔는지. 대상은 히스토리 전체(GIT_DEPTH: 0) &amp;mdash; 자격은 지워도 과거 커밋에 남는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;언제 도나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 조건 없이 모든 커밋. 어느 파일이 바뀌든 자격은 섞일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 하나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;--redact: 찾은 값을 로그에 안 찍는다. CI 로그도 유출 경로다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패의 의미&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 유출. 대응은 커밋 수정이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 자격의 폐기&amp;middot;재발급이 먼저&lt;/b&gt;다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;dep-scan:queue-go &amp;mdash; 선언한 의존성에 알려진 결함이 있나 (trivy fs)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무엇을 보나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;go.sum의 모듈 버전들을 공개 취약점 목록과 대조.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CVE란&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 공개된 보안 취약점 하나하나에 붙는 전 세계 공통 일련번호. 번호가 있어야 스캐너가 &quot;이 버전에 이 결함 있음&quot;을 기계적으로 대조한다. 심각도는 별도로 CVSS 점수(0&amp;ndash;10)로 매기며 HIGH&amp;middot;CRITICAL이 그 등급이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;언제 도나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 조건 없이 모든 커밋. 단 이유가 secret-scan과 다르다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;코드가 그대로여도 새 CVE 발표 하나로 결과가 바뀐다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;어제 통과한 커밋이 오늘 실패할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패의 의미&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 내 잘못이 아니라 낡음. 대응은 버전 올리기.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;lint:queue-go &amp;mdash; 컴파일은 되는데 틀린 코드 (go vet)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무엇을 보나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; Printf 인자 불일치, 잠긴 뮤텍스 복사처럼 컴파일러는 통과시키지만 의도와 다르게 도는 자리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특성&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 판정에 타입 정보가 필요해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의존 패키지를 전부 컴파일한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 job의 시간은 코드 크기가 아니라 컴파일 캐시가 정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패의 의미&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 코드 결함. 대응은 코드 수정.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;lint:booking &amp;mdash; 바이트코드 정적 분석 (SpotBugs)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무엇을 보나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; null 역참조&amp;middot;자원 미해제&amp;middot;오버라이드 실수류. 소스가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컴파일된 바이트코드&lt;/b&gt;를 훑는 도구라&lt;span&gt;&amp;nbsp;&lt;/span&gt;mvn compile이 앞에 온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;딸린 설정 셋&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;왜&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAVEN_OPTS: -Xmx768m&lt;/td&gt;
&lt;td&gt;JVM은 힙 상한이 없으면 &quot;컨테이너가 보는 메모리&quot;의 25%를 잡는다. job 컨테이너에 제한이 없어 VM 전체가 보인다 &amp;rarr; 상한 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;artifacts&lt;span&gt;&amp;nbsp;&lt;/span&gt;(when: always)&lt;/td&gt;
&lt;td&gt;콘솔엔 개수만 남고 상세는 리포트 파일에 있다. 컨테이너째 사라지기 전에 GitLab에 올린다. 실패했을 때야말로 받아 봐야 하므로 always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spotbugs-exclude.xml&lt;/td&gt;
&lt;td&gt;도구가 프레임워크를 모른다. 스프링 생성자 주입을 캡슐화 위반으로 오탐 &amp;rarr; 그 패턴만 근거와 함께 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실패의 의미&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 코드 결함이거나 오탐. 오탐이면 &quot;왜 오탐인지&quot;를 필터에 근거로 남기고 좁게 제외한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;test &amp;mdash; 비어 있는 칸&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stage 선언만 있고 job이 없다. 11부에서 채운다.&lt;/li&gt;
&lt;li&gt;미룬 이유 &amp;mdash; 테스트는 앱을 띄워 동작을 보면서 써야 의미 있는 단언이 나온다. 껍데기 테스트를 채우면 초록불이 거짓말을 한다.&lt;/li&gt;
&lt;li&gt;이 칸이 빈 동안 이 파이프라인을 뭐라고 불러야 하는지는 판정 절에서.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;build &amp;mdash; 이미지를 만든다, 올리지는 않는다&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이미지&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;docker:27-cli. CLI만 들어 있고 데몬은 소켓 너머 데스크탑 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI는 한 줄만 안다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;docker build ./$SVC. Go 컴파일인지 Maven인지 정적 파일 복사인지는 각 서비스의 멀티스테이지 Dockerfile이 품는다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;언어 차이를 CI가 아니라 Dockerfile이 가둔다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;--pull&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 베이스 이미지를 매번 레지스트리에서 확인. 같은 태그라도 업스트림은 패치를 넣어 다시 굽는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;산출물&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; job 컨테이너가 아니라 데몬에 남는다. 다음 칸이 이름($SVC:$TAG)으로 이어받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;scan &amp;mdash; 만든 이미지의 실물 검사 (trivy image)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;dep-scan이 &quot;선언&quot;을 봤다면 여기는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;실제로 담긴 것&quot;&lt;/b&gt;을 본다 &amp;mdash; 베이스 OS 패키지, jar 안 라이브러리, Go 바이너리 안 모듈까지 이미지를 열어 읽는다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;--severity HIGH,CRITICAL   높은 등급만
--ignore-unfixed           수정판이 나온 것만    &amp;larr; &quot;막으려면 고칠 수 있어야 한다&quot;
--exit-code 1              걸리면 job 실패 &amp;rarr; publish 가 안 열린다&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수정판이 없는 결함으로 막으면 파이프라인이 영구히 빨간불이 되고, 결국 게이트를 끄게 된다. 그래서 고칠 수 있는 것만 막는다.&lt;/li&gt;
&lt;li&gt;운영 특성 둘 &amp;mdash; 캐시 디렉터리가 job 이름별로 갈라져 있다(TRIVY_CACHE_DIR). 제한 시간이 20분이다(TRIVY_TIMEOUT). 왜 그래야 했는지는 여정의 scan 실타래에 나온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;publish &amp;mdash; 검증 통과본만, dev 만&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;$CI_COMMIT_BRANCH == &quot;dev&quot;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; dev가 아니면 이 job은 화면에 생기지도 않는다. 아무 브랜치의 이미지가 배포 후보와 같은 자리에 쌓이지 않게 하는 벽.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그인&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; job마다 발급되고 끝나면 폐기되는 토큰. 표준 입력으로 넣는다(--password-stdin) &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-p는 비밀번호가 명령줄 인자에 남는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;interruptible: false&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; push가 중간에 끊기면 레지스트리에 레이어가 일부만 올라간 채 남는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;파일 밖의 짝 &amp;mdash; 러너와 Dockerfile&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;.gitlab-ci.yml만으로는 이 파이프라인이 재현되지 않는다.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;&lt;b&gt;어디&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.6977%;&quot;&gt;&lt;b&gt;무엇&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 51.6279%;&quot;&gt;&lt;b&gt;왜&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;러너&lt;span&gt;&amp;nbsp;&lt;/span&gt;config.toml&lt;/td&gt;
&lt;td style=&quot;width: 30.6977%;&quot;&gt;&lt;b&gt;concurrent = 2&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 51.6279%;&quot;&gt;러너가 동시에 드는 job 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;러너&lt;span&gt;&amp;nbsp;&lt;/span&gt;config.toml&lt;/td&gt;
&lt;td style=&quot;width: 30.6977%;&quot;&gt;&lt;b&gt;캐시 볼륨 넷 (maven&amp;middot;trivy&amp;middot;gomod&amp;middot;gobuild)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 51.6279%;&quot;&gt;job 컨테이너는 버려진다. 내려받은 것을 남기려면 밖(볼륨)에 둬야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;각 Dockerfile&lt;/td&gt;
&lt;td style=&quot;width: 30.6977%;&quot;&gt;&lt;b&gt;BuildKit 캐시 마운트 (RUN --mount=type=cache)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 51.6279%;&quot;&gt;docker build는 데몬에서 돈다. 러너 볼륨이 안 닿는다 &amp;mdash; 캐시가 두 층인 이유&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 상태로 캐시가 채워진 판이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;job 10개, 1분 54초&lt;/b&gt;다. 여기까지가 지도다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;여정&amp;nbsp;&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;판     결과      시간      한 일
#1     Passed    6:06     3단(check&amp;middot;build&amp;middot;package) 첫 판.  이미지 빌드 + push
#2     Passed    0:46     로그인을 표준 입력으로
#3~4   Passed    0:40대   push 는 dev 머지 뒤에만
#5     Failed    9:03     5단 재편.  scan 셋 전부 실패 &amp;mdash; 72건
#6     Failed   10:04     1차 수정.  booking 21 &amp;middot; frontend 33 남음
#7     Failed   11:16     2차 수정 + 새 게이트 둘.  booking 1 &amp;middot; SpotBugs 23 &amp;middot; 429 등장
#8     Warning  10:15     3차 수정.  게이트 전부 통과, 유예 하나만 빨강
#9     Failed    8:42     캐시 도입.  캐시끼리 충돌
#10    Failed   14:26     충돌 해소.  이번엔 시간 초과
#11    Passed    4:27     시간 제한 상향.  전부 초록 (유예 2개 있는 상태)
#12    Passed    3:40     유예 제거.  열 job 이 전부 막는 게이트로
#13    Passed    4:00     메모리 상한 &amp;middot; 자동 취소 &amp;middot; Go 컴파일 캐시
#14    Passed    1:54     빈 커밋 &amp;mdash; 캐시가 채워진 상태의 실측&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래부터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실타래순&lt;/b&gt;이다 &amp;mdash; 절 하나가 job 하나의 처음부터 끝. 절의 배열은 이야기가 시작된 판 순서라, 기록표와 같은 방향으로 흐른다.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;&lt;b&gt;실타래&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;&lt;b&gt;&lt;span style=&quot;white-space: pre;&quot;&gt; &lt;/span&gt;판&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;&lt;b&gt;막힌 것&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;&lt;b&gt;한 작업&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;&lt;b&gt;지금&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;출발점&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#1&amp;ndash;#4&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;태그 규칙 &amp;middot; dev 한정 &amp;middot; stdin 로그인&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;공통 배경&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;판마다 같은 것을 다시 받음&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;캐시 두 층&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;scan 셋&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#5&amp;ndash;#11&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;72건 &amp;rarr; 잠금 충돌 &amp;rarr; 시간 초과&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;코드&amp;middot;베이스 수정 &amp;rarr; 캐시 폴더 분리 &amp;rarr; TIMEOUT 20m&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;0:12&amp;ndash;0:32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;게이트 반입&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#7 &amp;rarr; #12&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;새 게이트가 첫 판부터 막음&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;allow_failure 유예 &amp;rarr; 정리 &amp;rarr; 제거&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;유예 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;dep-scan&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#7&amp;ndash;#10&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;429 차단&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;대상을 queue-go로 축소&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;0:15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;lint:booking&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#7&amp;ndash;#13&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;오탐 23 &amp;rarr; 상세 안 보임 &amp;rarr; 힙 2GB&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;필터 &amp;rarr; artifacts &amp;rarr; MAVEN_OPTS&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;0:36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;build 셋&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#9&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;의존성 바꾼 커밋마다 전부 재다운로드&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;BuildKit 캐시 마운트&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;0:11&amp;ndash;0:16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;lint:queue-go&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#12&amp;ndash;#14&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;판마다 전부 재컴파일 + 옆 job까지 밀림&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;컴파일 결과 캐시 볼륨&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;0:14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;캐시의 대가&lt;/td&gt;
&lt;td style=&quot;width: 8.48837%;&quot;&gt;#14 결산&lt;/td&gt;
&lt;td style=&quot;width: 29.186%;&quot;&gt;디스크 &amp;mdash; C: 여유 1GB&lt;/td&gt;
&lt;td style=&quot;width: 40.1163%;&quot;&gt;(부채로 기록)&lt;/td&gt;
&lt;td style=&quot;width: 9.4186%;&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;출발점 &amp;mdash; 검증 없이 올리던 시절 (#1&amp;ndash;#4)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 파일은 stage 셋(secret scan&amp;middot;build&amp;middot;package)이었고 네 판이 다 통과했다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;통과한 이유는 잘돼서가 아니라 검증이 없어서다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 시기에 정해져 뒤까지 살아남은 것:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;태그 규칙&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;브랜치&amp;gt;-&amp;lt;파이프라인번호&amp;gt;-&amp;lt;커밋해시&amp;gt;&lt;/li&gt;
&lt;li&gt;push는 dev 머지 뒤에만&lt;/li&gt;
&lt;li&gt;로그인은 표준 입력으로&lt;/li&gt;
&lt;li&gt;&lt;b&gt;6:06 &amp;rarr; 0:46 실측&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 소켓 마운트 덕에 레이어 캐시가 job 컨테이너 밖(데몬)에 남는다는 첫 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;공통 배경 &amp;mdash; 캐시란, 그리고 왜 CI에서 문제인가&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러 실타래가 이 개념 위에 서 있어서 먼저 놓는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;캐시란&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 한 번 받아 둔 것을 버리지 않고 뒀다가, 다음에 같은 게 필요할 때 다시 쓰는 저장소.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 CI에서 문제인가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; job 컨테이너가 버려질 때 안에서 내려받은 것도 같이 사라진다. 캐시가 없으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;매 판이 처음&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하면 뭐가 바뀌나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 첫 판만 받고, 그다음 판부터는 네트워크에 안 나간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 파이프라인에서 &quot;판마다 다시 받던 것&quot;이 이만큼이었다. #7의 11:16 중 실제 작업은 2&amp;ndash;3분, 나머지가 전부 다운로드다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;trivy 취약점 DB   103MB    &amp;times; scan 계열 job 4개
trivy Java DB     903MB    &amp;times; 1 (booking)
Maven 의존성       수백 개   &amp;times; lint 와 build 각각
Go 모듈            수십 개&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;trivy DB가 뭔가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 스캐너가 대조할 &quot;알려진 결함 목록&quot;. 전 세계 패키지의 CVE 목록이라 크고, 새 CVE가 매일 등록되므로 낡으면 다시 받는다(3일 만료).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 다운로드가 뭔가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;pom.xml&amp;middot;go.mod에 적힌 건 이름과 버전뿐이다. 실물(jar&amp;middot;소스)은 중앙 저장소에서 받아야 컴파일이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;남길 자리는 두 층이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;job 안에서 받는 것 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;러너 볼륨&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;docker build&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에서 받는 것 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;BuildKit 마운트&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(빌드는 데몬에서 돌아 러너 볼륨이 안 닿는다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;둘 다 #9에서 넣었다. 그 뒤 각 실타래에서 무슨 일이 났는지가 아래다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;scan 셋 &amp;mdash; 72건에서 0까지, 그리고 스캐너 자신의 두 사건 (#5&amp;ndash;#11)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;72건이 쏟아졌다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5단 재편 첫 판(#5)에서 scan 셋이 전부 실패했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;scan:queue-go    HIGH 2
scan:booking     HIGH 33 &amp;middot; CRITICAL 4
scan:frontend    HIGH 31 &amp;middot; CRITICAL 2&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;게이트가&lt;span&gt;&amp;nbsp;&lt;/span&gt;--ignore-unfixed니 72건은 전부&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수정판이 이미 나와 있는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;결함이다. 문제는 왜 이렇게 쌓였느냐 &amp;mdash; 서비스마다 층이 달랐다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;queue-go &amp;mdash; 간접 의존 (2건)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;걸린&lt;span&gt;&amp;nbsp;&lt;/span&gt;golang.org/x/text&amp;middot;google.golang.org/grpc는 직접 import한 적이 없다. OTel 익스포터와 gin의 검증기가 딸고 들어온&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;간접 의존&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;그래도 바이너리에 들어가는 것은 마찬가지라 스캐너가 잡는다.&lt;/li&gt;
&lt;li&gt;수정 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;go get &amp;lt;모듈&amp;gt;@&amp;lt;버전&amp;gt; &amp;amp;&amp;amp; go mod tidy&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 줄.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;booking &amp;mdash; BOM이 정하는 버전들 (37건)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jar에서 37건, 베이스 OS는 0건. 걸린 tomcat&amp;middot;spring&amp;middot;jackson&amp;middot;netty는 전부&lt;span&gt;&amp;nbsp;&lt;/span&gt;pom.xml에 버전이 안 적힌 것들이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BOM(Bill of Materials)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; Spring Boot 부모 POM이 수백 개 라이브러리의 버전을 한 번에 정한다. 개발자는 이름만 적고 버전은 부모가 준다. 대가 &amp;mdash; 부모를 안 올리면 딸린 전부가 함께 낡는다. 3.3.4는 그 사이 tomcat에 RCE(원격 코드 실행)급이 쌓일 만큼 낡아 있었다.&lt;/li&gt;
&lt;li&gt;수정도 같은 구조로, 부모 한 줄. 다만 한 번에 안 끝났다:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lsl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;3.5.14     37 &amp;rarr; 21     전부 &quot;한 패치 모자람&quot; (#6)
3.5.16     21 &amp;rarr; 1      3.5 라인 최신 (#7)
netty 핀    1 &amp;rarr; 0      BOM 이 주는 4.1.135 가 수정판 4.1.136 보다 한 칸 낮았다 (#8)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BOM 기본값 &amp;rarr; 안 되는 것만 개별 핀.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;순서가 그렇다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;frontend &amp;mdash; 죽은 베이스 태그 (33건)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드도 의존성도 없는 서비스에서 33건. 전부 베이스 이미지(alpine 3.21.3)의 OS 패키지.&lt;/li&gt;
&lt;li&gt;처음엔 로컬 캐시가 낡은 탓으로 보고&lt;span&gt;&amp;nbsp;&lt;/span&gt;--pull을 붙였다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;33건이 그대로였다 &amp;mdash; 진단이 틀렸다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실제 원인 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;nginx-unprivileged:1.27-alpine은 업스트림이 더 굽지 않는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;멈춘 태그&lt;/b&gt;. 새로 받아도 같은 낡은 이미지가 온다.&lt;/li&gt;
&lt;li&gt;살아 있는 라인(1.31-alpine)으로 옮기니 0.&lt;span&gt;&amp;nbsp;&lt;/span&gt;--pull은 살아 있는 태그에는 맞는 조치라 남겼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셋을 관통하는 결론&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;queue-go    모듈 버전을 고정해 뒀다     &amp;rarr;  낡았다
booking     BOM 버전을 고정해 뒀다      &amp;rarr;  낡았다
frontend    베이스 태그를 고정해 뒀다    &amp;rarr;  낡았다 (태그가 죽기까지 했다)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고정해 둔 것은 시간이 지나면 낡는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;코드가 안 바뀌었으니 아무 신호가 없었고, 스캔이 처음으로 그 시간을 드러냈다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사건 1 &amp;mdash; 캐시가 서로를 깨뜨렸다 (배타 잠금)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;찾은 것을 다 고치자, 이번엔 스캐너 자신이 깨졌다. #9에서 scan 둘이 죽었다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;cache may be in use by another process: timeout.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;잠금(lock)이 뭔가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 여러 프로세스가 같은 파일을 동시에 고치면 반쯤 쓰인 데이터를 읽는 쪽이 생긴다. 그래서 &quot;하나가 잡으면 나머지는 기다린다&quot;는 표시를 걸어 둔다. '배타'는 한 번에 하나만.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 터졌나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; trivy는 DB 갱신을 지키려고 캐시 디렉터리에 이 잠금을 건다. 볼륨 하나를 scan 셋이 공유했는데, 같은 판에&lt;span&gt;&amp;nbsp;&lt;/span&gt;concurrent(러너가 동시에 드는 job 수)를 1&amp;rarr;2로 올렸다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;둘이 실제로 동시에 그 디렉터리를 열었고&lt;/b&gt;, 늦은 쪽은 기다리다 죽었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배운 것&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 동시성은 숨어 있던 공유 자원을 드러낸다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해소&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;TRIVY_CACHE_DIR를 job별 폴더로(#10). 잠금이 안 겹친다. 대가 &amp;mdash; DB가 폴더 수만큼 여러 벌(디스크 실타래에서 돌아온다).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사건 2 &amp;mdash; 시간 제한의 범위&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폴더를 가르자 다음이 나왔다. #10에서 scan:booking이&lt;span&gt;&amp;nbsp;&lt;/span&gt;context deadline exceeded(시한 초과)로 죽었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;타임아웃이 뭔가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; &quot;이 시간 안에 안 끝나면 강제로 실패&quot;라는 상한. 멈춘 작업이 슬롯을 무한정 잡는 것을 막는 장치다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함정은 범위다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; trivy 기본 5분 안에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB 내려받기가 포함된다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;갈라진 새 폴더에 Java DB 903MB를 받는데, 옆 scan이 같은 회선으로 제 DB를 받고 있어 9분 10초가 걸렸다. 스캔은 시작도 못 하고 시간이 끝났다 &amp;mdash; 취약점 유무와 무관한 실패.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해소&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;TRIVY_TIMEOUT: 20m(#11). 캐시가 더운 판은 이 값 근처에도 안 간다. 죽은 판이 캐시는 채워 놓아서(로그 끝 &quot;Java DB is cached&quot;) 다음 판부터 다운로드 줄 자체가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 실타래의 끝 &amp;mdash; scan:booking 3:21 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0:32&lt;/b&gt;.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;게이트 반입 &amp;mdash; allow_failure 유예 &amp;rarr; 정리 &amp;rarr; 제거 (#7 &amp;rarr; #12)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 두 실타래(dep-scan &amp;middot; lint:booking)는 #7에서 함께 들어왔다. 들어올 때 공통 장치가 있었다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;allow_failure: true. job은 돌고 결과도 남지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실패해도 뒤 stage를 막지 않는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;스위치다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;왜&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; stage는 벽이다. 새 게이트가 check에서 빨간불이면 build&amp;middot;scan이 아예 안 돈다. 새 게이트는 &quot;이미 쌓여 있던 것&quot;을 처음 훑는 것이라 첫 판에 반드시 뭔가를 들고 나온다(실제로 SpotBugs 23건, dep-scan은 못 돌았다). 막는 상태로 넣었다면 같은 판의 이미지 쪽 수정이 먹혔는지 확인할 길이 없었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; ① 유예로 넣는다(무엇을 잡는지 본다) ② 잡힌 것을 정리한다 ③&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유예를 걷는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;③을 안 하면 아무도 안 보는 노란불로 남는다 &amp;mdash; 게이트가 아니라 장식이 된다.&lt;/li&gt;
&lt;li&gt;#12에서 두 줄을 지웠다. 그 판부터 열 job이 전부 조건 없이 막는다.&lt;/li&gt;
&lt;li&gt;파이프라인 상태가 넷인 이유가 여기 있다 &amp;mdash; Passed /&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Warning&lt;/b&gt;(통과인데 유예 job 중 실패가 있음, #8이 그것) / Failed / Canceled.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 장치를 달고 들어온 두 job이 각자 무엇에 막혔는지가 다음 두 실타래다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;dep-scan &amp;mdash; 남의 서버가 화를 냈다 (429, #7&amp;ndash;#10)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;붙인 첫 판(#7)부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;429 Too Many Requests로 죽었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;rate limit이 뭔가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 서버가 한 이용자(보통 IP 단위)의 요청 횟수에 상한을 거는 것. Maven Central은 전 세계가 무료로 쓰는 공용 서버라, 한 IP가 과도하게 받으면 다른 이용자 몫이 줄어든다. 넘으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;429와&lt;span&gt;&amp;nbsp;&lt;/span&gt;Retry-After(몇 초 뒤에 오라)를 돌려준다 &amp;mdash; 처음엔 89초, 나중엔 30분.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 걸렸나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 당시 dep-scan은 저장소 전체를 봤다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;pom.xml은 버전을 부모 POM이 정하므로 trivy가 실제 버전을 알려면 Maven Central에서 부모를 받아야 한다. 그런데 같은 공인 IP에서 booking 빌드가 판마다 의존성 수백 개를 받고 있었다. 서버 눈에는 전부 한 명이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배운 것&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 원인이 내 코드가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;외부 서비스의 정책&lt;/b&gt;인 실패가 있다. 오류의 주체가 누구인지부터 갈라야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해소&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 대상을&lt;span&gt;&amp;nbsp;&lt;/span&gt;./queue-go로 좁힌다(#10).&lt;span&gt;&amp;nbsp;&lt;/span&gt;go.sum은 모든 버전이 파일 안에 있어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원격 조회가 아예 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;429가 &quot;덜 나는&quot; 게 아니라 날 수가 없어진다.&lt;/li&gt;
&lt;li&gt;Java를 버린 게 아니다 &amp;mdash; scan:booking이 jar 실물을 읽는 쪽이 선언 추정보다 정확하고, 37건을 0까지 몰고 간 것도 그쪽이다. 잃은 것은 &quot;build 전에 잡는다&quot;는 이점 하나.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 실타래의 끝 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0:15&lt;/b&gt;, 원격 조회 0회.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;lint:booking &amp;mdash; 오탐 23건, 리포트, 그리고 JVM (#7&amp;ndash;#13)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;붙인 첫 판(#7)에 23건이 나왔다. 이 실타래는 사건 셋이 연달아다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;오탐 23건&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 전부 두 패턴(EI_EXPOSE_REP/EI_EXPOSE_REP2), 해부에서 본 스프링 주입 오탐이다. 실제 결함 0. 두 패턴만 필터로 빼고 나머지 규칙을 살리니 0(#8). 도구를 붙이는 첫 판은 &quot;무엇이 오탐인지 근거를 정리하는 판&quot;이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세가 안 보였다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 콘솔엔 &quot;23 bugs&quot;만 찍힌다. 어느 파일 몇 줄인지는 리포트 파일에 있는데, job이 끝나며 컨테이너째 사라졌다. &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;artifacts(when: always)로 리포트를 GitLab에 올린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;힙 2GB&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 해부의 25% 규칙 그대로, JVM이 VM 전체(7.8GB)를 보고 힙을 2GB 가까이 잡았다. 동시에 도는 job 수만큼 곱해지는 값이라&lt;span&gt;&amp;nbsp;&lt;/span&gt;MAVEN_OPTS: -Xmx768m로 고정(#13).&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 실타래의 끝 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0:36&lt;/b&gt;.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;build 셋 &amp;mdash; 의존성을 바꾼 날만 느려지는 문제 (#9)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;build는 평소 빠르다. 도커 레이어 캐시가 있어서다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;COPY pom.xml&lt;span&gt;&amp;nbsp;&lt;/span&gt;내용이 그대로면 그 아래&lt;span&gt;&amp;nbsp;&lt;/span&gt;RUN(의존성 다운로드) 결과를 재사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;함정&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 그 파일이 한 글자만 바뀌어도 아래가 통째로 무효가 되어 처음부터 다시 받는다. 하필&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의존성을 고친 커밋, CI 결과가 제일 필요한 순간에&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그렇게 된다. 실제로 pom을 고치던 판들에서 build:booking이 1:33&amp;ndash;2:27까지 늘었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해소&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; BuildKit 캐시 마운트(RUN --mount=type=cache,target=/root/.m2). 내려받은 것을 레이어 밖 저장소에 두어, 목록이 바뀌어도 바뀐 것만 받는다(#9).&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 실타래의 끝 &amp;mdash; build 셋&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0:11&amp;ndash;0:16&lt;/b&gt;.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;lint:queue-go &amp;mdash; 판마다 전부 재컴파일, 그리고 경합 (#12&amp;ndash;#14)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전부 초록이 된 #12에서도 이 job이 2:34로 제일 길었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Go의 캐시는 둘이다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 모듈 캐시(/go/pkg/mod)는 내려받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;소스&lt;/b&gt;를, 빌드 캐시(/root/.cache/go-build)는 그것을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컴파일한 결과&lt;/b&gt;를 담는다. 소스가 안 바뀐 패키지는 빌드 캐시가 있으면 다시 컴파일하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뭐가 빠졌었나&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 러너 볼륨에 모듈 캐시만 있었다. 소스는 안 받는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컴파일은 판마다 처음부터&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;했다. go vet이 의존 전부를 컴파일하므로 그게 2분을 먹었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해소&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;gobuild-cache&lt;span&gt;&amp;nbsp;&lt;/span&gt;볼륨 추가. #13이 캐시를 채우고(399.7MB), 같은 소스로 한 판 더:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2:34 &amp;rarr; 0:14.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;덤 &amp;mdash; 경합의 발견.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 판에서 lint:booking도 1:37 &amp;rarr; 0:36이 됐다. 설정을 하나도 안 바꾼 job이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;경합(contention)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; CPU&amp;middot;디스크&amp;middot;회선은 한정돼 있고 동시에 도는 job들이 나눠 쓴다. 2분 34초짜리가 CPU를 잡는 동안 옆 슬롯이 밀리고 있었던 것. 병목 하나를 지우면 그늘의 job들이 제 속도를 되찾는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;캐시의 대가 &amp;mdash; 디스크, 그리고 VHDX (#14 결산)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐시 실타래들의 공통 청구서다. #14 시점 캐시 볼륨:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;maven 145MB &amp;middot; gomod 433MB &amp;middot; gobuild 400MB &amp;middot; trivy 8.5GB&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;trivy가 큰 것은 잠금 문제로 폴더를 갈라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB가 네 벌&lt;/b&gt;이 됐기 때문 &amp;mdash; 잠금 충돌과 디스크를 맞바꾼 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VHDX가 뭔가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; Docker Desktop은 리눅스 VM(WSL2) 안에서 돈다. 그 VM의 디스크 전체가 Windows에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;docker_data.vhdx라는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;파일 하나&lt;/b&gt;로 보인다. 이미지 10GB&amp;middot;빌드 캐시 9GB&amp;middot;볼륨 12GB가 전부 그 안이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무슨 일이&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 이 파일이 32GB까지 자라 C 드라이브 여유가 1.0GB까지 내려갔다. VHDX는 안에서 늘어난 만큼 자동으로 커지지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;안에서 지워도 저절로 줄지는 않는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;회수는 별도 작업이다.&lt;/li&gt;
&lt;li&gt;함정 하나 더 &amp;mdash; 캐시 볼륨은 job이 돌 때만 컨테이너에 붙어서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;docker volume prune(안 쓰는 볼륨 일괄 삭제)에는 &quot;안 쓰는 볼륨&quot;으로 보인다. 정리 명령 한 번이 캐시 전부를 지운다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시를 운영한다는 것은 그 수명과 크기를 관리하는 일까지다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;판정 &amp;mdash; 그래서 이게 맞는 CI인가&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;표준 8단계와 대조&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CI 표준 구성(lint &amp;middot; build &amp;middot; test &amp;middot; secret scan &amp;middot; dependency scan &amp;middot; package &amp;middot; image scan &amp;middot; publish)에 겹쳐 보면:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;있다      lint(go vet &amp;middot; SpotBugs) &amp;middot; secret(gitleaks) &amp;middot; dep(trivy fs)
          build &amp;rarr; image scan(trivy image) &amp;rarr; publish(dev 한정)
없다      test        ★ 칸만 선언.  11부에서 앱 동작을 보며 채운다
          SBOM &amp;middot; 이미지 서명 &amp;middot; 커버리지
미설정    러너 Protected &amp;middot; allowed_images / MR &quot;Pipelines must succeed&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;test가 비어 있는 동안은 온전한 CI가 아니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;CI의 정의가 &quot;합칠 때마다 자동 검증&quot;인데 그 핵심이 테스트다. 지금은 &quot;빌드&amp;middot;보안 스캔 자동화&quot;까지다.&lt;/li&gt;
&lt;li&gt;그 한계를 그대로 두는 쪽을 골랐다 &amp;mdash; 껍데기 테스트로 초록불을 만드는 것보다 낫다.&lt;/li&gt;
&lt;li&gt;머지 게이트가 꺼져 있는 것도 남은 구멍이다. 빨간불이어도 머지 버튼이 눌린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SonarQube와 무엇이 다른가&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;겹치는 것은 lint 계열.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; SonarQube &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 이 파이프라인 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내가 쓴 코드&lt;/td&gt;
&lt;td&gt;정적 분석&amp;middot;중복&amp;middot;커버리지. 서버에 쌓여 추이가 남는다&lt;/td&gt;
&lt;td&gt;go vet &amp;middot; SpotBugs (판마다 일회성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가져다 쓴 것 (의존성 CVE)&lt;/td&gt;
&lt;td&gt;범위 밖 (Community 기준)&lt;/td&gt;
&lt;td&gt;dep-scan &amp;middot; scan (trivy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이미지 안 OS 패키지&lt;/td&gt;
&lt;td&gt;범위 밖&lt;/td&gt;
&lt;td&gt;scan (trivy)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 14판에 대입하면 &amp;mdash; SpotBugs 23건만 SonarQube 영역이다. queue-go 2건&amp;middot;booking 37건&amp;middot;frontend 33건은 SonarQube를 깔았어도 그대로 통과했다. trivy 두 job은 어느 쪽이든 필요하다.&lt;/li&gt;
&lt;li&gt;구조에서 배울 것 &amp;mdash; allow_failure로 손으로 만든 순서(&quot;기존 부채는 기준선, 새로 들어오는 것만 막는다&quot;)를 SonarQube는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;New Code&lt;/b&gt;라는 기능으로 제공한다. 기준일 이후 코드만 Quality Gate로 판정하는 방식이다.&lt;/li&gt;
&lt;li&gt;도입하지 않은 근거 둘 &amp;mdash; 서버+DB로 상시 2GB대 메모리를 더 쓴다. 무료판(Community)은 브랜치&amp;middot;MR 분석이 없어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MR 게이트로는 반쪽&lt;/b&gt;이다. 13부 실측 후 재평가.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;채용공고의 문구와 대조&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공고에 반복되는 문구는 넷이다. 각각 무슨 뜻이고, 이 기록의 어느 부분이 답이 되는지.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%; height: 194px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 26px;&quot;&gt;
&lt;td style=&quot;height: 26px;&quot;&gt;&lt;b&gt;공고 문구&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 26px;&quot;&gt;&lt;b&gt;무슨 뜻인가&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 26px;&quot;&gt;&lt;b&gt;이 기록에서&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;CI/CD 파이프라인 구축&amp;middot;운영&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;push부터 배포 직전까지를 자동 단계로 잇고, 깨지면 고치며 유지하는 일&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;14판 전체. 특히 &quot;넣고 &amp;rarr; 깨지고 &amp;rarr; 고치는&quot; 순환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;컨테이너 기반 빌드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;빌드 도구(Go&amp;middot;Maven)를 머신에 안 깔고 컨테이너로 쓰는 것. 어느 머신에서든 같은 결과&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;job마다 이미지 지정 &amp;middot; 소켓 마운트 대 dind 선택 &amp;middot; 멀티스테이지 &amp;middot; BuildKit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;DevSecOps / 보안 스캔&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;보안 검사를 배포 뒤가 아니라 파이프라인 안(개발 단계)으로 당기는 것&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;secret&amp;middot;dep&amp;middot;image 3종 게이트 &amp;middot; 72건 &amp;rarr; 0 &amp;middot; &quot;고칠 수 있는 것만 막는다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;빌드 최적화&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;파이프라인 소요를 줄이는 일. 피드백이 10분을 넘으면 개발자가 결과를 안 기다리고 떠난다&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;10:15 &amp;rarr; 1:54. 두 층 캐시 &amp;middot; 병목 추적(잠금&amp;middot;타임아웃&amp;middot;경합)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;답이 안 되는 것도 그대로 남는다 &amp;mdash; 테스트 자동화(11부 전까지), 대규모&amp;middot;다인원 환경(러너 한 대, 개발자 한 명의 기록이다).&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;부록&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;A &amp;mdash; 커밋 열아홉&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;eed81cd   CI 파이프라인을 붙인다 &amp;mdash; 이미지 빌드와 레지스트리 push
02e0792   레지스트리 로그인을 표준 입력으로 받는다
394d5bc   push 는 dev 에 머지된 뒤에만 한다
71c7383   파이프라인을 5단으로 재편한다 &amp;mdash; check&amp;middot;test&amp;middot;build&amp;middot;scan&amp;middot;publish
a4751ec   queue-go: 취약점이 보고된 간접 의존 모듈을 올린다
957a2ee   booking: Spring Boot 부모 버전을 3.3.4 에서 3.5.14 로 올린다
5a534f3   이미지 빌드에 --pull 을 붙이고 스캔 로그의 진행바를 끈다
ebbfa2d   check 칸에 의존성 스캔과 booking 정적분석을 붙인다
9c4c4fa   booking: Spring Boot 부모를 3.5.16 으로 올린다
2f408f7   frontend: nginx 베이스를 1.31-alpine 으로 올린다
fdf3ed2   booking: netty 를 4.1.136.Final 로 고정한다
9cf1c05   SpotBugs 제외 필터를 두고 리포트를 아티팩트로 남긴다
ac6b801   빌드 안에서 받는 의존성을 BuildKit 캐시 마운트로 옮긴다
1e7f85c   scan job 마다 trivy 캐시 폴더를 갈라 준다
a6c9c49   의존성 스캔 대상을 queue-go 로 좁힌다
07f087a   build&amp;middot;scan&amp;middot;publish 가 앞 stage 아티팩트를 안 받게 한다
c6c508d   이미지 스캔 제한 시간을 20분으로 둔다
ec17637   의존성 스캔과 정적 분석을 막는 게이트로 돌린다
060bee1   job 의 메모리 상한 &amp;middot; 제한 시간 &amp;middot; 자동 취소를 정한다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;git 밖이라 커밋에 없는 변경 &amp;mdash; 러너&lt;span&gt;&amp;nbsp;&lt;/span&gt;config.toml(concurrent = 2&lt;span&gt;&amp;nbsp;&lt;/span&gt;+ 캐시 볼륨 넷),&lt;span&gt;&amp;nbsp;&lt;/span&gt;.wslconfig(메모리 상한 6&amp;rarr;8GB). 러너 설정은 도커 볼륨 안에 있어 저장소에 남지 않는다. 데스크탑을 갈아엎으면 사라지는 부채로, 10부 &amp;sect;9에서 다룬다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;B &amp;mdash; job별 시간 변화 (초록 판 기준)&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;job                 #8       #12      #14
secret-scan         0:10     0:14     0:15
dep-scan(:queue-go) 0:27✗    0:15     0:15
lint:queue-go       1:53     2:17     0:14    &amp;larr; gobuild-cache
lint:booking        1:04     1:15     0:36    &amp;larr; 경합 해소
build:queue-go      0:12     0:19     0:16
build:booking       1:33     0:18     0:15    &amp;larr; BuildKit 캐시
build:frontend      0:14     0:11     0:11
scan:queue-go       0:43     0:14     0:12
scan:booking        3:21     0:36     0:32    &amp;larr; trivy 캐시
scan:frontend       0:33     0:18     0:16
─────────────────────────────────────────────
전체                10:15    3:40     1:54&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;※ 파이프라인 목록 화면이 #11&amp;middot;#14를 Warning으로 표시하는 문제가 남았다. GitLab 내부 데이터(status=success,&lt;span&gt;&amp;nbsp;&lt;/span&gt;has_warnings=false)와 어긋나는 표시다. 상세 화면과 API가 맞다.&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/220</guid>
      <comments>https://zed6740.tistory.com/220#entry220comment</comments>
      <pubDate>Sun, 9 Aug 2026 13:17:13 +0900</pubDate>
    </item>
    <item>
      <title>번외 &amp;mdash; WSL2와 커밋 한도</title>
      <link>https://zed6740.tistory.com/219</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9부에서 GitLab을 데스크탑에 세우고 작업하는 동안 하루에 몇 번씩 가까이 뭔가가 죽었다.&lt;br /&gt;GitLab이 내려가고, Chrome 탭이 비워지고, VS Code가 다시 뜨고, Docker Desktop이 아예 시작을 못 했다.&lt;br /&gt;작업 관리자를 열면 메모리가 80퍼센트를 넘어 있었다. &lt;b&gt;물리 램이 모자란다고 판단하고&lt;br /&gt;DDR3 4GB 네 장 중 두 장을 8GB로 바꿀 주문을 넣었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 원인은 램이 아니었다. 이 글은 그 판단이 왜 틀렸는지, 무엇이 진짜 원인이었는지,&lt;br /&gt;어떻게 찾아서 고쳤는지를 순서대로 적은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 가리려면 &lt;b&gt;무엇이 무엇 위에 있는지부터&lt;/b&gt; 알아야 했다. WSL2&amp;middot;배포판&amp;middot;컨테이너&amp;middot;&lt;code&gt;Vmmem&lt;/code&gt;이&lt;br /&gt;각각 무엇이고 어느 층에 있는지를 갈라 놓고 시작한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 무엇이 안 잡혔나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로 못 하던 것들이 이랬다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Docker Desktop &amp;middot; docker &amp;middot; dockerd &amp;middot; WSL2 &amp;middot; wsl &amp;middot; 배포판 &amp;middot; 컨테이너 &amp;middot; Vmmem&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름은 다 들어 봤는데 &lt;b&gt;무엇이 무엇 위에 있는지&lt;/b&gt;가 안 그려졌다. 그 상태로는 작업 관리자에서 &lt;code&gt;Vmmem&lt;/code&gt;이 1위인 것을 봐도 그게 무엇인지조차 몰랐고, 화면에 &lt;code&gt;OOMKilled&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;그래서 층부터 갈랐다. &lt;b&gt;무엇이 무엇 위에 서 있는지를 모르면 어디를 손댈지도 모른다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개념 &amp;mdash; 무엇이 무엇 위에 있나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출발점은 이것 하나다. &lt;b&gt;이 데스크탑의 Windows에 WSL2를 켜고 Ubuntu 배포판을 깔아서 써 왔다.&lt;/b&gt; &lt;code&gt;Vmmem&lt;/code&gt;이 무엇이고 왜 그렇게 먹는지를 알려면 그 두 단어부터 갈라야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.1 WSL2 &amp;mdash; Windows 안에 띄운 가상머신&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL2는 &lt;b&gt;Windows 안에서 리눅스 커널을 실제로 부팅시키는 가상머신&lt;/b&gt;이다. &quot;실제로&quot;가 핵심이고, 그게 이전 세대와 갈리는 지점이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;WSL1   가상머신이 아니었다. 리눅스 커널이 없었다.
       리눅스 프로그램이 커널에 하는 요청을 Windows 커널이 받아 바꿔 처리했다
       &amp;rarr; 커널 기능 자체가 필요한 도커는 아예 못 돌았다

WSL2   가상머신이다. 리눅스 커널이 그 안에서 부팅한다
       &amp;rarr; cgroup &amp;middot; namespace 가 진짜로 있다  &amp;rarr; 도커가 돈다&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;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl --version&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;WSL 버전: 2.7.11.0
커널 버전: 6.18.33.2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리눅스 커널이 버전까지 찍힌다.&lt;/b&gt; Microsoft가 만들어 배포하는 커널이고, 이것이 안에서 부팅해 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그 가상머신은 무엇 위에 올라가나 &amp;mdash; 하이퍼바이저&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 한 대에 OS는 원래 하나다. CPU도 메모리도 디스크도 그 OS가 전부 자기 것으로 쓴다. &lt;b&gt;OS를 둘 이상 동시에 돌리려면 그것들을 나눠서, 서로의 존재를 모른 채 쓰게 해 줄 층이 하나 필요하다.&lt;/b&gt; 그 층이 &lt;b&gt;하이퍼바이저&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;하는 일     CPU 시간 &amp;middot; 메모리 영역 &amp;middot; 디스크를 게스트마다 잘라서 준다
            한 게스트가 다른 게스트의 메모리를 못 건드리게 막는다

Type 1      하드웨어 바로 위에서 돈다      Hyper-V &amp;middot; KVM(Proxmox) &amp;middot; ESXi
Type 2      OS 위에서 앱으로 돈다        VirtualBox &amp;middot; VMware Workstation&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL2를 켤 때 Windows 기능에서 &lt;b&gt;Virtual Machine Platform&lt;/b&gt;을 켜는데, 그것이 Windows의 하이퍼바이저 &lt;b&gt;Hyper-V&lt;/b&gt;다. Proxmox VE 쪽은 Debian 리눅스 커널에 모듈로 들어 있는 &lt;b&gt;KVM&lt;/b&gt;이 같은 자리에 있다. &lt;b&gt;둘 다 Type 1이고, OS에 이미 딸려 온 것을 켜서 쓴다.&lt;/b&gt; VirtualBox처럼 따로 설치하는 프로그램이 아니다.&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;하이퍼바이저를 켜면 그것을 켠 OS 자신도 하이퍼바이저 아래로 내려앉는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;물리 하드웨어
  └ Hyper-V (하이퍼바이저)
       ├ Windows 자신                          &amp;larr; 게스트가 된다
       └ WSL2 VM  &amp;rarr;  리눅스 커널 6.18.33.2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;켜기 전에는 Windows가 하드웨어를 직접 쓰지만, 켜고 나면 하이퍼바이저가 하드웨어를 잡는다. &lt;b&gt;WSL2 VM은 Windows 밑이 아니라 Windows 옆에 나란히 놓인다.&lt;/b&gt; Proxmox도 같아서, Debian이 KVM을 켜는 순간 그 Debian도 VM 3대와 나란한 자리로 내려간다.&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;Proxmox만 특별한 방식이 아니다.&lt;/b&gt; 노트북에서 하던 것과 데스크탑에서 도는 것이 같은 구조이고, 다른 것은 그 위에 무엇을 올렸느냐뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;구조는 같고 방향이 반대다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Proxmox    서버 한 대를 여러 대로 나눠 쓴다      게스트를 호스트에서 떼어낸다
WSL2       Windows 에서 리눅스를 쓴다           게스트를 호스트에 붙인다&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 메모리를 받는 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Proxmox VM   만들 때 8GB 로 지정하면 켜져 있는 동안 계속 8GB 를 점유한다
WSL2 VM      Windows 의 물리 메모리에서 그때그때 받아 온다
             0 에서 시작해 상한까지 오르내린다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WSL2는 받아 오는 곳이 Windows의 물리 메모리다.&lt;/b&gt; 별도로 떼어 둔 몫이 없으니, VM이 커지는 만큼 Windows 쪽에서 그만큼이 빠진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;물리 16GB
  ├ Windows 앱들이 쓰는 몫
  └ WSL2 VM 이 받아 간 몫        0 에서 상한 사이를 오르내린다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상한이 있고, 그 상한은 Windows 쪽 설정 파일이 정한다.&lt;/b&gt; 어느 파일에 무엇을 적어 뒀는지는 배포판을 다룬 뒤에 연다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;켜 놓은 채로도 쓰는 양이 계속 오르내린다. 안에서 뭔가 돌면 더 받아 오고, 한가해지면 일부를 돌려준다. Windows 쪽에서 그 오르내림이 어떤 숫자로 보이는지는 뒤의 &lt;code&gt;Vmmem&lt;/code&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;② 켜져 있는 시간&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Proxmox VM   켜 두면 계속 떠 있다
WSL2 VM      아무것도 안 켜면 VM 자체가 없다.  명령 하나를 치는 순간 뜬다&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;③ 부팅&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Proxmox VM   BIOS/UEFI &amp;rarr; 부트로더 &amp;rarr; 커널 &amp;rarr; init      수십 초
WSL2 VM      커널 이미지를 바로 올린다. 부트로더가 없다   1초 안쪽&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;④ 호스트와의 경계&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Proxmox VM   완전히 분리. 네트워크로만 닿는다 (SSH)
WSL2 VM      /mnt/c 로 Windows 디스크가 들어와 있다
             localhost 를 Windows 와 공유한다
             리눅스 셸에서 .exe 를 실행할 수 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 작업 방식을 정하는 것이 &lt;code&gt;/mnt/c&lt;/code&gt;다. &lt;b&gt;같은 파일을 두 이름으로 보는 것&lt;/b&gt;이라 복사본이 아니다(마운트의 효과 &amp;mdash; Windows 드라이브를 리눅스 파일시스템 안의 한 자리에 붙여 둔 것).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;Windows   C:\Users\on\Desktop\loadmap\cgv-infra\...
Linux     /mnt/c/Users/on/Desktop/loadmap/cgv-infra/...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code에서 저장하면 리눅스 셸에서 바로 읽힌다. Proxmox VM이었으면 &lt;code&gt;scp&lt;/code&gt;로 옮겨야 했을 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;넷 중 이 글에 걸리는 것&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;넷째, 호스트와의 경계가 없는 것&lt;/b&gt;이 데스크탑에서 WSL2를 고른 이유다. VS Code로 Windows 쪽에서 편집하고 같은 파일을 리눅스에서 빌드한다. 옮기는 절차가 없다.&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;pre class=&quot;&quot;&gt;&lt;code&gt;메모리를 빌려 온다      떼어 받은 메모리라면 Windows 가 모자라도 VM 은 자기 몫을 쥔다
                     빌려 오니 Windows 쪽이 막히면 VM 도 같이 막힌다

필요할 때만 뜬다        없다가 생기고, 생기면 커지고, 커지면 잘 안 줄어든다
                     숫자가 계속 움직여서 무엇이 정상인지 잡히지 않는다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 노드는 서로 떨어져 있어야 하니 Proxmox가 맞고, 데스크탑에서 파일을 옮기지 않고 두 쪽을 오가려면 WSL2가 맞다. &lt;b&gt;호스트에 붙인 대가로 호스트 사정도 같이 넘어온다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;다루는 명령은 &lt;code&gt;wsl&lt;/code&gt; 하나다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;wsl -l -v           뭐가 켜져 있나
wsl -d &amp;lt;이름&amp;gt;        그것으로 들어간다
wsl --terminate     하나를 내린다
wsl --shutdown      VM 을 통째로 내린다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;이름&amp;gt;&lt;/code&gt; 자리에 무엇이 들어가는지가 다음 절이다. &lt;b&gt;커널은 섰지만, 커널만으로는 명령 한 줄 못 친다.&lt;/b&gt;&lt;/p&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;&lt;b&gt;1.2 커널과 배포판&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘이 각각 무엇을 하는지는 &lt;code&gt;ls&lt;/code&gt; 한 줄을 따라가면 갈린다.&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&lt;code&gt;ls&lt;/code&gt; 하나를 칠 때 무슨 일이 나나&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ ls&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;   ① bash 가 &quot;ls&quot; 라는 파일을 찾는다              /usr/bin/ls
   ② bash 가 커널에게 부탁한다                    &quot;이 파일 실행해 줘&quot;
★ ③ 커널이 디스크를 읽어 메모리에 올린다          &amp;rarr;  ls 프로세스가 뜬다
   ④ ls 가 커널에게 부탁한다                      &quot;이 디렉터리에 뭐 있는지 알려 줘&quot;
★ ⑤ 커널이 디스크를 실제로 읽어서 알려 준다
   ⑥ ls 가 화면에 찍는다

★ = 디스크를 실제로 건드리는 단계.  둘 다 커널이 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;커널 &amp;mdash; 하드웨어를 직접 만지는 유일한 것&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;커널이 독점하는 것    CPU 시간 배분 &amp;middot; 메모리 할당 &amp;middot; 디스크 읽고 쓰기 &amp;middot; 네트워크 카드
프로그램이 하는 것     커널에게 부탁한다 (= 시스템콜)&lt;/code&gt;&lt;/pre&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;배포판 &amp;mdash; 커널에게 부탁하는 프로그램들의 묶음&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 ①의 &lt;code&gt;bash&lt;/code&gt;, ②의 &lt;code&gt;ls&lt;/code&gt;, 그리고 &lt;code&gt;/usr/bin&lt;/code&gt;이라는 디렉터리 자체까지 &lt;b&gt;전부 배포판이 넣어 준 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;셸               명령을 입력받을 창구 (bash)
기본 명령어       ls &amp;middot; cp &amp;middot; ps &amp;middot; grep
패키지 관리자      프로그램을 설치하는 도구 (apt &amp;middot; dnf &amp;middot; apk)
디렉터리 배치      /etc &amp;middot; /usr &amp;middot; /var 를 어디에 어떻게 둘지
초기화 시스템      부팅하며 뭘 순서대로 띄울지 (systemd)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;리눅스&quot;는 원래 &quot;커널 이름&quot;&lt;/b&gt;이고, 위를 골라 한 벌로 묶어 놓은 것이 배포판이다. 누가 어떻게 묶었느냐로 이름이 갈린다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Ubuntu &amp;middot; Debian        패키지 apt &amp;middot; .deb
Rocky &amp;middot; RHEL &amp;middot; CentOS  패키지 dnf &amp;middot; .rpm
Alpine                 패키지 apk. 작아서 컨테이너 베이스로 많이 쓴다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;커널만 있으면 부팅이 끝까지 못 간다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스가 켜지는 순서는 이렇다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;① 커널을 메모리에 올린다
② 커널이 하드웨어를 초기화한다            CPU &amp;middot; 메모리 &amp;middot; 디스크 &amp;middot; NIC 인식
③ 커널이 루트 파일시스템(/)을 마운트한다
④ 커널이 /sbin/init 을 실행한다           PID 1
⑤ init 이 나머지를 띄운다                 로그 &amp;middot; 네트워크 &amp;middot; sshd &amp;middot; 셸&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커널이 직접 하는 것은 ①②까지다.&lt;/b&gt; ③의 &lt;code&gt;/&lt;/code&gt;도 ④의 &lt;code&gt;/sbin/init&lt;/code&gt;도 커널 안에 없다 &amp;mdash;&lt;b&gt; 배포판이 준다&lt;/b&gt;. 없으면 이 줄에서 멈춘다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Kernel panic - not syncing: No init found.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령이 안 먹는 정도가 아니라 &lt;b&gt;프롬프트를 띄울 &lt;code&gt;bash&lt;/code&gt; 자체가 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;내가 미리 예전에 깐 것이 Ubuntu 배포판이다&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실물은 Windows에 있는 파일 하나다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Get-ChildItem &quot;$env:LOCALAPPDATA\wsl&quot; -Recurse -Filter &quot;ext4.vhdx&quot; | Select-Object FullName&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\on\AppData\Local\wsl\{a3e25de2-...}\ext4.vhdx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;vhdx&lt;/code&gt;는 가상 디스크 파일이고, &lt;b&gt;그 안에 리눅스 파일시스템 하나가 통째로 들어 있다.&lt;/b&gt; 부팅 ③에서 커널이 &lt;code&gt;/&lt;/code&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;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl -d Ubuntu -- ls /&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;bin  boot  dev  etc  home  lib  media  mnt  opt  proc  root
run  sbin  snap  srv  sys  tmp  usr  var&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;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl -d Ubuntu -- cat /etc/os-release
wsl -d Ubuntu -- uname -r&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;PRETTY_NAME=&quot;Ubuntu 24.04.4 LTS&quot;
6.18.33.2-microsoft-standard-WSL2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포판은 Ubuntu인데 커널은 Microsoft가 만든 것이다.&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;code&gt;/sbin/init&lt;/code&gt;도 그 파일 안에 있다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl -d Ubuntu -- ps aux&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;USER   PID  COMMAND
root     1  /sbin/init          &amp;larr; Ubuntu 의 ext4.vhdx 에 든 파일이 PID 1 이 됐다
root    43  systemd-journald
root   151  systemd-resolved&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;여기까지로 완성된 것&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;WSL2 VM  ─  리눅스 커널 6.18.33.2         Microsoft 가 준다.  고를 여지가 없다
             └ Ubuntu 배포판               내가 깔았다.  ext4.vhdx 파일 하나
                  bash &amp;middot; ls &amp;middot; apt &amp;middot; systemd &amp;middot; /etc &amp;middot; /home&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부팅 ①에서 ⑤까지가 다 돌았다는 증거가 &quot;wsl&quot;명령어로 들어가는&lt;b&gt; 프롬프트&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;PS C:\Users\on\Desktop\loadmap&amp;gt; wsl
on@DESKTOP-BSPGJMO:/mnt/c/Users/on/Desktop/loadmap$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 프롬프트가 Ubuntu 배포판이 준 &lt;code&gt;bash&lt;/code&gt;다.&lt;/b&gt; 커널만 있었으면 이 줄이 안 나온다. 여기서 &lt;code&gt;kubectl&lt;/code&gt;&amp;middot;&lt;code&gt;kubeseal&lt;/code&gt;&amp;middot;&lt;code&gt;git&lt;/code&gt;을 치면 먹는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Windows 안에 리눅스가 섰다.&lt;/b&gt; 여기에 필요한 것을 &lt;code&gt;apt&lt;/code&gt;로 얹으면 되는 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그 VM에 준 자원 &amp;mdash; &lt;code&gt;.wslconfig&lt;/code&gt;&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM이 Windows에서 받아 갈 수 있는 상한은 &lt;b&gt;Windows 쪽 파일 하나&lt;/b&gt;로 정한다. 리눅스 안이 아니라 밖이다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\on\.wslconfig&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[wsl2]
memory=6GB
processors=6
swap=4GB&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memory       VM 이 Windows 물리 메모리에서 받아 갈 수 있는 최대치
processors   VM 에 줄 논리 프로세서 수 (i7-2600K 8스레드 중 6)
swap         VM 안의 스왑.  Windows 페이지파일과는 별개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일이 없으면 기본값이 쓰인다.&lt;/b&gt; &lt;code&gt;memory&lt;/code&gt; 기본값은 물리의 절반이라 이 데스크탑에서는 8GB가 된다. 6GB로 낮춘 것은 Windows 쪽에 자리를 남기려던 것이다.&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;바꾼 값은 VM을 다시 띄워야 먹는다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl --shutdown&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.wslconfig&lt;/code&gt;가 정하는 것은 &lt;b&gt;상한 하나뿐&lt;/b&gt;이다. 실제로 얼마를 받아 가는지는 그때그때 달라진다.&lt;/p&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;&lt;b&gt;1.3 GitLab을 어떻게 세울 것인가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 데스크탑 환경이었다 &amp;mdash; &lt;b&gt;WSL2 가상머신 하나에 Ubuntu 배포판 하나.&lt;/b&gt; 그 상태에서 프로젝트를 이어가려면 &lt;b&gt;GitLab을 이 데스크탑에 세워야 했다.&lt;/b&gt;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;GitLab이 요구하는 환경&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;OS        리눅스만.  Windows 에서는 어떤 방식으로도 네이티브로 안 돈다
CPU       4코어 권장 (최소 2)
RAM       4GB 필수 최소, 8GB 권장.  swap 은 별도로 더 필요
디스크      설치에 2.5GB + 저장소 데이터&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데스크탑과 대조하면 걸리는 줄이 하나다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;요구            이 데스크탑
──────────────────────────────────────────────
리눅스           WSL2 안에 커널이 있다                    ✅
4코어            .wslconfig  processors=6                ✅
디스크           C 드라이브 여유 49.7GB                    ✅
RAM 4GB         VM 상한이 6GB.  Ubuntu 배포판과 나눠 쓴다   &amp;larr; 여기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitLab 최소가 4GB인데 VM 전체가 6GB다.&lt;/b&gt; 그 6GB를 Ubuntu 쪽 작업(&lt;code&gt;kubectl&lt;/code&gt;&amp;middot;빌드)과 나눠 쓰면 남는 것이 2GB 남짓이다. 그래서 GitLab 쪽 설정을 눌러 공식 최소 아래로 돌리고 있고, &lt;b&gt;무엇을 눌렀는지는 방식을 고른 뒤 기동 파일에서 같이 본다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;세울 수 있는 방법 셋&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;① Ubuntu 배포판에 직접 설치            apt install gitlab-ce.  도커 없이
② Docker Desktop 을 깔고 컨테이너로
③ Ubuntu 배포판에 도커 엔진만 깔고 컨테이너로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 클러스터에 Helm으로 올리는 길과 노트북에 VM을 하나 더 두는 길도 있지만, 노드 RAM 8GB에 LGTM 스택이 이미 돌고 노트북 RAM도 노드 3대에 배분돼 있어 자리가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;① Ubuntu 배포판에 직접 설치&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;sudo apt install gitlab-ce&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;WSL2 VM
  └ 리눅스 커널
      └ Ubuntu 배포판
          └ gitlab-ce 패키지     PostgreSQL &amp;middot; Redis &amp;middot; nginx 가 같이 깔린다&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;얻는 것    셋 중 제일 가볍다.  GitLab 말고 얹히는 것이 없다

치르는 것   GitLab 이 그 Ubuntu 를 점유한다.  앞으로 무엇을 깔든 한 공간을 쓴다
          apt purge 해도 설정과 데이터가 남는다
          버전을 올렸다 문제가 생기면 배포판째 되돌려야 한다
          &amp;rarr; 미리 wsl --export 로 통째 백업을 해 뒀어야 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab은 서비스로 돌아야 해서 &lt;code&gt;systemd&lt;/code&gt;가 필요하다. WSL2는 지원하지만 기본이 꺼져 있어 켜야 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# /etc/wsl.conf
[boot]
systemd=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;② Docker Desktop을 깔고 컨테이너로&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Windows
  ├ Docker Desktop.exe            앱.  GUI &amp;middot; 자동 업데이트 &amp;middot; 백엔드 관리
  └ WSL2 VM
      ├ Ubuntu 배포판
      └ docker-desktop 배포판       앱이 자기 몫으로 깐 것
           └ dockerd
                └ gitlab 컨테이너&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop은 Windows 앱이고, 깔면 &lt;b&gt;배포판과 엔진이 따라온다.&lt;/b&gt; GitLab은 그 엔진이 만든 컨테이너 안에서 돈다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;얻는 것    GUI 로 컨테이너 &amp;middot; 이미지 &amp;middot; 볼륨을 본다
          엔진 설치와 업데이트를 앱이 한다
          WSL 통합 설정을 앱이 해 준다

치르는 것   배포판이 하나 더 생긴다 (docker-desktop)
          Windows 쪽에 앱 프로세스가 뜬다 &amp;mdash; 작업 관리자에서 약 200MB
          로그인하면 자동으로 뜬다 (설정으로 끌 수 있다)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;③ Ubuntu 배포판에 도커 엔진만 깔기&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo apt install docker-ce
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;WSL2 VM
  └ 리눅스 커널
      └ Ubuntu 배포판
          └ dockerd (엔진)
               └ gitlab 컨테이너     그 안에 리눅스 한 벌 + GitLab&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨테이너로 돌리는 것은 ②와 똑같다.&lt;/b&gt; 다른 것은 엔진을 앱이 관리하느냐 내가 &lt;code&gt;apt&lt;/code&gt;로 깔고 관리하느냐뿐이고, 그 차이로 배포판 하나와 Windows 앱이 빠진다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;얻는 것    배포판은 그대로 1개인데 GitLab 이 Ubuntu 를 점유하지 않는다
          PostgreSQL &amp;middot; Redis &amp;middot; nginx 가 전부 컨테이너 안에서 끝난다
          이미지 태그가 버전을 고정한다.  문제가 생기면 태그만 바꿔 다시 띄운다
          지울 때 컨테이너와 볼륨만 지우면 Ubuntu 는 그대로다

치르는 것   dockerd 가 뜬다.  ① 보다 그만큼 더 쓴다
          엔진 설치 &amp;middot; 업데이트 &amp;middot; WSL 설정을 직접 한다
          GUI 가 없다.  상태는 docker ps &amp;middot; docker stats 로 본다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;셋을 나란히&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                    ① 직접 설치         ② Docker Desktop     ③ 엔진 직접
────────────────────────────────────────────────────────────────────────────
배포판                1개                2개                  1개
추가로 뜨는 것          없음               dockerd + Windows 앱   dockerd
GitLab 이 사는 곳      Ubuntu 안          컨테이너 안           컨테이너 안
버전 되돌리기           배포판째 백업/복원    태그를 바꾼다          태그를 바꾼다
지운 뒤               잔재가 남는다        Ubuntu 그대로         Ubuntu 그대로
관리                  손수               앱이 해 준다           손수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;② &lt;b&gt;&lt;span&gt;&amp;nbsp;&quot;&lt;/span&gt;Docker Desktop을 깔고 컨테이너로&quot;&lt;/b&gt; 방식을 골랐다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메모리만 놓고 보면 셋의 차이는 수백 MB다.&lt;/b&gt; GitLab 본체가 2.87GB이므로 어느 쪽을 골라도 그것이 제일 큰 몫이고, 뒤에 겪은 크래시도 이 선택과는 무관했다.&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; 배포판 하나와 Windows 앱 200MB를 안 쓰고 끝난다. 그것을 모르고 ②를 골랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;기동 파일 &amp;mdash; 4GB 최소를 어떻게 눌렀나&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 어떤 설정으로 띄울지는 &lt;code&gt;docker-compose.yml&lt;/code&gt; 한 장에 적는다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;services:
  gitlab:
    image: gitlab/gitlab-ce:19.2.1-ce.0
    container_name: gitlab
    restart: unless-stopped
    shm_size: '256m'
    ports: ['8929:8929', '2424:22']
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://192.168.0.167:8929'
        puma['worker_processes'] = 0
        sidekiq['max_concurrency'] = 10
        prometheus_monitoring['enable'] = false
        gitlab_kas['enable'] = false&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;공식 최소 4GB를 2GB대로 끌어내린 설정&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;puma worker_processes = 0        웹 요청을 받는 워커 프로세스를 안 띄운다
                                 마스터 하나가 직접 처리한다.  혼자 쓰니 동시 요청이 없다

sidekiq max_concurrency = 10     백그라운드 작업 스레드 수.  기본 20 의 절반

prometheus_monitoring = false    GitLab 이 자체 내장한 감시 스택을 끈다
                                 exporter 프로세스가 여럿 안 뜬다

gitlab_kas = false               쿠버네티스 에이전트 서버.  안 쓰는 기능이라 끈다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;넷 다 &quot;프로세스 수를 줄이는&quot; 설정&lt;/b&gt;이다. GitLab이 무거운 이유가 기능 하나가 커서가 아니라 &lt;b&gt;한 컨테이너 안에서 여럿이 같이 뜨기 때문&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;code&gt;restart: unless-stopped&lt;/code&gt;도 뒤에 걸린다. &lt;b&gt;엔진이 뜨면 GitLab이 따라 뜬다&lt;/b&gt;는 뜻이라, 내가 켜지 않아도 로그인만 하면 올라와 있다.&lt;/p&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;&lt;b&gt;1.4 그렇게 세우니 무엇이 생겼나&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker compose up&lt;/code&gt;을 친 것이 전부였다. 실제로 무엇이 생겼는지는 명령 두 줄로 확인된다.&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;배포판이 하나 더 생겼다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;wsl -l -v&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt; * Ubuntu           Running
   docker-desktop   Stopped&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker-desktop&lt;/code&gt;은 &lt;b&gt;내가 깐 적이 없는데 목록에 있다.&lt;/b&gt; Docker Desktop이 엔진을 둘 자리로 만든 것이다.&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;code&gt;State&lt;/code&gt; 칸의 뜻은 이렇다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Running    그 배포판의 ext4.vhdx 안에 든 프로그램들이 프로세스로 돌고 있다
Stopped    파일은 그대로 있고, 돌고 있는 것이 하나도 없다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Stopped&lt;/code&gt;는 없어진 것이 아니다.&lt;/b&gt; 파일은 그대로 있고 아무것도 안 돌 뿐이라, 명령 한 줄이면 다시 &lt;code&gt;Running&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 Ubuntu에 얹지 않고 따로 깐 이유는 &lt;b&gt;앱이 업데이트할 때 그 배포판을 통째로 갈아엎기 때문&lt;/b&gt;이다. 내 Ubuntu에 얹었으면 Docker를 갱신할 때마다 거기 깔아 둔 것이 날아간다.&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;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Docker Desktop     Windows 앱.  아이콘으로 켜는 것
docker-desktop     배포판.  wsl -l -v 에 나오는 것.  엔진(dockerd)이 여기 산다
docker             명령어.  Windows 에 있다.  엔진에 지시만 보낸다&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;GitLab은 그 위에(docker-desktop 위에) 얹었다.&lt;/p&gt;
&lt;pre class=&quot;tex&quot;&gt;&lt;code&gt;docker-compose.yml     C:\Users\on\Desktop\loadmap\gitlab\
docker compose up -d   PowerShell 에서 친다

  &amp;rarr; docker (Windows) 가 dockerd (VM 안) 에 지시를 보낸다
     &amp;rarr; dockerd 가 gitlab/gitlab-ce 이미지로 컨테이너를 띄운다
        &amp;rarr; 그 안에서 프로세스 아홉이 돈다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker&lt;/code&gt; 명령이 Windows에 있어서 PowerShell에서 친다. Ubuntu 배포판 쪽에서는 안 먹는다 &amp;mdash; Docker Desktop의 WSL 통합을 그 배포판에 대해 안 켰기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그런데 가상머신은 여전히 하나다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;가상머신     1개      Get-Process vmmem 을 쳐도 vmmem 하나만 나온다
커널        1개      두 배포판 다 6.18.33.2-microsoft-standard-WSL2
배포판       2개      Ubuntu &amp;middot; docker-desktop
컨테이너     1개      gitlab&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;배포판이 늘어도 VM은 안 는다.&lt;/b&gt; GitLab 컨테이너도 마찬가지다 &amp;mdash; 자기 커널이 없어서 VM의 커널을 빌려 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결과가 하나 나온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;.wslconfig 의 memory=6GB 는 배포판마다 6GB 가 아니라, 둘이 합쳐 6GB 다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 쪽 작업과 GitLab이 각자 몫을 갖는 것이 아니라 &lt;b&gt;한 통을 나눠 쓴다.&lt;/b&gt; 한쪽이 커지면 다른 쪽이 밀린다.&lt;/p&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;&lt;b&gt;1.5 Vmmem &amp;mdash; VM 전체를 Windows가 보는 이름&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 관리자 목록에 &lt;code&gt;gitlab&lt;/code&gt;이라는 줄은 없다. &lt;b&gt;VM 안에 있던 것 전부가 &lt;code&gt;Vmmem&lt;/code&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;724&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsTvPH/dJMcadbN3xV/65qO1p3mTvW0U9HhWycKxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsTvPH/dJMcadbN3xV/65qO1p3mTvW0U9HhWycKxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsTvPH/dJMcadbN3xV/65qO1p3mTvW0U9HhWycKxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsTvPH%2FdJMcadbN3xV%2F65qO1p3mTvW0U9HhWycKxK%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;724&quot; height=&quot;404&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows가 프로그램을 실행하면 그 메모리는 그 프로세스 앞으로 달린다. 그런데 WSL2 VM은 Windows가 실행하는 프로그램이 아니라 하이퍼바이저가 돌리는 것이라, 어떤 Windows 프로세스에도 안 속한다. 그대로 두면 작업 관리자에서 &lt;b&gt;&quot;메모리가 80퍼센트인데 목록을 다 더해도 그만큼이 안 나오는&quot;&lt;/b&gt; 상태가 된다. 그 구멍을 표시하려고 Windows가 만든 자리가 &lt;b&gt;&lt;code&gt;Vmmem&lt;/code&gt;&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;① 프로세스가 아니다     작업 끝내기로 못 끈다.  내리려면 wsl --shutdown
② 안이 안 보인다        배포판 둘과 GitLab 컨테이너가 한 숫자에 뭉쳐 있다
③ CPU 도 대표한다      Vmmem 의 CPU % 는 VM 안 리눅스가 쓰는 양이다
④ 물리 RAM 기준이다     VM 이 요구한 양이 아니라 지금 물리 RAM 에 올라와 있는 양이다&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;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;PowerShell 에서 wsl 을 쳐서 Ubuntu 배포판을 켠다    &amp;rarr;  Vmmem 이 오른다
Docker Desktop 에서 GitLab 컨테이너를 켠다         &amp;rarr;  같은 Vmmem 이 또 오른다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포판을 켜든 컨테이너를 켜든 늘어나는 것은 &lt;code&gt;Vmmem&lt;/code&gt; 한 줄이다.&lt;/b&gt; 목록에 새 줄이 생기지 않는다.&lt;/p&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;&lt;b&gt;1.6 전체 그림&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Windows 10  (물리 16GB, i7-2600K 4코어 8스레드)
  │
  ├─ Docker Desktop.exe       앱. VM 과 엔진을 켜고 끈다
  ├─ docker.exe               명령어. 엔진에 지시를 보낸다
  ├─ wsl.exe                  명령어. 배포판을 켜고 셸을 연다
  │
  ├─ Chrome &amp;middot; VS Code &amp;middot; Claude Code &amp;middot; Defender
  │
  └─ WSL2 가상머신                        &amp;larr; 작업 관리자에 Vmmem 으로 보인다
       │  리눅스 커널 6.18.33.2-microsoft-standard-WSL2
       │
       ├─ Ubuntu 배포판                   kubectl &amp;middot; kubeseal &amp;middot; kubeconfig
       │
       └─ docker-desktop 배포판
            └─ dockerd
                 └─ gitlab 컨테이너
                      nginx &amp;middot; workhorse &amp;middot; puma &amp;middot; sidekiq &amp;middot; postgresql
                      redis &amp;middot; gitaly &amp;middot; sshd &amp;middot; logrotate       프로세스 아홉&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;GitLab은 프로세스인가 컨테이너인가&quot;의 답이 여기 있다 &amp;mdash; 둘 다다.&lt;/b&gt; 컨테이너 하나 안에 프로세스가 아홉이다.&lt;/p&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;&lt;b&gt;1.7 켤 때마다 무엇이 오르나&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;wsl 을 친다                      Docker Desktop 아이콘을 클릭한다
──────────────────────────────────────────────────────────────
① VM 을 띄운다                   ① VM 을 띄운다            &amp;larr; 같은 VM 하나
② Ubuntu 배포판을 띄운다          ② docker-desktop 배포판을 띄운다
③ 셸을 하나 연다                  ③ 그 안에서 dockerd 를 띄운다
                                ④ dockerd 가 gitlab 컨테이너를 띄운다
                                ⑤ 컨테이너 안에서 프로세스 아홉이 뜬다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘 다 같은 가상머신을 쓰고, 각자 자기 배포판을 켠다.&lt;/b&gt; 그래서 &lt;code&gt;Vmmem&lt;/code&gt;이 두 개로 갈라지지 않고 한 숫자가 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;무엇으로 재나 &amp;mdash; 명령 넷&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;wsl -l -v                            배포판이 켜져 있나
wsl -d docker-desktop -- free -h     VM 전체 장부
docker stats --no-stream             컨테이너별
Get-Process vmmem                    Windows 가 본 VM 의 몫&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;free -h&lt;/code&gt;에 &lt;code&gt;-d docker-desktop&lt;/code&gt;을 쓴 이유가 하나 있다. 이 값은 &lt;b&gt;커널이 보고하는 것&lt;/b&gt;이고 커널은 VM에 하나뿐이라, &lt;b&gt;&lt;code&gt;Ubuntu&lt;/code&gt;에서 묻든 &lt;code&gt;docker-desktop&lt;/code&gt;에서 묻든 같은 숫자가 나온다&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;code&gt;-d Ubuntu&lt;/code&gt;로 물으면 &lt;b&gt;꺼져 있던 Ubuntu가 그 명령 때문에 켜진다.&lt;/b&gt; 재려고 친 명령이 재는 대상을 바꿔 버리므로, &lt;b&gt;이미 떠 있는 쪽(데스크탑과 함께 자동 부팅되는 docker-desktop)&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;code&gt;free -h&lt;/code&gt;는 두 줄을 준다. &lt;b&gt;&lt;code&gt;Mem&lt;/code&gt;과 &lt;code&gt;Swap&lt;/code&gt;이고, 칸 이름은 두 줄이 같이 쓴다.&lt;/b&gt; 각 줄이 무엇인지는 아래에서 하나씩 본다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;               total   used   free   shared  buff/cache  available
Mem:           5.8G    2.9G   483M   97.8M    2.4G        2.6G
Swap:          4.0G    0      4.0G&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;code&gt;Mem&lt;/code&gt; 줄 &amp;mdash; VM 의 물리 메모리&lt;/b&gt;&lt;/span&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;total&lt;/code&gt; 5.8G &amp;mdash; 상한이 정한다&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;.wslconfig&lt;/code&gt;의 &lt;code&gt;memory=6GB&lt;/code&gt;가 출발점이다&lt;/li&gt;
&lt;li&gt;VM이 부팅하면서 리눅스 커널이 자기 코드와 자료구조 몫을 먼저 떼어 간다&lt;/li&gt;
&lt;li&gt;남은 것이 &lt;code&gt;total&lt;/code&gt;이라 6GB를 줬는데 5.8G로 보인다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VM 안에서 무슨 짓을 해도 이 위로는 못 간다.&lt;/b&gt; &lt;code&gt;memory&lt;/code&gt;를 8GB로 올리면 &lt;code&gt;total&lt;/code&gt;도 같이 커진다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;used&lt;/code&gt; 2.9G &amp;mdash; 프로세스가 쥐고 있는 양&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;docker stats&lt;/code&gt;가 보여준 &lt;code&gt;gitlab 2.906GiB&lt;/code&gt;와 거의 같다&lt;/li&gt;
&lt;li&gt;&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;&lt;code&gt;free&lt;/code&gt; 483M &amp;mdash; 아무도 안 쓰는 순수 빈 공간&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;이게 정상이다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;리눅스는 빈 메모리를 놀리지 않는다. 놀려도 얻는 게 없으니 읽은 파일로 채워 둔다&lt;/li&gt;
&lt;li&gt;그래서 &lt;b&gt;오래 쓸수록 &lt;code&gt;free&lt;/code&gt;는 0에 가까워진다.&lt;/b&gt; 부족을 뜻하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;shared&lt;/code&gt; 97.8M &amp;mdash; 프로세스끼리 공유하는 몫&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;tmpfs&lt;/code&gt;나 &lt;code&gt;/dev/shm&lt;/code&gt; 같은 공유 메모리다&lt;/li&gt;
&lt;li&gt;GitLab 기동 파일의 &lt;code&gt;shm_size: '256m'&lt;/code&gt;가 여기 반영된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;used&lt;/code&gt;에 이미 포함된 값&lt;/b&gt;이라 따로 더하면 안 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;buff/cache&lt;/code&gt; 2.4G &amp;mdash; 쓰고 있지만 언제든 내놓는 것&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;같은 파일을 또 읽으면 디스크에 안 가고 여기서 준다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요구가 오면 버려서 내준다.&lt;/b&gt; &lt;code&gt;used&lt;/code&gt;와 성격이 다르다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;used&lt;/code&gt; &amp;mdash; 프로세스가 요구한 것. 못 뺏는다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;buff/cache&lt;/code&gt; &amp;mdash; 커널이 알아서 채운 것. 요구가 오면 내놓는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전부 내놓지는 못한다. 그 예외는 &lt;code&gt;available&lt;/code&gt; 항목에서 다룬다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;available&lt;/code&gt; 2.6G &amp;mdash; 이 칸을 본다&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;&lt;code&gt;available&lt;/code&gt; = &lt;code&gt;free&lt;/code&gt; + &lt;b&gt;&lt;code&gt;buff/cache&lt;/code&gt; 중 버릴 수 있는 부분&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;단순히 더한 값보다 작다
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;free 483M  +  buff/cache 2.4G  =  2.88G
available                       =  2.6G
차이                             약 280M   &amp;larr; 못 버리는 몫&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;buff/cache&lt;/code&gt; 중 못 버리는 것&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;shared&lt;/code&gt; 97.8M &amp;mdash; &lt;code&gt;tmpfs&lt;/code&gt;&amp;middot;&lt;code&gt;/dev/shm&lt;/code&gt;. 디스크에 원본이 없어서 버리면 내용이 사라진다&lt;/li&gt;
&lt;li&gt;dirty 페이지 &amp;mdash; 고쳤는데 아직 디스크에 안 쓴 것. 먼저 써야 버릴 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;나머지는 &lt;b&gt;디스크에서 읽어 온 사본&lt;/b&gt;이라 버려도 원본이 남는다. 그래서 대부분 회수된다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;free&lt;/code&gt;가 483M이라고 겁먹을 게 아니라 &lt;b&gt;&lt;code&gt;available&lt;/code&gt; 2.6G가 실제 여유다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;반대로 &lt;b&gt;이 칸이 0에 가까워지면&lt;/b&gt; 캐시를 다 버려도 모자란 상태다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 칸이 &lt;code&gt;total&lt;/code&gt;을 나눠 갖는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;total  =  used  +  free  +  buff/cache
5.8G   =  2.9G  +  483M  +  2.4G&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;code&gt;Swap&lt;/code&gt; 줄 &amp;mdash; 모자랄 때 디스크로 밀어내는 자리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스왑이 무엇인지부터 짚는다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;RAM 이 모자라면
   지금 당장 안 쓰는 내용을 디스크에 내보내고 그 자리를 비운다
   나중에 그 내용이 필요해지면 디스크에서 다시 읽어 온다
   RAM 보다 훨씬 느리지만, 메모리가 모자라 죽는 것은 막는다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;메모리를 늘리는 것&quot;이 아니라 &quot;느려지더라도 버티게 하는 것&quot;&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;&lt;code&gt;total&lt;/code&gt; 4.0G &amp;mdash; &lt;code&gt;.wslconfig&lt;/code&gt;의 &lt;code&gt;swap=4GB&lt;/code&gt;가 만든 것&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;VM 안의 스왑&lt;/b&gt;이고, &lt;code&gt;Mem&lt;/code&gt;의 5.8G와는 &lt;b&gt;별개로 더 있는 자리&lt;/b&gt;다&lt;/li&gt;
&lt;li&gt;상한 &lt;code&gt;memory=6GB&lt;/code&gt;에 포함되는 값이 아니다&lt;/li&gt;
&lt;li&gt;Windows에도 같은 역할을 하는 것이 있고, 그것의 이름이 &lt;b&gt;페이지파일&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;Swap&lt;/code&gt; &amp;mdash; VM 안. 게스트 리눅스가 자기 몫 5.8G가 모자랄 때 쓴다&lt;/li&gt;
&lt;li&gt;페이지파일 &amp;mdash; VM 밖. Windows가 물리 RAM 16GB가 모자랄 때 쓴다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;층이 달라서 서로 무관하게 동작한다.&lt;/b&gt; 하나가 가득 차도 다른 하나는 모른다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;used&lt;/code&gt; 0 &amp;mdash; 아직 안 쓰고 있다&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;Mem&lt;/code&gt;의 &lt;code&gt;available&lt;/code&gt;이 0에 가까워져야 여기로 밀어내기 시작한다&lt;/li&gt;
&lt;li&gt;밀어내기 시작하면 그만큼 &lt;b&gt;더 버틴다&lt;/b&gt;. 그 대신 디스크를 오가느라 느려진다&lt;/li&gt;
&lt;li&gt;이 값이 오르고 있다면 &lt;b&gt;VM 메모리가 빠듯하다는 신호&lt;/b&gt;다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;free&lt;/code&gt; 4.0G &amp;mdash; 남은 스왑&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;출발점 &amp;mdash; GitLab만 떠 있는 상태&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 켠 것이 없어도 로그인만 하면 여기까지 온다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;Docker Desktop 설정 AutoStart=true            로그인하면 앱이 뜬다 (창은 안 열린다)
docker-compose.yml  restart: unless-stopped   엔진이 뜨면 GitLab 이 따라 뜬다&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;code&gt;Ubuntu&lt;/code&gt;는 안 켰다. 그 상태에서 재면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;wsl -l -v         Ubuntu Stopped  /  docker-desktop Running
docker stats      gitlab  2.906 GiB / 5.787 GiB
free -h           total 5.8G  used 2.9G  free 483M  cache 2.4G  available 2.6G
Vmmem             2,508 MB
Windows           물리 여유 5.5GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;used 2.9G&lt;/code&gt;와 &lt;code&gt;gitlab 2.906GiB&lt;/code&gt;가 거의 같다. &lt;b&gt;VM 안에서 쓰는 것의 거의 전부가 GitLab&lt;/b&gt;이고, 엔진과 그 배포판은 수십 MB다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;여기서 Ubuntu를 켜면&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;wsl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3초 간격으로 3분간 재면 이렇게 움직인다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;07:56:41   Vmmem 3,138 MB
07:56:52   Vmmem 3,740 MB
07:57:00   Vmmem 3,781 MB     &amp;larr; 20초 만에 여기까지
07:59:59   Vmmem 3,819 MB     &amp;larr; 그 뒤 3분간 38MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부팅은 20초 안에 끝나고 그 뒤로는 거의 안 는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;전후를 나란히&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                    Ubuntu Stopped     Ubuntu Running     차이
────────────────────────────────────────────────────────────────
Vmmem               2,508 MB           3,822 MB          +1,314 MB
게스트 used          2.9 G              2.9 G             변화 없음
게스트 buff/cache     2.4 G              2.8 G             +0.4 G
게스트 available      2.6 G              2.6 G             변화 없음
Windows 물리 여유     5.5 GB             4.1 GB            -1.4 GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;게스트 쪽 세 줄 &amp;mdash; VM 안에서 무슨 일이 있었나&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;used         2.9 G &amp;rarr; 2.9 G      Ubuntu 프로세스 약 120MB 는 반올림에 묻혔다
                                init &amp;middot; journald &amp;middot; resolved &amp;middot; systemd --user
buff/cache   2.4 G &amp;rarr; 2.8 G      부팅하며 ext4.vhdx 에서 파일을 읽었다
available    2.6 G &amp;rarr; 2.6 G      늘어난 게 캐시라 여유는 그대로다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;VM 안에서 실제로 일어난 일은 &quot;캐시가 0.4G 늘었다&quot;가 거의 전부다.&lt;/b&gt; Ubuntu 배포판 자체가 무거운 것을 띄우지 않아서 &lt;code&gt;used&lt;/code&gt;는 눈에 띄게 안 늘었고, &lt;code&gt;available&lt;/code&gt;도 그대로다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그런데 &lt;code&gt;Vmmem&lt;/code&gt;은 1.3GB 올랐다&lt;/b&gt;&lt;/span&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게스트는 0.4G만 늘었는데 &lt;code&gt;Vmmem&lt;/code&gt;은 그 세 배가 넘게 올랐다. &lt;b&gt;두 숫자가 재는 대상이 다르기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;게스트 회계    프로세스가 요구해서 받아 둔 양          used + cache  =  5.3G &amp;rarr; 5.7G
Vmmem        그중 지금 물리 RAM 에 올라와 있는 양                    2.5GB &amp;rarr; 3.8GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;게스트가 5.7G를 쥐고 있다고 계산해도 물리 RAM에는 3.8GB만 있다.&lt;/b&gt; 나머지는 &lt;b&gt;물리 RAM 밖&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;b&gt;vmmem에 대한 배정은 고정이 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;VM 이 한가하면     Windows 가 물리 RAM 배정을 줄인다     Ubuntu 켜기 전  2.5 GB
VM 이 활발해지면    배정을 늘린다                      Ubuntu 켠 뒤   3.8 GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ubuntu 부팅은 파일을 대량으로 읽는 작업&lt;/b&gt;이다. 한가하던 VM이 갑자기 활발해지자 Windows가 vmmem에 배정을 늘렸다.&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;늘어난 1.3GB의 내역은 위 네 숫자에서 그대로 나온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;켜기 전    게스트 보유 5.3 G  &amp;minus;  물리 배정 2.5 GB  =  물리 밖 2.8 G
켠 뒤      게스트 보유 5.7 G  &amp;minus;  물리 배정 3.8 GB  =  물리 밖 1.9 G
                                                  물리 밖이 0.9G 줄었다

Vmmem 증가  1.3 GB  =  새로 요구한 0.4 G  +  밖에서 되돌아온 0.9 G&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증가분의 절반 이상이 새로 쓴 것이 아니다.&lt;/b&gt; 원래 물리 밖에 있던 몫이 다시 올라온 것이고, 그만큼 Windows 쪽 여유가 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그래서 표를 이렇게 읽는다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;게스트 세 줄    VM 안에서 무엇이 얼마나 쓰는지
Vmmem 줄      Windows 가 그 VM 에 물리 RAM 을 얼마나 내주고 있는지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 줄이 같은 것을 재지 않으므로 증가분도 같지 않다.&lt;/b&gt; 이 표에서 읽을 것은 &lt;b&gt;&quot;Ubuntu를 켜니 Windows 쪽에서 1.4GB가 빠졌다&quot;&lt;/b&gt; 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Vmmem&lt;/code&gt; 밖에 나가 있던 그 몫이 어디에 있었는지는 뒤에서 다시 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;여기까지가 아무것도 안 하고 있을 때의 값이다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;GitLab 이 떠 있고 Ubuntu 를 켰다.  그게 전부다
게스트 available   2.6 G
Windows 물리 여유   4.1 GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 작업은 여기에 &lt;b&gt;&lt;code&gt;kubectl&lt;/code&gt;&amp;middot;&lt;code&gt;git&lt;/code&gt;&amp;middot;빌드가 올라가고, GitLab이 clone&amp;middot;push를 처리하고, Chrome과 VS Code가 얹힌 상태&lt;/b&gt;다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 증상 &amp;mdash; 작업하다 하나씩 죽었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 하려면 넷을 켠다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;GitLab           Docker Desktop 이 띄운 컨테이너.  저장소와 MR 이 여기 있다
Ubuntu 배포판     kubectl &amp;middot; kubeseal.  노트북 k3s 클러스터를 만진다
Chrome           GitLab &amp;middot; Proxmox &amp;middot; Grafana &amp;middot; ArgoCD 화면을 띄운다
VS Code          Claude Code 와 문서 작업&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;&lt;b&gt;2.1 작업 중에 하나씩 멈추거나 다시 떴다&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Chrome 탭이 비워진다              다시 열면 처음부터 불러온다
VS Code 가 통째로 다시 뜬다        다시 뜨는 화면에 OOMKilled 가 찍힌다
kubectl 이 갑자기 안 먹는다        wsl -l -v 를 보면 Ubuntu 가 Stopped 로 바뀌어 있다
GitLab 화면이 안 열린다            docker ps 에 gitlab 이 없다
Docker Desktop 이 아예 시작을 못 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;규칙이 없었다.&lt;/b&gt; 어떤 날은 Chrome만 날아가고, 어떤 날은 &lt;code&gt;Ubuntu&lt;/code&gt;가 통째로 &lt;code&gt;Stopped&lt;/code&gt;가 되고, 어떤 날은 Docker Desktop이 시작조차 못 했다.&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;b&gt;2.2 작업 관리자를 열어 보니 메모리가 차 있었다&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;메모리        80&amp;ndash;90 퍼센트
상위 셋       Vmmem &amp;middot; Chrome &amp;middot; VS Code&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작업을 이어 갈수록 셋이 계속 올랐다.&lt;/b&gt; Chrome은 탭을 열수록, VS Code는 창과 확장이 늘수록, &lt;code&gt;Vmmem&lt;/code&gt;은 GitLab이 일할수록 커졌다.&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;b&gt;2.3 메모리가 꽉 차서 하나씩 죽는 것으로 봤다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증상과 숫자와 에러 문구가 같은 방향을 가리켰다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;화면에 OOMKilled 가 찍혔다        Out Of Memory &amp;mdash; 글자 그대로 메모리 부족이다
메모리가 80퍼센트를 넘어 있었다
제일 많이 먹는 셋이 계속 올랐다&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.4 그래서 램을 늘리기로 했다&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재      DDR3 4GB &amp;times; 4 = 16GB      슬롯 넷이 전부 차 있다
교체 후    8GB &amp;times; 2 + 4GB &amp;times; 2 = 24GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬롯이 비어 있지 않아 &lt;b&gt;추가가 아니라 교체&lt;/b&gt;다. 4GB 두 장을 빼고 8GB 두 장을 넣는 것으로 주문을 넣었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;보드    Gigabyte P67A-UD3R-B3   슬롯 4개 &amp;middot; 최대 32GB &amp;middot; DDR3
CPU     i7-2600K               4코어 8스레드
BIOS    F9 (2012-03)            8GB 모듈을 못 잡던 초기 리비전이 아니다
모듈    DDR3 8GB PC3-12800 240핀 Unbuffered DIMM&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;&lt;b&gt;2.5 램이 오기 전에 로그를 한 번 더 봤다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문은 넣었지만 &lt;b&gt;무엇이 무엇을 죽였는지는 여전히 설명할 수 없었다.&lt;/b&gt; 그래서 넷을 전부 띄워 메모리를 채우고, 죽는 순간의 기록을 남겼다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;GitLab 컨테이너를 띄운다
Ubuntu 배포판을 켠다
Chrome 으로 GitLab &amp;middot; Grafana &amp;middot; ArgoCD 를 연다
VS Code 를 띄운다
그 상태에서 GitLab 에 clone &amp;middot; push 를 걸어 부하를 준다&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;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;h2 data-ke-size=&quot;size26&quot;&gt;3. 로그가 진단과 안 맞았다&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 GitLab &amp;mdash; &lt;code&gt;SIGKILL&lt;/code&gt;을 받았다는 것까지만 나왔다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;죽은 직후의 컨테이너를 그대로 조회했다. &lt;code&gt;docker ps&lt;/code&gt;는 이름과 상태를 한 줄로 요약해 주는 데 그치지만, &lt;code&gt;docker inspect&lt;/code&gt;는 그 컨테이너의 설정과 상태를 전부 꺼내 준다. 지워지지 않고 &lt;code&gt;Exited&lt;/code&gt;로 남아 있으면 &lt;b&gt;죽은 컨테이너도 조회된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 치면 JSON이 200줄 넘게 나오므로 &lt;code&gt;--format&lt;/code&gt;으로 넷만 뽑았다.&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;docker inspect gitlab --format &quot;restart={{.HostConfig.RestartPolicy.Name}} memlimit={{.HostConfig.Memory}} oomkilled={{.State.OOMKilled}} exit={{.State.ExitCode}}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;restart=unless-stopped  memlimit=0  oomkilled=false  exit=137&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷을 같이 뽑은 이유는 앞의 둘과 뒤의 둘이 성격이 다르기 때문이다. &lt;code&gt;HostConfig&lt;/code&gt;는 컨테이너를 만들 때 준 설정이라 안 변하고, &lt;code&gt;State&lt;/code&gt;는 마지막 종료 결과라 죽을 때마다 갱신된다. &lt;b&gt;&quot;내가 뭘 걸어 뒀나&quot;와 &quot;실제로 어떻게 끝났나&quot;를 나란히 놓아야&lt;/b&gt; 뒤의 이야기가 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&lt;code&gt;exit=137&lt;/code&gt; &amp;mdash; 강제 종료됐다는 것까지만 알려준다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 신호로 종료된 프로세스의 종료 코드는 &lt;code&gt;128 + 신호번호&lt;/code&gt;다. &lt;code&gt;137&lt;/code&gt;은 &lt;code&gt;128 + 9&lt;/code&gt;이고 9번 신호가 &lt;code&gt;SIGKILL&lt;/code&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;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;h5&gt;&lt;b&gt;&lt;code&gt;oomkilled=false&lt;/code&gt; &amp;mdash; 판정할 경로가 없었다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 부족으로 죽었다면 이 값이 &lt;code&gt;true&lt;/code&gt;여야 할 것 같은데 아니었다. &lt;code&gt;memlimit=0&lt;/code&gt;이 이유다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커는 컨테이너마다 메모리 한도를 걸 수 있는데, 안 걸면 &lt;code&gt;0&lt;/code&gt;이 되고 한도 없음이다. 그리고 도커가 &lt;code&gt;true&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SIGKILL&lt;/code&gt;이 오는 길이 여럿인데 그중 하나만 도커가 판정한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;① 컨테이너가 자기 한도를 넘었다    cgroup 이 죽인다              OOMKilled=true    메모리 때문
② VM 전체 메모리가 바닥났다       리눅스 커널이 골라 죽인다        OOMKilled=false   메모리 때문
③ VM &amp;middot; 엔진이 내려갔다           그 위의 것이 전부 끌려간다       OOMKilled=false   원인 미상
④ 사람이 docker kill 을 쳤다                                 OOMKilled=false   메모리 무관&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memlimit=0&lt;/code&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;h5&gt;&lt;b&gt;여기까지 알아낸 것&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;알아낸 것    SIGKILL 을 받고 끝났다
            ① (컨테이너 한도 초과) 은 아니다 &amp;mdash; 한도를 안 걸었으니까

못 알아낸 것  ②③④ 중 무엇인지
            메모리 때문인지조차 이 기록으로는 모른다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;메모리가 꽉 차서 죽었다&quot;는 것은 증상을 보고 세운 짐작이었고, 기록은 그것을 확인해 주지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한도를 걸어 뒀다면 &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; 하나로 ①인지 아닌지가 갈렸을 것이다. &lt;b&gt;한도는 제한이자 진단 도구&lt;/b&gt;인데 안 걸어 둬서 다른 기록을 더 뒤져야 했다. 어디에 어떻게 거는지는 뒤에서 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;Docker Desktop 쪽 기록&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;&lt;b&gt;3.2 Docker Desktop &amp;mdash; 시작조차 못 하고 &lt;code&gt;wsl.exe&lt;/code&gt;가 죽었다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab을 다시 띄우려고 Docker Desktop을 켰다. &lt;b&gt;그런데 이번엔 앱이 시작조차 못 하고 에러창을 띄웠다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;running wslexec: DockerDesktop/Wsl/ExecError:
  wsl.exe -d docker-desktop -u root -e wsl-bootstrap run ...
  exit status 0xc00000fd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&lt;code&gt;0xc00000fd&lt;/code&gt; &amp;mdash; 스택을 늘리지 못했다는 뜻이다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;0xc00000fd  =  STATUS_STACK_OVERFLOW&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;&quot;&gt;&lt;code&gt;함수를 부른다      그 함수의 지역 변수와, 끝나고 돌아갈 주소가 스택에 쌓인다
함수가 끝난다      그만큼 걷힌다
깊이 들어간다      함수 안에서 함수를 부르면 계속 쌓인다&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스택도 메모리다.&lt;/b&gt; 늘린다는 것은 곧 &lt;b&gt;Windows에 메모리를 더 달라고 요청하는 것&lt;/b&gt;이고, 그 요청이 거절되면 이 코드가 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;죽은 대상이 &lt;code&gt;wsl.exe&lt;/code&gt;였다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab도 아니고 엔진도 아니다. &lt;code&gt;wsl.exe&lt;/code&gt;는 &lt;b&gt;배포판을 켜는 Windows 쪽 명령&lt;/b&gt;이고, 컨테이너 메모리와는 상관이 없다. GitLab이 아무리 커져도 이 명령이 죽을 일은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;로그를 순서대로 읽으면 어디서 터졌는지 나온다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;wsl-bootstrap&lt;/code&gt;은 Docker Desktop이 &lt;code&gt;docker-desktop&lt;/code&gt; 배포판 안에서 돌리는 &lt;b&gt;준비 프로그램&lt;/b&gt;이다. 엔진이 뜰 환경을 만드는 일을 순서대로 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;componentsVersion.json 없음               [W] 경고.  첫 부팅이면 정상
detected existing ext4 file system        데이터 디스크를 찾았다 (이미지 &amp;middot; 볼륨)
docker-desktop.iso is present in cache    엔진 이미지가 이미 있다
starting linuxkit container with unshare  엔진을 격리해서 띄운다
Restoring existing exposed ports          전에 열려 있던 포트를 복원한다
starting cross-distro routing service     배포판끼리 통신하는 경로를 연다
tcp forward 0.0.0.0:2424 &amp;rarr; 172.18.0.2:22
tcp forward 0.0.0.0:8929 &amp;rarr; 172.18.0.2:8929
                                          &amp;larr; 여기서 exit 0xc00000fd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 두 줄이 &lt;b&gt;GitLab 컨테이너의 포트를 Windows 쪽으로 내보내는 작업&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;2424 &amp;middot; 8929      docker-compose.yml 에 적어 둔 포트.  SSH 와 HTTP
172.18.0.2       GitLab 컨테이너가 받은 IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitLab은 이미 떠서 IP까지 받은 상태였다.&lt;/b&gt; &lt;code&gt;restart: unless-stopped&lt;/code&gt;가 걸려 있어 엔진이 뜨자마자 따라 뜬 것이고, 그 마지막 연결 작업 중에 &lt;code&gt;wsl.exe&lt;/code&gt;가 죽었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&lt;code&gt;wsl.exe&lt;/code&gt;가 죽으면 그 밑이 전부 끌려간다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;wsl.exe 가 죽는다
  &amp;rarr; WSL2 VM 이 내려간다
     &amp;rarr; docker-desktop 배포판이 내려간다  &amp;rarr;  gitlab 컨테이너도 끝난다
     &amp;rarr; Ubuntu 배포판도 같이 내려간다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitLab에 &lt;code&gt;SIGKILL&lt;/code&gt;을 보낸 것이 무엇인지가 여기서 갈린다.&lt;/b&gt; 컨테이너가 자기 한도를 넘은 것도, VM 안에서 리눅스 커널이 골라 죽인 것도 아니었다. &lt;b&gt;VM 자체가 내려가면서 그 위의 것이 전부 끝난 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 중에 &lt;code&gt;kubectl&lt;/code&gt;이 갑자기 안 먹고 &lt;code&gt;wsl -l -v&lt;/code&gt;에 &lt;code&gt;Ubuntu&lt;/code&gt;가 &lt;code&gt;Stopped&lt;/code&gt;로 바뀌어 있던 것도 같은 이유였다. Ubuntu 쪽에서 뭘 잘못한 게 아니라 &lt;b&gt;Docker 쪽 사고에 같이 휩쓸린 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;질문이 옮겨진다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;전    GitLab 이 왜 죽었나
후    wsl.exe 가 스택을 늘리려는 요청은 왜 거절됐나&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제가 리눅스 안이 아니라 Windows 쪽에 있다.&lt;/b&gt; 그러면 다음으로 볼 것은 하나다 &amp;mdash; &lt;b&gt;그때 Windows에 메모리가 없었나.&lt;/b&gt;&lt;/p&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;&lt;b&gt;3.3 물리 메모리는 남아 있었다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 거절됐다면 물리 RAM이 바닥나 있어야 말이 된다. 그때 값을 봤다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Get-CimInstance Win32_OperatingSystem | ForEach-Object {
  &quot;물리 {0:N1}GB / 여유 {1:N1}GB&quot; -f ($_.TotalVisibleMemorySize/1MB), ($_.FreePhysicalMemory/1MB) }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;물리 16.0GB / 여유 6.6GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6GB 넘게 남아 있었다.&lt;/b&gt; 메모리가 없어서 못 준 것이 아니다.&lt;/p&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;&lt;b&gt;3.4 그래서 남은 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;wsl.exe&lt;/code&gt;가 죽은 순간에 한 일은 하나였다. &lt;b&gt;스택을 한 칸 더 쓰려고 Windows에 &quot;메모리를 조금만 더 달라&quot;고 한 것&lt;/b&gt;이다. Windows는 그것을 거절했고, 그래서 &lt;code&gt;STATUS_STACK_OVERFLOW&lt;/code&gt;로 끝났다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;① 물리 메모리는 6.6GB 남아 있었다        메모리가 없어서 못 준 것이 아니다
② 그런데 그 요청은 거절됐다               있는데도 안 준 것이다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주문까지 넣은 진단이 여기서 무너졌다.&lt;/b&gt; 램을 24GB로 늘려도, &lt;b&gt;물리가 남아 있는데 거절하는 그 무언가&lt;/b&gt;가 그대로면 같은 일이 또 난다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Windows 는 물리 RAM 이 남아 있어도 메모리 요청을 거절할 수 있다
그렇게 만드는 다른 한도가 있다&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 진짜 원인 &amp;mdash; 커밋 한도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 관리자 &lt;b&gt;성능 탭&lt;/b&gt;을 열었다. 프로세스 목록이 프로그램별로 보여주는 것과 달리, 여기는 &lt;b&gt;Windows 쪽 메모리를 통째로&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;715&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYP0pi/dJMcabdZFnb/7Tm9CCbmrUY4lbnl8B93nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYP0pi/dJMcabdZFnb/7Tm9CCbmrUY4lbnl8B93nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYP0pi/dJMcabdZFnb/7Tm9CCbmrUY4lbnl8B93nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYP0pi%2FdJMcabdZFnb%2F7Tm9CCbmrUY4lbnl8B93nk%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;715&quot; height=&quot;636&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;사용 중 (압축)   프로세스들이 쥐고 있는 물리 메모리.  괄호는 그중 압축해 눌러 담은 몫
사용 가능        지금부터 새 프로그램이 받을 수 있는 양
캐시됨          디스크에서 읽어 둔 사본.  필요하면 버린다
커밋됨          &amp;larr; 이것만 성격이 다르다                                        ★
페이징 풀        커널이 쓰는 몫 중 디스크로 밀어낼 수 있는 것
비페이징 풀       커널이 쓰는 몫 중 항상 물리에 있어야 하는 것 (드라이버 등)
사용된 슬롯       메모리 슬롯이 몇 개 차 있나&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&lt;code&gt;free -h&lt;/code&gt;와 나란히 놓아 보면&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM 안을 볼 때 쓴 명령이 &lt;code&gt;free -h&lt;/code&gt;였다. 같은 &quot;메모리 장부&quot;인데 &lt;b&gt;재는 대상이 다르다 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt; &lt;br /&gt;이쪽은 &lt;b&gt;VM 안(&lt;code&gt;total 5.8Gi&lt;/code&gt;는 &lt;code&gt;.wslconfig&lt;/code&gt;의 &lt;code&gt;memory=6GB&lt;/code&gt;에서 커널 몫을 뺀 값)&lt;/b&gt;이고, &lt;br /&gt;성능 탭은 &lt;b&gt;Windows 전체(물리 16.0GB)&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;b&gt;여기서 볼 것은 값이 아니라 칸 이름이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl -d Ubuntu -- free -h&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;               total   used   free   shared  buff/cache  available
Mem:           5.8Gi   3.2Gi  98Mi   98Mi    2.8Gi       2.6Gi
Swap:          4.0Gi   424Ki  4.0Gi&lt;/code&gt;&lt;/pre&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;used&lt;/code&gt; &amp;harr; &lt;code&gt;사용 중&lt;/code&gt;&lt;/b&gt; &amp;mdash; 프로세스가 실제로 쥐고 있는 양&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;buff/cache&lt;/code&gt; &amp;harr; &lt;code&gt;캐시됨&lt;/code&gt;&lt;/b&gt; &amp;mdash; 디스크에서 읽은 파일을 RAM에 남겨 둔 사본이다. &lt;b&gt;원본이 디스크에 그대로 있어 언제든 버려도 되고&lt;/b&gt;, 그래서 &lt;code&gt;available&lt;/code&gt;&amp;middot;&lt;code&gt;사용 가능&lt;/code&gt; 안에 이미 포함돼 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;available&lt;/code&gt; &amp;harr; &lt;code&gt;사용 가능&lt;/code&gt;&lt;/b&gt; &amp;mdash; 지금부터 새로 쓸 수 있는 양&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Swap&lt;/code&gt; &amp;harr; &lt;code&gt;페이지파일&lt;/code&gt;&lt;/b&gt; &amp;mdash; RAM이 모자랄 때 안 쓰는 것을 내려두는 디스크 자리다. &lt;b&gt;캐시와 방향이 반대라 버릴 수 없다&lt;/b&gt; &amp;mdash; RAM에만 있던 내용이라 디스크에 써 두지 않으면 사라진다. 다만 이 &lt;code&gt;Swap 4.0Gi&lt;/code&gt;는 VM 안 리눅스의 것(&lt;code&gt;.wslconfig&lt;/code&gt;의 &lt;code&gt;swap=4GB&lt;/code&gt;)이고, Windows 페이지파일과는 별개다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;커밋됨&lt;/code&gt; &amp;harr; 대응하는 칸이 없다&lt;/b&gt; ★&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&lt;code&gt;free -h&lt;/code&gt;에는 커밋에 해당하는 칸이 없다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 &lt;b&gt;요청을 일단 받아준다.&lt;/b&gt; 예약 총량으로 벽을 세우지 않고, 실제로 물리가 바닥나면 그때 OOM killer가 프로세스를 골라 죽인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows는 반대다. &lt;b&gt;예약 단계에서 한도를 넘으면 그 자리에서 거절한다.&lt;/b&gt; 죽이는 대신 요청이 실패하고, 요청한 쪽이 알아서 무너진다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;리눅스      일단 받는다   &amp;rarr;  물리가 바닥나면  &amp;rarr;  누군가를 죽인다      OOM killer 가 있다
Windows    한도를 넘으면  &amp;rarr;  그 자리에서 거절  &amp;rarr;  요청한 쪽이 실패한다   OOM killer 가 없다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;free -h&lt;/code&gt;만 봐서는 이번 원인이 안 잡히는 이유가 여기 있다.&lt;/b&gt; 문제를 만든 항목이 리눅스 쪽 화면에는 아예 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;&lt;code&gt;커밋됨&lt;/code&gt;만 분모가 다르다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용 중 &amp;middot; 사용 가능    합치면 물리 RAM 이 된다        9.4 + 6.6 = 16.0 GB
커밋됨               분모가 물리 RAM 이 아니다       15.5 / 17.9 GB

물리 여유             6.6 GB
커밋 여유             2.4 GB&lt;/code&gt;&lt;/pre&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;그래서 둘을 봐야 했다 &amp;mdash; &lt;b&gt;커밋이 무엇이고, 왜 한도가 하필 17.9GB인가.&lt;/b&gt;&lt;/p&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;&lt;b&gt;4.1 커밋이 무엇인가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 메모리를 요청하면 Windows는 물리 RAM을 바로 주지 않는다. &lt;b&gt;&quot;이만큼은 보장한다&quot;고 예약만 해 둔다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1GB 를 요청한다        예약 +1GB      물리 RAM 은 아직 안 썼다
100MB 를 실제로 쓴다    물리 +100MB    예약은 여전히 1GB
나머지 900MB          안 쓰고 있는데도 계속 예약돼 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;안 쓰고 있어도 다른 프로그램이 그 자리를 못 쓴다.&lt;/b&gt; Windows가 이미 약속해 뒀기 때문이다.&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;p data-ke-size=&quot;size16&quot;&gt;지금 돌고 있는 모든 프로그램의 예약을 전부 더한 것이 &lt;b&gt;커밋&lt;/b&gt;이고, 그 합이 넘을 수 없는 선이 &lt;b&gt;커밋 한도&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;커밋됨   15.5 / 17.9 GB
         커밋      한도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;한도가 물리 RAM보다 크다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;물리 RAM     16.0 GB
커밋 한도     17.9 GB      물리보다 1.9GB 더 크다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;없는 메모리를 약속해 둔 셈이다. &lt;b&gt;그게 가능한 이유가 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;페이지파일&lt;/span&gt;이고, 이 1.9GB가 곧 페이지파일 크기다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.2 페이지파일이 무엇인가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약속을 지키려면 실제로 내줄 자리가 있어야 한다. 물리 RAM이 비어 있으면 거기서 주고, &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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt; 물리 RAM 자리를 돌려쓰게 해 주는 임시 보관소&lt;/b&gt;&lt;/span&gt;다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;물리 RAM 16GB 가 다 찼는데 새 프로그램이 1GB 를 달라고 한다

① Windows 가 지금 안 쓰는 1GB 를 고른다        예: 30분째 안 만진 Chrome 탭
② 그 내용을 페이지파일에 써 둔다                디스크로 옮긴다
③ 비워진 물리 RAM 1GB 를 새 프로그램에게 준다
④ 나중에 그 Chrome 탭을 다시 보면              디스크에서 읽어 다시 올린다.  느리다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 &lt;code&gt;swap&lt;/code&gt;과 같은 역할이다. &lt;b&gt;약속한 총량이 동시에 물리에 올라와 있을 필요가 없어지므로&lt;/b&gt;, 물리 16GB로 그보다 큰 총량을 약속할 수 있다. 페이지파일이 한도에 그대로 더해지는 것이 이 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대가는 속도다. 디스크로 내려간 것을 다시 쓰려면 읽어 올려야 하고, RAM보다 훨씬 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;크기는 보통 Windows에 맡긴다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows 기본값이 &lt;b&gt;자동 관리&lt;/b&gt;이고, 그때 Windows가 하는 일이 이렇다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;크기 결정    물리 메모리 &amp;middot; 커밋 총량 &amp;middot; 크래시 덤프 설정을 보고 정한다
최소        RAM &amp;divide; 8 (최대 32GB)
확장        커밋이 한도의 90% 를 넘으면 늘린다
최대        물리 메모리의 3배 또는 4GB 중 큰 값.  단 그 볼륨 크기의 1/8 이하&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지파일 크기는 크래시 덤프 설정과 커밋 총량의 피크에 달려 있고, 둘 다 시스템마다 다르다. 그래서 페이지파일 크기도 시스템마다 다르며 일반화할 수 없다.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/how-to-determine-the-appropriate-page-file-size-for-64-bit-versions-of-windows&quot;&gt;How to determine the appropriate page file size for 64-bit versions of Windows&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 관리는 &lt;b&gt;그 판단을 Windows가 대신해 주는 것&lt;/b&gt;이다. 커밋이 차오르면 늘리고 한가해지면 줄이니, 내가 피크를 재서 숫자를 정할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;대신 디스크를 먹는다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지파일은 &lt;b&gt;실제로 디스크에 만들어지는 파일&lt;/b&gt;이다. 8GB로 잡으면 그 드라이브에서 8GB가 사라지고, 자동으로 두면 필요할 때 더 커지므로 그만큼 더 먹는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;디스크 용량 확보&quot;를 검색하면 페이지파일을 줄이거나 끄라는 방법이 나온다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;자동 관리를 끈다
C: 를 &quot;페이징 파일 없음&quot; 으로 바꾼다      C 에서 그만큼 용량이 돌아온다
필요하면 다른 드라이브에 작게 만든다&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그래서 설정을 열어 봤다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뺄셈으로 나온 페이지파일 크기가 1.9GB(&lt;code&gt;커밋 한도 17.9 &amp;minus; 물리 16.0&lt;/code&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;/p&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;&lt;b&gt;4.3 고정 페이징 파일 크기가 2GB로 고정돼 있었다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Win + R&lt;/code&gt; &amp;rarr; &lt;code&gt;sysdm.cpl&lt;/code&gt; &amp;rarr; &lt;b&gt;고급&lt;/b&gt; 탭 &amp;rarr; 성능 &lt;b&gt;설정&lt;/b&gt; &amp;rarr; &lt;b&gt;고급&lt;/b&gt; 탭 &amp;rarr; 가상 메모리 &lt;b&gt;변경\&lt;/b&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;392&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwdPcK/dJMcaafdPbz/IiyCxBWBORphZtztuFr7l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwdPcK/dJMcaafdPbz/IiyCxBWBORphZtztuFr7l1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwdPcK/dJMcaafdPbz/IiyCxBWBORphZtztuFr7l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwdPcK%2FdJMcaafdPbz%2FIiyCxBWBORphZtztuFr7l1%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;392&quot; height=&quot;197&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;627&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKE49H/dJMcabE65SS/nZkWYmaDL5ybqRWQYA0GL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKE49H/dJMcabE65SS/nZkWYmaDL5ybqRWQYA0GL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKE49H/dJMcabE65SS/nZkWYmaDL5ybqRWQYA0GL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKE49H%2FdJMcabE65SS%2FnZkWYmaDL5ybqRWQYA0GL1%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;420&quot; height=&quot;627&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;627&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oxgku/dJMcagl71xq/yfxUW7LknzHHfyip5HJZX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oxgku/dJMcagl71xq/yfxUW7LknzHHfyip5HJZX0/img.png&quot; data-alt=&quot;가상 메모리 대화상자 &amp;amp;mdash; C: 없음, E: 512 - 2048&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oxgku/dJMcagl71xq/yfxUW7LknzHHfyip5HJZX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foxgku%2FdJMcagl71xq%2FyfxUW7LknzHHfyip5HJZX0%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;392&quot; height=&quot;525&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상 메모리 대화상자 &amp;mdash; C: 없음, E: 512 - 2048&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;자동 관리 체크가 꺼져 있다        크기를 Windows 가 아니라 내가 정하고 있다
C:  없음                       232GB 짜리 드라이브에 페이지파일이 아예 없다
E:  512 - 2048                 여기 하나뿐이고 최대가 2048MB 다
현재 총 크기 1960MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1960MB가 뺄셈으로 나온 그 1.9GB다.&lt;/b&gt; 예상한 대로였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;두 줄이 겹쳐서 천장이 고정됐다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;자동 관리 해제      크기를 정하는 주체가 Windows 에서 나로 바뀐다
최대 2048MB       그렇게 내가 넣어 둔 천장이 2GB 다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘은 나란한 두 원인이 아니라 &lt;b&gt;하나를 두 각도에서 말한 것&lt;/b&gt;이다. 체크를 풀지 않으면 크기 칸이 잠겨서 값을 넣을 수 없고,&lt;b&gt; 값을 넣어 두면 Windows는 그 위로 못 올린다(내 데스크탑 상황).&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;커밋 한도 = 물리 16.0GB + 페이지파일 1.9GB = 17.9GB&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;p data-ke-size=&quot;size16&quot;&gt;이 설정을 언제 했는지도 기억이 났다. 예전에 C 드라이브 용량이 모자라 &quot;디스크 용량 확보&quot;를 찾아보고 &lt;a href=&quot;https://blog.naver.com/auslicht/220701618586&quot;&gt;그대로 따라 한 것&lt;/a&gt;이다. 그때는 페이지파일이 커밋 한도를 정한다는 것을 몰랐다.&lt;/p&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;&lt;b&gt;4.4 왜 그게 &lt;code&gt;0xc00000fd&lt;/code&gt; 를 만드나&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 나온 것을 모으면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;4.1   커밋 = 프로그램들이 약속받아 둔 총량.  커밋 한도 = 그 합이 넘을 수 없는 선
4.2   커밋 한도 = 물리 RAM + 페이지파일
4.3   자동 관리가 꺼진 채 페이지파일이 2GB 로 고정돼 있었다  &amp;rarr;  한도 17.9GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 상황에 물리적인 램 사용율과 커밋률에 대한 숫자가 이랬다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용 중 9.4 GB  +  사용 가능 6.6 GB  =  16.0 GB      물리는 이렇게 나뉘어 있다
커밋됨          15.5 / 17.9 GB                      약속은 이만큼 나가 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;물리와 커밋은 다른 장부다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;물리 장부    지금 실제로 어디에 무엇이 올라와 있나
커밋 장부    앞으로 누구에게 얼마를 주기로 했나&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;약속(15.5GB)이 실제 사용(9.4GB)보다 크다.&lt;/b&gt; 차이 6.1GB는 약속만 받고 아직 안 쓴 몫이고, &lt;b&gt;지금 어디에도 실체가 없다. 장부에만 적혀 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그래서 &lt;code&gt;사용 가능 6.6GB&lt;/code&gt;를 새로 못 준다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;약속은 했는데 아직 안 쓴 몫       6.1 GB   (커밋됨 &amp;minus; 물리 사용 중 = 15.5 &amp;minus; 9.4)
그것이 나중에 쓰이면 들어갈 자리    사용 가능 6.6 GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;사용 가능 6.6GB&lt;/code&gt;는 아무나 써도 되는 자리가 아니라, 이미 약속된 6.1GB가 들어올 자리다.(왜 6.1GB? 커밋된 용량 15.5에서 물리적 사용중인 용량 6.4를 빼면 나오는 값 - 미리 쓴다고 약속된 값 = 커밋)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 새 요청까지 받아주면 같은 자리를 두 번 약속하는 셈이 된다. Windows는 그것을 안 한다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용 가능           6.6 GB
&amp;minus; 이미 약속된 몫      6.1 GB
────────────────────────
물리에서 남는 것      0.5 GB
+ 페이지파일          1.9 GB
────────────────────────
커밋 여유           2.4 GB      &amp;larr; 17.9 &amp;minus; 15.5 와 같다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작업 관리자가 보여주던 &lt;code&gt;커밋 여유 2.4GB&lt;/code&gt;가 바로 이 계산의 결과다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;거기서 GitLab을 띄웠다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.wslconfig&lt;/code&gt;의 &lt;code&gt;memory=6GB&lt;/code&gt;는 &lt;b&gt;VM 쪽 상한&lt;/b&gt;이지, Windows가 그만큼 내주겠다는 보장이 아니다. VM이 커지는 만큼 &lt;b&gt;Windows 커밋 장부를 그대로 먹는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;그 상태            커밋 여유 2.4 GB
wsl 을 친다        Ubuntu 부팅으로 VM 이 커진다.  실측 Vmmem +1.3GB
                  &amp;rarr; 커밋 여유 약 1.1 GB
GitLab 까지 뜬다    컨테이너 정착만 2.9 GiB 를 요구한다
                  &amp;rarr; 남은 1.1GB 로는 못 받는다&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;거절당한 것은 &lt;code&gt;wsl.exe&lt;/code&gt; 였다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab이 요청했다가 거절당한 것이 아니다. &lt;b&gt;여유를 먹은 쪽과 거절당한 쪽이 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;GitLab 이 커진다  &amp;rarr;  VM 이 Windows 에 커밋을 더 요구한다  &amp;rarr;  여유가 바닥난다
                                                          &amp;darr;
                            wsl.exe 가 스택 커밋을 요청한다  &amp;rarr;  거절  &amp;rarr;  0xc00000fd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택은 처음부터 크게 잡지 않고 &lt;b&gt;깊이 들어갈 때마다 새 페이지를 커밋해 늘려 간다.&lt;/b&gt; 그 커밋이 거절되면 Windows는 &lt;code&gt;STATUS_STACK_OVERFLOW&lt;/code&gt;로 보고한다. 요청이 크든 작든 상관없다 &amp;mdash; &lt;b&gt;여유가 모자라면 똑같이 거절이다.&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;code&gt;wsl.exe&lt;/code&gt;가 죽으면 &lt;b&gt;VM이 통째로 내려가면서 그 위의 배포판과 컨테이너가 전부 끝난다.&lt;/b&gt; GitLab이 &lt;code&gt;oomkilled=false&lt;/code&gt;에 &lt;code&gt;exit 137&lt;/code&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;h5&gt;&lt;b&gt;그래서 어떤 날은 되고 어떤 날은 죽었다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋은 탭을 열고 닫을 때마다 오르내리는데 &lt;b&gt;한도는 17.9GB에 고정돼 있다.&lt;/b&gt; 여유가 2.4GB밖에 없는 상태에서는, 그날 켜 둔 것이 조금만 달라도 GitLab이 들어가기도 하고 안 들어가기도 한다.&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;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;&lt;b&gt;4.5 사슬 전체&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;① 페이지파일을 E 드라이브에 최대 2048MB 로 고정해 뒀다     자동 관리 해제 상태
② 커밋 한도 = 물리 16.0GB + 1.9GB = 17.9GB
③ 크롬 &amp;middot; VS Code &amp;middot; Windows 가 15.5GB 를 약속받아 뒀다     여유 2.4GB
④ GitLab 을 띄우자 VM 이 커밋을 더 요구했다               여유가 바닥난다
⑤ 그다음 커밋 요청이 거절됐다                            물리는 6.6GB 남았지만 한도가 먼저 막았다
⑥ 그 요청을 한 쪽이 wsl.exe 의 스택이었다                 0xc00000fd
⑦ wsl.exe 가 죽고 VM 이 통째로 내려갔다                   Ubuntu 도 GitLab 도 같이&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⑤에서 거절당하는 쪽은 그때그때 다르다.&lt;/b&gt; Chrome이 요청했으면 탭이 비워지고, VS Code면 통째로 다시 뜬다. 날마다 죽는 것이 달라 규칙이 없어 보이던 것이 이것이었다 &amp;mdash; &lt;b&gt;원인은 하나인데 걸리는 쪽이 매번 달랐다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;문제는 요구가 아니라 천장이었다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;요구    크롬 탭 &amp;middot; VS Code 확장 &amp;middot; VM.  작업하는 내내 오르내린다
천장    자동 관리를 꺼 두고 2048MB 로 고정해 뒀다.  안 움직인다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows에서 &lt;b&gt;커밋이 물리 RAM을 넘는 것은 사고가 아니라 기본 동작&lt;/b&gt;이다. 프로그램은 언제나 실제 쓸 양보다 크게 예약하고, 그 초과분을 받아주라고 페이지파일이 있다. &lt;b&gt;커밋이 15.5GB까지 오른 것 자체는 정상이다.&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;b&gt;천장이 물리보다 1.9GB밖에 높지 않았던 것&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;램을 24GB 로 늘린다        한도 = 24 + 1.9 = 25.9 GB     8GB 오른다.  부품값이 든다
페이지파일 상한을 올린다     한도 = 16 + 12 = 28.0 GB      12GB 오른다.  돈이 안 든다&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;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;h2 data-ke-size=&quot;size26&quot;&gt;5. 고친 과정&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 페이지파일을 다시 세웠다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번에 나눠 했다. &lt;b&gt;먼저 &lt;code&gt;C:&lt;/code&gt;에 8GB를 잡아 한도부터 올려 죽는 것이 멈추는지 봤고&lt;/b&gt;, 멈춘 것을 확인한 뒤 &lt;b&gt;자리를 &lt;code&gt;E:&lt;/code&gt;로 옮기면서 12GB로 키웠다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;고치기 전    C: 없음           E: 512 - 2048       한도 17.9 GB
1차         C: 8192 - 16384   E: 없음             한도 24.0 GB
2차         C: 없음           E: 12288 - 16384    한도 28.0 GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차만으로 GitLab이 죽지 않고 계속 떠 있었다. &lt;b&gt;원인을 제대로 짚었다는 것이 여기서 확인됐다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 그동안 &lt;b&gt;커밋이 최고 20.4GB까지 올라갔다.&lt;/b&gt; 한도 24.0GB에 3.6GB까지 붙은 것이라, 2차에서 자리를 옮기면서 크기를 키웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Win + R&lt;/code&gt; &amp;rarr; &lt;code&gt;sysdm.cpl&lt;/code&gt; &amp;rarr; &lt;b&gt;고급&lt;/b&gt; 탭 &amp;rarr; 성능 &lt;b&gt;설정&lt;/b&gt; &amp;rarr; &lt;b&gt;고급&lt;/b&gt; 탭 &amp;rarr; 가상 메모리 &lt;b&gt;변경&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;자동 관리 체크    해제 그대로 둔다
C:              페이징 파일 없음
E:              사용자 지정 크기.  처음 12288 MB / 최대 16384 MB&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FUrxg/dJMcadCPuSM/ksLl6k04T9APtoUfFM1ct1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FUrxg/dJMcadCPuSM/ksLl6k04T9APtoUfFM1ct1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FUrxg/dJMcadCPuSM/ksLl6k04T9APtoUfFM1ct1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFUrxg%2FdJMcadCPuSM%2FksLl6k04T9APtoUfFM1ct1%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;385&quot; height=&quot;519&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;519&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 넣고 &lt;b&gt;&lt;code&gt;설정(S)&lt;/code&gt;을 눌러야&lt;/b&gt; 목록에 반영된다. 그 상태로 &lt;code&gt;확인&lt;/code&gt;하고 재부팅해야 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;왜 &lt;code&gt;C:&lt;/code&gt;가 아니라 &lt;code&gt;E:&lt;/code&gt;인가&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;디스크 0   Samsung SSD 850 EVO 120GB   &amp;rarr;  E:   111GB 중 35.5GB 여유
디스크 1   WDC WDS250G2B0A (WD Blue)   &amp;rarr;  C:   232GB 중 16.8GB 여유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;C:&lt;/code&gt;의 16.8GB는 &lt;b&gt;이미 8GB 페이지파일이 올라간 상태의 여유&lt;/b&gt;다. 여기에 12GB를 두면 12.8GB가 남고, 나중에 16GB까지 자라면 8.8GB가 된다. 232GB짜리 시스템 드라이브에 남길 여유로는 빠듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;E:&lt;/code&gt;에 두면 &lt;b&gt;&lt;code&gt;C:&lt;/code&gt;의 여유가 24.8GB로 돌아오고&lt;/b&gt;, &lt;code&gt;E:&lt;/code&gt;에도 19.5GB가 남아 나중에 더 키울 자리가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능상으로도 불리하지 않다. &lt;b&gt;둘 다 SATA SSD&lt;/b&gt;라 느려질 이유가 없고, &lt;b&gt;물리 디스크가 서로 달라 OS의 디스크 작업과 페이징이 같은 디스크를 놓고 다투지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;왜 자동 관리가 아니라 수동인가&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 관리로 두면 Windows가 최대치를 &lt;b&gt;&lt;code&gt;물리의 3배 또는 4GB 중 큰 값, 단 그 볼륨 크기의 1/8 이하&lt;/code&gt;&lt;/b&gt;로 잡는다. &lt;code&gt;E:&lt;/code&gt;에 대입하면 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;물리의 3배        48.0 GB
볼륨의 1/8        111GB &amp;divide; 8 = 13.9 GB      &amp;larr; 둘 중 작은 쪽이 천장이 된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;E:&lt;/code&gt;에 자동 관리를 걸면 최대가 13.9GB에서 막힌다.&lt;/b&gt; 수동으로 &lt;code&gt;16384&lt;/code&gt;를 넣는 쪽이 천장이 더 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;왜 12288 - 16384 인가&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;관측된 커밋 피크    20.4 GB       GitLab + Ubuntu + Chrome + VS Code 를 다 켠 상태
물리              16.0 GB
최소로 필요한 것    20.4 &amp;minus; 16.0 = 4.4 GB

처음 12288        한도 28.0GB 로 부팅한다.  피크 대비 7.6GB 여유
최대 16384        모자라면 32.0GB 까지 자란다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 크기를 크게 잡으면 &lt;b&gt;커밋이 몰릴 때 파일이 자라기를 기다리지 않아도 되고&lt;/b&gt;, 파일이 한 덩어리로 잡혀 조각나지 않는다. 대가는 &lt;code&gt;E:&lt;/code&gt;에서 12GB가 계속 나가 있는 것 하나다.&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;b&gt;5.2 커밋 한도가 28.0GB가 됐다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재부팅하고 같은 자리를 다시 봤다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                    고치기 전            고친 후
──────────────────────────────────────────────────────
페이지파일            E:  512 - 2048      E:  12288 - 16384
커밋 한도             17.9 GB             28.0 GB       &amp;larr; 바뀐 것은 이 줄뿐이다
물리 RAM             16.0 GB             16.0 GB        한 장도 안 바꿨다&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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;고치기 전    커밋 15.5 / 17.9 GB    여유  2.4 GB
고친 후      커밋 15.1 / 28.0 GB    여유 12.9 GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;켜 둔 것이 같아서가 아니라 한도가 달라서 벌어진 차이다.&lt;/b&gt; 커밋을 만든 쪽은 그때나 지금이나 Chrome&amp;middot;VS Code&amp;middot;Windows이고, 바꾼 것은 페이지파일 한 줄이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;증상          하루에 열 번 가까이 뭔가가 죽었다
처음 진단      메모리 80퍼센트.  물리 램 부족으로 보고 8GB 두 장을 주문했다
안 맞은 기록    oomkilled=false  &amp;middot;  0xc00000fd  &amp;middot;  물리 여유 6.6GB
진짜 원인      커밋 한도 17.9GB.  페이지파일을 2GB 로 고정해 둔 설정이 그대로 남아 있었다
고친 것        페이지파일 12GB.  한도 28.0GB.  커밋 여유 2.4 &amp;rarr; 12.9GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상이 가리키는 곳과 원인이 있는 곳이 달랐다.&lt;/b&gt; 메모리가 80퍼센트라는 것은 사실이었지만 그것이 죽은 이유는 아니었고, 죽은 이유는 같은 화면의 다른 칸에 있었다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그래서 램 주문은 취소했다&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;주문한 것    DDR3 8GB &amp;times; 2      4GB 두 장을 빼고 넣을 생각이었다
근거        메모리 80퍼센트.  뭔가가 계속 죽는다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 근거가 기록과 안 맞았다. &lt;b&gt;물리 RAM은 6.6GB가 남은 상태에서 요청이 거절됐고&lt;/b&gt;, 페이지파일만 바꾸니 커밋이 비슷한 시점끼리 견줘 &lt;b&gt;여유가 2.4GB에서 12.9GB가 됐다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그렇다고 안 사도 된다는 뜻은 아니다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리 16GB는 그대로다. &lt;b&gt;천장을 올린 것이지 RAM을 늘린 것이 아니다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;지금        16 + 12 = 28.0 GB       필요하면 16 까지 자라 32.0 GB
더 올린다    최대를 24576 으로       16 + 24 = 40.0 GB.  E: 여유로 가능하다&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;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;pre class=&quot;&quot;&gt;&lt;code&gt;천장이 낮다      요청이 거절된다    &amp;rarr;  죽는다        천장을 올리면 해결된다
물리가 모자라다   요청은 통과한다    &amp;rarr;  디스크를 오간다  &amp;rarr;  느려진다   천장으로는 해결 안 된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지파일을 40GB로 잡아도 &lt;b&gt;물리 16GB를 넘는 몫은 결국 디스크를 오간다.&lt;/b&gt; 이것은 램 말고 답이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;둘을 가르는 값&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PeakUsage&lt;/code&gt; 하나다. &lt;b&gt;페이지파일로 실제로 얼마나 밀려났는지&lt;/b&gt;를 알려주는 값이고, 지금은 1MB다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;1MB 근처에 머문다      밀어낼 일이 없었다.  물리로 충분하다  &amp;rarr;  램은 아직 필요 없다
GB 단위로 오른다       물리가 모자라 밀어내기 시작했다        &amp;rarr;  ②&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;넣는다면&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;지금    DDR3 4GB &amp;times; 4 = 16GB              슬롯 4개가 전부 차 있다
교체    8GB &amp;times; 2 + 4GB &amp;times; 2 = 24GB          추가가 아니라 교체다
한도    24 + 12 = 36.0 GB                 페이지파일을 안 건드려도 8GB 오른다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;램을 늘렸다고 페이지파일을 따라 늘릴 이유는 없다.&lt;/b&gt; 물리가 커지면 밀어낼 일이 줄어들어 수요는 오히려 준다.&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/219</guid>
      <comments>https://zed6740.tistory.com/219#entry219comment</comments>
      <pubDate>Wed, 5 Aug 2026 13:22:23 +0900</pubDate>
    </item>
    <item>
      <title>노트북 홈서버 k3s 구축기 - 9. GitLab 세우기</title>
      <link>https://zed6740.tistory.com/218</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 이어받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8부 끝에서 관측 파이프라인은 흐른다. 같은 k3s 프로세스를 kubelet&amp;middot;apiserver 두 job으로 겹쳐 긁던 시리즈 사본을 걷어 거절이 0이 됐고, 파이프라인 자신을 보는 계기판도 Mimir 3축 12패널로 섰다. 계기판은 git에 push하면 ArgoCD가 가져가 Grafana에 앉힌다 &amp;mdash; 커밋이 곧 배포다.&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;1&amp;ndash;5부에서 호스트와 노드, 6부에서 클러스터, 7부에서 플랫폼과 GitOps 인계, 8부에서 관측까지 섰다. 남은 길은 앱을 향한다 &amp;mdash; 10부 이미지, 11부 앱 배포, 12부 앱 관측, 13부 부하와 스펙 산정, 14부 외부 공개.&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;push마다 최대 3분을 기다린다.&lt;/b&gt; ArgoCD가 GitHub를 주기적으로 다시 읽는 방식이라, 8부에서 계기판을 고칠 때마다 이 대기를 반복했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계기판은 열어봐야 안다.&lt;/b&gt; 알림 셋은 조건까지 확정했고 구현이 비어 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터 자체를 보는 눈이 없다.&lt;/b&gt; 계기판의 질문은 &quot;관측이 도는가&quot;였다. &quot;클러스터가 건강한가&quot;를 보는 대시보드는 아직 한 장도 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grafana&amp;middot;ArgoCD가 &lt;code&gt;kubectl port-forward&lt;/code&gt;로만 열린다.&lt;/b&gt; 명령을 띄워 둔 동안만 내 PC의 포트가 파드로 이어지고, 터미널을 닫으면 끊긴다.&lt;/li&gt;
&lt;/ul&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;b&gt;이 편이 하는 일&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 서버(GitLab)를 데스크탑에 세우고 GitOps 소스를 그리로 옮긴다. Git 서버가 ArgoCD와 같은 망에 서면 webhook이 성립해 push 즉시 sync가 된다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;지금:  push ─▶ GitHub ─(3분 폴링)─ ArgoCD ─▶ 클러스터
9부:   push ─▶ GitLab(데스크탑) ─ webhook ─▶ ArgoCD ─▶ 클러스터
                     └─ 미러 ─▶ GitHub(쇼케이스)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;webhook이 성립하려면 GitLab이 ArgoCD를 주소로 부를 수 있어야 하고, 그러려면 ArgoCD가 포워딩이 아니라 고정된 주소로 서야 한다 &amp;mdash; 넷째 자리가 여기서 같이 닫힌다. 루프가 초 단위가 되면 그 위에서 알림 룰과 인프라 대시보드를 만든다. 둘 다 git push로 배달되는 물건이라, 네 자리가 이 한 흐름으로 닫힌다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 왜 Git 서버를 직접 세우나   지금 끊긴 구간
2. 개념과 출발선              Git 서버 &amp;middot; CI/CD &amp;middot; GitOps &amp;mdash; 지금 배선과 자리 만들기
3. 구조와 설치                물건 &amp;middot; 부품 &amp;middot; omnibus &amp;middot; 기동 &amp;middot; 저장소 이전 &amp;middot; 미러
4. GitLab을 운영 원본으로     브랜치 &amp;middot; 자격 &amp;middot; 노출 &amp;middot; repoURL 전환 &amp;middot; webhook
5. 루프 위에서                알림 셋 &amp;middot; 인프라 대시보드 두 장
6. 대조와 다음                닫는 조건 &amp;middot; 보안 되짚기 &amp;middot; 10부로&lt;/code&gt;&lt;/pre&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 왜 Git 서버를 직접 세우나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 홈랩의 목표는 &lt;b&gt;온프레미스 서버 한 대에 stg&amp;middot;prd로 나아갈 수 있는 dev 환경을 세우는 것&lt;/b&gt;이다. 배포판은 k3s, 배포할 서비스는 CGV 예매 대기열(queue&amp;middot;booking&amp;middot;frontend)이고 docker compose에서 예매 왕복까지 확인된 상태다(&lt;a href=&quot;https://github.com/sss654654/cgv-onprem&quot;&gt;cgv-onprem&lt;/a&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;pre class=&quot;stata&quot;&gt;&lt;code&gt;Git 저장소 ─▶ CI(러너)가 이미지 빌드 ─▶ 레지스트리에 저장 ─▶ CD가 클러스터에 반영&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;7부에서 App으로 걸어만 둔 앱 셋이 &lt;code&gt;ImagePullBackOff&lt;/code&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;Git 저장소&lt;/b&gt; &amp;mdash; 코드는 GitHub(관리형)에 있다. 직접 운영하는 Git 서버는 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI(러너)&lt;/b&gt; &amp;mdash; 없다. 이미지를 만들 곳이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레지스트리&lt;/b&gt; &amp;mdash; 없다. 만든 이미지를 올릴 곳이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CD&lt;/b&gt; &amp;mdash; ArgoCD가 자리를 잡고 있다(7부에서 인계). 앱 이미지가 흘러들 길이 없을 뿐이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 빈 구간들을 채우는 답으로 &lt;b&gt;GitLab 셀프호스팅&lt;/b&gt;을 골랐다. 저장소&amp;middot;CI 러너&amp;middot;레지스트리가 한 제품에 들어 있어, GitHub가 맡던 저장소 자리까지 옮기면 코드가 배포되기까지의 전 구간이 한 자리에 선다.&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;폴링 문제도 같은 자리에서 풀린다. GitHub은 인터넷 쪽에 있고 이 클러스터는 공유기 뒤 사설 IP라, 밖에서 안으로 들어오는 연결이 닿지 않는다 &amp;mdash; GitHub이 ArgoCD를 부를 방법이 없다. Git 서버가 같은 망에 서면 그 호출이 성립한다.&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;/b&gt; 정본이 자기가 정의하는 대상 안에 살면 대상과 함께 무너진다. 어느 장비인지는 무게까지 재서 정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역할 = GitLab이 운영의 원본.&lt;/b&gt; GitOps 소스도 러너&amp;middot;CI&amp;middot;레지스트리도 여기 얹고, GitHub는 미러로 받는 사본이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;범위 = 이 편은 루프 완성까지.&lt;/b&gt; 나머지 셋이 10부에서 서면 앱 이미지가 생기고, 11부에서 앱이 뜬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 개념과 출발선 &amp;mdash; Git 서버, CI/CD, GitOps&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 편에서 만지는 이름은 셋이다 &amp;mdash; Git 서버, CI/CD, GitOps. 겹쳐 다니지만 다른 물건이다.&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;b&gt;2.1 Git 서버 &amp;mdash; 원본이 앉는 자리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 자체는 어느 한 곳을 원본으로 정하지 않는다. 저장소의 완전한 복제가 내 컴퓨터에도 서버에도 있고, push&amp;middot;pull은 복제끼리 맞추는 동작이다. 다만 여러 사람과 여러 도구가 한 저장소를 쓰려면 &quot;여기가 원본&quot;이라고 합의한 자리가 필요하다. 그 자리를 드는 것이 Git 서버고, 지금까지 그 자리는 GitHub였다.&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;b&gt;2.2 CI/CD &amp;mdash; 코드가 클러스터까지 가는 길&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CI(Continuous Integration)&lt;/b&gt; &amp;mdash; push된 코드를 자동으로 빌드&amp;middot;검사해 산출물로 만드는 것. 여기서 산출물은 컨테이너 이미지고, 빌드를 실제로 도는 일꾼이 &lt;b&gt;러너&lt;/b&gt;다. 러너가 만든 이미지는 &lt;b&gt;레지스트리&lt;/b&gt;에 올라간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CD(Continuous Deployment)&lt;/b&gt; &amp;mdash; 그 산출물이 실제 환경에 반영되는 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD에는 방향이 둘 있다.&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;push형&lt;/b&gt; &amp;mdash; 파이프라인이 밖에서 클러스터로 kubectl&amp;middot;helm을 쏜다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pull형&lt;/b&gt; &amp;mdash; 클러스터 안의 에이전트가 git을 보고 스스로 상태를 맞춘다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클러스터는 pull형이다. 클러스터 전권 자격을 바깥 파이프라인에 내주지 않아도 되고, git과 클러스터가 계속 대조된다. 그 방식의 이름이 GitOps다.&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;b&gt;2.3 GitOps &amp;mdash; git이 정본, 클러스터가 따라온다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언은 하나다. &lt;b&gt;저장소가 원하는 상태의 정본이고, 클러스터의 실제 상태는 그걸 따라온다.&lt;/b&gt; 7부에서 &quot;커밋이 곧 배포&quot;가 된 것이 이 선언을 켠 것이다.&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;따라오게 만드는 동작은 ArgoCD의 세 단계 반복이다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;① refresh   저장소를 읽어 &quot;원하는 상태&quot;를 갱신한다     &amp;larr; 기본 3분 주기 폴링
② diff      클러스터의 &quot;실제 상태&quot;와 비교한다
③ sync      다르면 적용한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sync를 자동으로 돌릴지는 Application의 정책으로 정한다. 손으로 kubectl을 쳐서 바꾼 것을 되돌리고(selfHeal), &lt;b&gt;정본에서 지운 것을 클러스터에서도 걷는(prune) 동작&lt;/b&gt;은 그 정책 아래 각각 켜 두는 옵션이고 &lt;b&gt;기본값은 꺼짐&lt;/b&gt;이다. 켜야 정본과 실제가 계속 같아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴링은 설정 없이 돌지만 push와 반영 사이에 최대 한 주기가 낀다. &lt;b&gt;webhook은 방향이 반대다&lt;/b&gt; &amp;mdash; git 서버가 push를 받는 순간 ArgoCD의 수신 주소(&lt;code&gt;/api/webhook&lt;/code&gt;)를 호출해 refresh를 그 자리에서 당긴다. 그래서 webhook은 설정 문제이기 전에 &lt;b&gt;&quot;git 서버가 ArgoCD에 닿을 수 있는가&quot;라는 네트워크 문제&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;&lt;b&gt;2.4 지금 배선의 좌표 셋&lt;/b&gt;&lt;/h4&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;클러스터가 저장소를 보게 만든 것은 7부의 root-app이다&lt;/b&gt; &amp;mdash; 저장소에 있는 다른 Application 정의들을 읽어 오는 최상위 Application 하나로, 이것만 손으로 넣으면(&lt;code&gt;kubectl apply&lt;/code&gt; 한 번) 나머지는 저장소에서 딸려 온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;App 19개가 각각 repoURL로 GitHub의 cgv-infra를 가리킨다.&lt;/b&gt; repoURL은 그 App이 읽어 갈 저장소 주소를 적는 칸이고, 그 자리는 13개 파일에 걸친 14줄이다(repoURL 11줄 + AppProject의 sourceRepos 3줄. AppProject는 App 묶음이 어느 저장소를 읽어도 되는지 범위를 정하는 리소스라, 여기 없는 주소는 App이 가리켜도 거부된다). &lt;b&gt;소스를 옮긴다는 것은 이 14줄을 바꾼다는 뜻이다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앱 코드(cgv-onprem)는 ArgoCD가 보지 않고, ArgoCD 자신은 ClusterIP뿐이다.&lt;/b&gt; 앱 배포는 차트가 가리키는 이미지로 일어나고, ArgoCD는 클러스터 밖에서 닿을 주소가 없다.&lt;/li&gt;
&lt;/ul&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;b&gt;2.5 자리 만들기 &amp;mdash; 주소 고정과 Docker&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데스크탑에 GitLab이 서기 전에 자리 둘을 만든다.&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; GitLab이 서면 데스크탑을 찾아오는 쪽이 생긴다 &amp;mdash; repoURL&amp;middot;git remote&amp;middot;webhook에 이 주소가 들어간다. 데스크탑 IP는 DHCP 자동 할당이라 바뀔 수 있으므로, &lt;b&gt;공유기에 DHCP 예약(이 MAC에는 항상 이 주소)을 건다&lt;/b&gt;. 기기 쪽 설정은 그대로고 결과만 고정된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker Desktop.&lt;/b&gt; &lt;b&gt;GitLab은 리눅스 프로그램이라 Windows에서 직접 못 돈다.&lt;/b&gt; Docker Desktop은 WSL2(Windows 안에서 리눅스 커널을 통째로 돌리는 가상머신 계층) 안의 리눅스에서 컨테이너를 돌리고, 컨테이너가 연 포트를 Windows 호스트 포트로 이어준다 &amp;mdash; 클러스터가 데스크탑 IP로 찾아왔을 때 컨테이너까지 닿는 길이 이 배선이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. GitLab의 구조와 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 블록이다. 앞은 세울 물건을 여는 쪽 &amp;mdash; 어떤 제품이고(3.1), 어떤 부품으로 이루어졌고(3.2), 무엇으로 묶여 있고(3.3), 어디에 두나(3.4). 기동 파일에 들어가는 값이 전부 여기서 나온다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;① 컨테이너 기동      3.5 기동 파일 &amp;middot; 3.6 기동과 확인
② root 계정 정리     3.7 첫 로그인과 잠그기
③ 저장소 둘 이전     3.8
④ GitHub 미러 연결   3.9&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;GitLab 안에서 파이프라인을 만들어 러너에게 일을 나눠 주는 쪽(Build &amp;rarr; Pipelines)과 컨테이너 레지스트리는 켜지 않는다 &amp;mdash; 러너 자체도 컨테이너 밖 별도 프로세스라 10부에서 붙인다.&lt;/p&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;&lt;b&gt;3.1 GitLab이라는 물건&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 저장소 호스팅에서 출발해 CI/CD&amp;middot;컨테이너 레지스트리&amp;middot;이슈&amp;middot;MR(Merge Request &amp;mdash; GitHub의 Pull Request에 해당하는, 브랜치를 합치자는 요청)까지 한 제품에 붙여 온 소프트웨어다. 셀프호스팅 판이 원형이고, GitLab.com이라는 관리형 서비스는 그 위에 얹힌 것이다.&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;라이선스는 오픈코어 &amp;mdash; 핵심은 오픈소스로 열고 그 위 기능을 유료로 파는 모델이다. 무료인 &lt;b&gt;CE&lt;/b&gt;(Community Edition)와 유료 기능을 얹은 EE로 나뉘고, 여기서 쓰는 것은 CE다. 이 편과 다음 편에 필요한 기능은 전부 CE에 있다.&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;요구조건은 둘이다 &amp;mdash; 저장소&amp;middot;CI&amp;middot;레지스트리를 &lt;b&gt;한 제품&lt;/b&gt;으로, 그리고 &lt;b&gt;내 망 안에&lt;/b&gt;. 이 둘을 받을 후보는 셋이었다.&lt;/p&gt;
&lt;table style=&quot;height: 130px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;width: 109px; height: 23px;&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th style=&quot;width: 308px; height: 23px;&quot;&gt;Git 서버&lt;/th&gt;
&lt;th style=&quot;width: 204px; height: 23px;&quot;&gt;CI&lt;/th&gt;
&lt;th style=&quot;width: 121px; height: 23px;&quot;&gt;레지스트리&lt;/th&gt;
&lt;th style=&quot;width: 111px; height: 23px;&quot;&gt;내 망에 설치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 44px;&quot;&gt;
&lt;td style=&quot;width: 109px; height: 44px;&quot;&gt;GitHub&lt;/td&gt;
&lt;td style=&quot;width: 308px; height: 44px;&quot;&gt;서비스로만&lt;/td&gt;
&lt;td style=&quot;width: 204px; height: 44px;&quot;&gt;Actions&lt;/td&gt;
&lt;td style=&quot;width: 121px; height: 44px;&quot;&gt;GHCR&lt;/td&gt;
&lt;td style=&quot;width: 111px; height: 44px;&quot;&gt;안 된다 (기업용 유료판 제외)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 109px; height: 42px;&quot;&gt;Gitea&lt;/td&gt;
&lt;td style=&quot;width: 308px; height: 42px;&quot;&gt;된다 &amp;mdash; 단일 바이너리, 메모리 수백 MB급&lt;/td&gt;
&lt;td style=&quot;width: 204px; height: 42px;&quot;&gt;GitHub Actions 호환(2023)&lt;/td&gt;
&lt;td style=&quot;width: 121px; height: 42px;&quot;&gt;경량 내장(2022)&lt;/td&gt;
&lt;td style=&quot;width: 111px; height: 42px;&quot;&gt;된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 109px; height: 21px;&quot;&gt;&lt;b&gt;GitLab CE&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 308px; height: 21px;&quot;&gt;&lt;b&gt;된다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 204px; height: 21px;&quot;&gt;&lt;b&gt;된다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 121px; height: 21px;&quot;&gt;&lt;b&gt;된다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 111px; height: 21px;&quot;&gt;&lt;b&gt;된다&lt;/b&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;GitHub는 내 망 조건에서 탈락한다. Gitea는 형식상 조건을 채우지만 CI&amp;middot;레지스트리가 뒤에 붙은 경량 구현이다. 온프렘&amp;middot;사내망에서 셀프호스팅 Git 서버로 널리 쓰이는 쪽은 GitLab이다.&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;조각을 모으는 길도 있다 &amp;mdash; Gitea(Git 서버) + Jenkins(CI) + Harbor(레지스트리). 조각마다 기능은 더 깊지만 세 제품을 설치&amp;middot;연결&amp;middot;운영해야 하고, 제품 사이마다 인증&amp;middot;webhook 배선이 생긴다.&lt;/p&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;&lt;b&gt;3.2 부품과 요청의 길&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 제품이라고 안까지 하나인 것은 아니다. GitLab은 역할이 나뉜 부품 묶음이다 &amp;mdash; Mimir가 distributor&amp;middot;ingester&amp;middot;querier로 나뉘어 있던 것과 같은 구도다. HTTP로 들어오는 길을 위에서 아래로 따라가면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;브라우저 &amp;middot; git (HTTP)
   │
   ▼
 nginx ─────────── HTTP 문
   │
   ▼
 workhorse ─────── 중계. 무거운 git 전송은 여기서 직접
   │      │
   │      └──────▶ gitaly  (clone&amp;middot;push의 실제 전송)
   ▼
 puma ──────────── 화면&amp;middot;API를 만드는 Rails 웹앱
   │
   ├──▶ PostgreSQL          장부: 사용자&amp;middot;프로젝트&amp;middot;권한
   ├──▶ gitaly              코드 조회
   └──▶ Redis 큐 ──▶ sidekiq ──▶ 미러 push &amp;middot; webhook 발사

 git (SSH) ──▶ sshd ──▶ gitlab-shell ──▶ gitaly&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 317px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;width: 126px; height: 23px;&quot;&gt;컴포넌트&lt;/th&gt;
&lt;th style=&quot;width: 119px; height: 23px;&quot;&gt;언어&lt;/th&gt;
&lt;th style=&quot;width: 201px; height: 23px;&quot;&gt;역할&lt;/th&gt;
&lt;th style=&quot;width: 407px; height: 23px;&quot;&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 42px;&quot;&gt;&lt;b&gt;nginx&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 42px;&quot;&gt;C &amp;mdash; 기성품&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 42px;&quot;&gt;리버스 프록시(요청을 대신 받아 뒤로 넘기는 앞단 서버)&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 42px;&quot;&gt;포트를 열고 요청을 뒤로 넘긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 42px;&quot;&gt;&lt;b&gt;workhorse&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 42px;&quot;&gt;Go&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 42px;&quot;&gt;git 전송 프록시&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 42px;&quot;&gt;pack 데이터(git이 커밋&amp;middot;파일을 한 덩어리로 압축해 주고받는 형식)와 대용량 업로드를 Rails 대신 받는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 21px;&quot;&gt;&lt;b&gt;puma&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 21px;&quot;&gt;Ruby (Rails)&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 21px;&quot;&gt;웹 애플리케이션 서버&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 21px;&quot;&gt;화면과 API를 만든다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 21px;&quot;&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 21px;&quot;&gt;C &amp;mdash; 기성품&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 21px;&quot;&gt;관계형 DB&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 21px;&quot;&gt;메타데이터 &amp;mdash; 사용자&amp;middot;프로젝트&amp;middot;MR&amp;middot;권한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 42px;&quot;&gt;&lt;b&gt;gitaly&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 42px;&quot;&gt;Go&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 42px;&quot;&gt;git 저장소 접근 서비스&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 42px;&quot;&gt;git 저장소 파일을 만지는 유일한 프로세스. 다른 부품은 RPC(다른 프로세스의 기능을 호출로 쓰는 방식)로만 닿는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 42px;&quot;&gt;&lt;b&gt;Redis&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 42px;&quot;&gt;C &amp;mdash; 기성품&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 42px;&quot;&gt;인메모리 캐시&amp;middot;잡 큐&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 42px;&quot;&gt;잡(응답과 분리해 나중에 처리할 작업 한 건)을 쌓아 두는 대기열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 42px;&quot;&gt;&lt;b&gt;sidekiq&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 42px;&quot;&gt;Ruby (Rails)&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 42px;&quot;&gt;비동기 잡 워커&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 42px;&quot;&gt;큐에서 잡을 꺼내 미러 push&amp;middot;webhook 발사&amp;middot;메일을 처리한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 126px; height: 42px;&quot;&gt;&lt;b&gt;sshd + gitlab-shell&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 119px; height: 42px;&quot;&gt;Go (sshd는 OpenSSH &amp;mdash; C)&lt;/td&gt;
&lt;td style=&quot;width: 201px; height: 42px;&quot;&gt;SSH 접속 처리&lt;/td&gt;
&lt;td style=&quot;width: 407px; height: 42px;&quot;&gt;git 명령의 권한을 내부 API(puma)에 물어 확인받고 gitaly로 넘긴다&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;언어에 규칙이 보인다. &lt;b&gt;몸통(화면&amp;middot;로직)은 Ruby, 굵은 통로(전송&amp;middot;git 연산&amp;middot;SSH)는 Go, 문&amp;middot;장부&amp;middot;큐는 기성품.&lt;/b&gt; 대기열은 Go, 예매는 Java로 가른 이 프로젝트의 폴리글랏과 같은 논리다.&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;통로가 Go인 이유는 연결을 무는 방식에 있다. Rails가 전송을 직접 물면 clone 하나에 웹앱이 전송 내내 묶이는데,&lt;b&gt; Go는 동시 연결 여럿을 적은 메모리로 오래 문다.&lt;/b&gt; 뒷일을 맡는 sidekiq만 Ruby인 것은 그것도 앱의 규칙이어서다 &amp;mdash; 언어를 가르면 같은 로직을 두 벌 짜게 된다.&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;코드의 실물은 DB에 없다.&lt;/b&gt; PostgreSQL은 사용자&amp;middot;권한&amp;middot;MR 같은 둘레 장부고, 코드는 gitaly가 드는 git 저장소에 있다. 화면 한 장을 만들 때도 puma는 두 곳을 다 묻는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;push의 응답과 뒷일은 분리돼 있다.&lt;/b&gt; gitaly가 저장을 마치면 push는 성공으로 돌아가고, 미러 push&amp;middot;webhook 발사 같은 뒷일은 그 시점에 Redis 큐에 들어가 있을 뿐이다. 실제 처리는 sidekiq이 따로 하므로 뒷일은 각자의 지연이나 실패를 가진다.&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;/p&gt;
&lt;table style=&quot;height: 86px; width: 575px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px; width: 278px;&quot;&gt;증상&lt;/th&gt;
&lt;th style=&quot;height: 23px; width: 297px;&quot;&gt;볼 부품&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 278px;&quot;&gt;&lt;b&gt;화면이 안 열린다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 297px;&quot;&gt;&lt;b&gt;nginx &amp;middot; puma&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 278px;&quot;&gt;&lt;b&gt;push&amp;middot;clone이 안 된다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 297px;&quot;&gt;&lt;b&gt;gitaly &amp;mdash; 인증에서 막히면 puma&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 278px;&quot;&gt;&lt;b&gt;미러&amp;middot;webhook이 안 온다&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 297px;&quot;&gt;&lt;b&gt;sidekiq&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&lt;b&gt;3.3 omnibus &amp;mdash; 부품을 하나로 묶는 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab이 내놓는 배포 형태는 둘이다.&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;omnibus 패키지&lt;/b&gt; &amp;mdash; GitLab과 그것이 돌아가는 데 필요한 소프트웨어를 전부 묶은 설치 패키지. 공식 설명은 &lt;b&gt;&quot;GitLab을 돌리는 데 필요한 서비스와 도구를 전부 한 패키지에 묶는다&quot;&lt;/b&gt;고 적는다. 담긴 목록과 버전은 설치 후 &lt;code&gt;/opt/gitlab/version-manifest.txt&lt;/code&gt;에서 확인된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cloud-native&lt;/b&gt; &amp;mdash; 쿠버네티스 차트로 부품을 파드 여럿에 흩는 형태.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우리가 받아 돌리는 &lt;code&gt;gitlab/gitlab-ce&lt;/code&gt; 이미지가 omnibus 패키지를 담은 것&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;묶는 층으로 보면 중간이다. k3s는 컨트롤플레인 넷을 한 프로세스로 합쳐 하나가 죽으면 다 죽지만, omnibus는 프로세스가 각자라 sidekiq이 죽어도 puma는 산다. 그렇다고 파드로 흩은 Mimir처럼 부품별로 자원을 제한할 수는 없다 &amp;mdash; &lt;b&gt;컨테이너가 하나라 limit도 하나&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: 191px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px; width: 42px;&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th style=&quot;height: 23px; width: 285px;&quot;&gt;부품을 직접 세운다면&lt;/th&gt;
&lt;th style=&quot;height: 23px; width: 528px;&quot;&gt;omnibus 패키지면&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px; width: 42px;&quot;&gt;&lt;b&gt;설치&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 285px;&quot;&gt;nginx&amp;middot;PostgreSQL&amp;middot;Redis&amp;hellip; 여덟을 각각 깔고 버전 궁합을 맞춘다&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 528px;&quot;&gt;패키지 하나에 검증된 조합으로 담겨 온다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px; width: 42px;&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 285px;&quot;&gt;부품마다 설정 문법이 달라 파일을 여덟 벌 쓴다&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 528px;&quot;&gt;&lt;b&gt;&lt;code&gt;gitlab.rb&lt;/code&gt;&lt;/b&gt; 한 장에 쓰면 &lt;b&gt;&lt;code&gt;gitlab-ctl reconfigure&lt;/code&gt;&lt;/b&gt; 가 부품별 파일(nginx 설정&amp;middot;postgresql.conf&amp;middot;puma 설정&amp;hellip;)로 펼친다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px; width: 42px;&quot;&gt;&lt;b&gt;기동&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 285px;&quot;&gt;부품마다 서비스로 등록하고 순서를 잡는다&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 528px;&quot;&gt;&lt;b&gt;runit&lt;/b&gt;이 전부 띄우고, 죽은 것만 다시 세운다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px; width: 42px;&quot;&gt;&lt;b&gt;조작&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 285px;&quot;&gt;부품마다 start&amp;middot;stop&amp;middot;status 명령이 다르다&lt;/td&gt;
&lt;td style=&quot;height: 42px; width: 528px;&quot;&gt;&lt;b&gt;&lt;code&gt;gitlab-ctl&lt;/code&gt;&lt;/b&gt; 하나로 GitLab 전체를 세우고 멈추고 본다&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;오른쪽 열의 이름 넷은 설정 &amp;rarr; 기동 &amp;rarr; 조작 순으로 이렇다.&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;gitlab.rb&lt;/code&gt;&lt;/b&gt; &amp;mdash; 설정 파일 하나. Ruby 문법이라 &lt;code&gt;puma['worker_processes'] = 0&lt;/code&gt; 같은 줄을 쓴다. Docker에서는 이 파일 대신 환경변수 &lt;code&gt;GITLAB_OMNIBUS_CONFIG&lt;/code&gt;에 같은 내용을 적는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;gitlab-ctl reconfigure&lt;/code&gt;&lt;/b&gt; &amp;mdash; &lt;code&gt;gitlab.rb&lt;/code&gt; 한 장을 읽어 부품별 설정 파일을 만들어 내고, 내용이 바뀐 부품만 다시 세운다. 패키지에 함께 담긴 구성 관리 도구(Chef)가 이 일을 한다. 몇 번을 돌려도 결과가 같고, 부품 설정 파일을 직접 고쳐 두면 다음 reconfigure가 덮어쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;runit&lt;/b&gt; &amp;mdash; 부품들을 프로세스로 띄워 두고 지켜보는 프로그램(프로세스 슈퍼바이저). 죽으면 다시 세운다. 보통 리눅스 서버에서 systemd가 하는 일을, 컨테이너 안에서 이 프로그램이 대신한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;gitlab-ctl&lt;/code&gt;&lt;/b&gt; &amp;mdash; GitLab을 다루는 명령이고, 위의 reconfigure도 그 하위 하나다. 나머지는 &lt;code&gt;status&lt;/code&gt;(부품별 생사), &lt;code&gt;start&lt;/code&gt;&amp;middot;&lt;code&gt;stop&lt;/code&gt;&amp;middot;&lt;code&gt;restart&lt;/code&gt;, &lt;code&gt;tail&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;설치되면 이 파일들이 성격에 따라 네 경로로 나뉜다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;경로&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;담기는 것&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;/opt/gitlab&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;프로그램 본체 &amp;mdash; 부품들의 실행 파일과 &lt;code&gt;gitlab-ctl&lt;/code&gt;. 담긴 목록인 &lt;code&gt;version-manifest.txt&lt;/code&gt;도 여기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;/etc/gitlab&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;설정 &amp;mdash; &lt;code&gt;gitlab.rb&lt;/code&gt;와 인증서&amp;middot;비밀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;/var/opt/gitlab&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;데이터 &amp;mdash; gitaly가 드는 git 저장소와 PostgreSQL 실물&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;/var/log/gitlab&lt;/code&gt;&lt;/b&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;&lt;b&gt;바뀌는 것과 안 바뀌는 것이 갈려 있다.&lt;/b&gt; 프로그램 본체는 패키지가 정하니 손댈 일이 없고, 나머지 셋은 쓰는 동안 계속 바뀐다. 이 구분이 그대로 기동 파일의 볼륨이 된다.&lt;/p&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;&lt;b&gt;3.4 어디에 세우나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자리는 &lt;b&gt;데스크탑의 Docker&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;b&gt;① 정본은 자기가 정의하는 대상 밖에 있어야 한다.&lt;/b&gt; 대상이 무너졌을 때 다시 세울 근거가 정본인데, 대상 안에 살면 함께 무너진다. 이 축이 클러스터 안(차트로 배포)과 클러스터 호스트인 노트북(VM 추가)을 지운다. 데스크탑은 물리적으로 분리된 장비다.&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;② webhook은 밖에서 안으로 들어오는 연결이다.&lt;/b&gt; ArgoCD가 저장소를 읽는 것은 안에서 밖으로 나가는 연결이라 GitLab이 어디 있든 된다 &amp;mdash; 지금 인터넷의 GitHub를 읽는 것이 그 증거다. webhook은 반대로 GitLab이 ArgoCD를 부른다. 클러스터는 공유기 NAT 뒤 사설 IP(192.168.0.201&amp;ndash;203)에 있어 인터넷에서 먼저 거는 연결이 닿지 않고, 데스크탑(192.168.0.167)은 같은 사설망이라 그 호출이 그대로 선다. GitHub로 걸려면 포트포워딩으로 클러스터를 인터넷에 내놓아야 하는데, 그것은 외부 공개(14부)의 일이다.&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;③ 빌드 I/O를 클러스터 디스크에서 뗀다.&lt;/b&gt; 클러스터 호스트인 노트북은 단일 USB SSD라 CI 빌드의 I/O가 부하 실측(13부)에 섞인다. 10부에서 러너가 붙으면 그 부하는 데스크탑 디스크에서 돈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데스크탑에 쿠버네티스를 새로 세워 차트로 올리는 길도 있었지만, Git 서버 하나를 위해 관리할 클러스터가 하나 더 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대가 &amp;mdash; 데스크탑 전원에 매인다.&lt;/b&gt; 꺼지면 sync가 멈춘다. 뜬 워크로드는 그대로 돌고, 켜면 ArgoCD가 따라잡는다.&lt;/p&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;&lt;b&gt;3.5 기동 파일 &amp;mdash; 컨테이너를 정의하는 한 장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자리가 정해졌으니 파일을 쓴다. 받아 올 것은 omnibus 패키지를 담은 공식 이미지고, 이 파일이 정하는 것은 그 이미지로 만들 컨테이너의 모양이다 &amp;mdash; 어떤 주소로 서고, 어느 포트를 잇고, 무엇을 남기고, 어느 버전을 쓰고, 메모리를 어디까지 줄일지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파일은 데스크탑 로컬 폴더에 둔다. cgv-infra 저장소에 넣으면 버전 관리가 따라오지만, 그 저장소는 클러스터를 정의하는 코드이기에 경계를 지킨다 &amp;mdash; 클러스터 밖 장비의 파일을 섞지 않는다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: start;&quot;&gt;&lt;code&gt;services:
  gitlab:
    image: gitlab/gitlab-ce:19.2.1-ce.0
    container_name: gitlab
    restart: unless-stopped
    shm_size: '256m'    # 공유 메모리(/dev/shm) &amp;mdash; 도커 기본 64m로는 내부 프로세스가 모자란다
    ports:
      - '8929:8929'     # HTTP
      - '2424:22'       # SSH &amp;mdash; 밖 2424를 안의 22로
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://192.168.0.167:8929'
        gitlab_rails['gitlab_shell_ssh_port'] = 2424
        gitlab_rails['time_zone'] = 'Asia/Seoul'
        puma['worker_processes'] = 0            # single 모드 &amp;mdash; 메모리 절감
        sidekiq['max_concurrency'] = 10         # 뒷일 동시성도 낮춤
        prometheus_monitoring['enable'] = false # 자기 관측 exporter 끔
        gitlab_kas['enable'] = false            # 클러스터 안 에이전트가 GitLab에 붙어 배포하는
                                                # GitLab 자체 GitOps 경로. ArgoCD가 그 자리라 안 씀
    volumes:
      - gitlab-config:/etc/gitlab
      - gitlab-logs:/var/log/gitlab
      - gitlab-data:/var/opt/gitlab
volumes:
  gitlab-config:
  gitlab-logs:
  gitlab-data:&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 파일에는 층이 둘 섞여 있다.&lt;/b&gt;&lt;span&gt; &lt;/span&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;바깥쪽(image&amp;middot;ports&amp;middot;volumes&amp;middot;shm_size)은 도커가 읽어 컨테이너를 만드는 값&lt;/b&gt;이고,&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GITLAB_OMNIBUS_CONFIG&amp;nbsp;블록&lt;/b&gt;은 컨테이너 안에서&lt;b&gt;&amp;nbsp;gitlab.rb&amp;nbsp;자리에 들어가 GitLab이 읽는 값&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;컨테이너가 처음 뜰 때 reconfigure가 이 블록을 부품별 설정 파일로 펼친다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GITLAB_OMNIBUS_CONFIG&amp;nbsp;블록&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;external_url&lt;/b&gt;부터. GitLab은 자기 주소를 알아야 한다. 화면의 clone 주소, 링크&amp;middot;리다이렉트(브라우저를 다른 주소로 넘기는 응답 &amp;mdash; 로그인 뒤 이동이 이걸로 돈다), 그리고 webhook을 발사하는 sidekiq(비동기 잡 워커)이 밝힐 발신처가 전부 이 값에서 나온다. 값에 포트를 붙이면 안의 nginx가 그 포트로 listen한다.&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;포트 매핑은 HTTP와 SSH가 비대칭이다.&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;컨테이너 안 listen&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;HTTP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;8929&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;8929:8929&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;external_url의 포트가 곧 nginx listen 포트&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SSH&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;22&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;2424:22&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;gitlab_shell_ssh_port&lt;/code&gt;는 clone 주소에 표시할 번호만 바꾼다&lt;/b&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;80&amp;middot;22를 그대로 안 쓰는 이유는 데스크탑의 다른 프로그램과 충돌 여지다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;볼륨은 셋이다.&lt;/b&gt; 설치 경로 넷 중 프로그램 본체(&lt;code&gt;/opt/gitlab&lt;/code&gt;)는 이미지 안에 그대로 들어 있어 남길 필요가 없다. 나머지 셋 &amp;mdash; 설정 &lt;code&gt;/etc/gitlab&lt;/code&gt;, 데이터 &lt;code&gt;/var/opt/gitlab&lt;/code&gt;, 로그 &lt;code&gt;/var/log/gitlab&lt;/code&gt; &amp;mdash; 은 쓰는 동안 계속 바뀌고 그 안에 운영의 원본이 있으므로, 컨테이너가 죽어도 남아야 한다. 컨테이너를 지웠다 다시 만들어도 이 셋이 살아 있으면 저장소도 계정도 그대로다.&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;code&gt;latest&lt;/code&gt; 대신 &lt;code&gt;19.2.1-ce.0&lt;/code&gt;을 쓴다. GitLab은 업그레이드 경로 규칙이 엄격해서(마이너를 건너뛰지 못하는 구간이 있다) 지금 무엇을 돌리는지 모르면 다음 업그레이드가 도박이 된다.&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;도 이 파일에 들어간다. GitLab의 기본 설정은 수백 명 규모 서버를 전제해(공식 최소 권장 RAM 4GB) 유휴에도 4GB급을 흡수하는데, 도커의 memory limit은 컨테이너 전체에 하나라 안의 부품별로는 못 건다. 조절은 &lt;code&gt;gitlab.rb&lt;/code&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;puma worker 수&lt;/b&gt; &amp;mdash; 동시 처리량과 맞바꿔 메모리를 크게 줄인다. 사용자가 하나라 성립한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sidekiq 동시성&lt;/b&gt; &amp;mdash; 같은 논리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내장 모니터링&lt;/b&gt; &amp;mdash; 패키지에 함께 담긴 exporter가 자기 지표를 따로 내는데, 같은 대상을 LGTM이 이미 본다. 켜 두면 같은 값을 두 곳에서 재면서 메모리만 더 쓴다.&lt;/li&gt;
&lt;/ul&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;&lt;b&gt;3.6 기동과 확인&lt;/b&gt;&lt;/h4&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;[PowerShell] &lt;code&gt;docker version&lt;/code&gt; &amp;mdash; 클라이언트&amp;middot;서버 둘 다 나와야 한다. 명령이 인식되지 않으면 설치 전에 열어 둔 세션이므로 터미널을 새로 띄운다.&lt;/li&gt;
&lt;li&gt;[PowerShell] 기동 파일을 둔 폴더에서 &lt;code&gt;docker compose up -d&lt;/code&gt; &amp;mdash; 첫 실행은 이미지 수 GB를 받는다.&lt;/li&gt;
&lt;li&gt;[PowerShell] &lt;code&gt;docker logs -f gitlab&lt;/code&gt; &amp;mdash; 첫 기동은 reconfigure가 돌아 수 분 걸린다.&lt;/li&gt;
&lt;li&gt;[PowerShell] &lt;code&gt;docker ps&lt;/code&gt;로 STATUS가 &lt;code&gt;(healthy)&lt;/code&gt;가 될 때까지 기다린 뒤 [브라우저] &lt;code&gt;http://192.168.0.167:8929&lt;/code&gt;.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1479&quot; data-origin-height=&quot;60&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RPyml/dJMcaa0wCzf/jsYIqwmXFp1jFFvMjDEtQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RPyml/dJMcaa0wCzf/jsYIqwmXFp1jFFvMjDEtQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RPyml/dJMcaa0wCzf/jsYIqwmXFp1jFFvMjDEtQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRPyml%2FdJMcaa0wCzf%2FjsYIqwmXFp1jFFvMjDEtQk%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;2015&quot; height=&quot;82&quot; data-origin-width=&quot;1479&quot; data-origin-height=&quot;60&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;(healthy)&lt;/code&gt;는 이미지에 정의된 헬스체크 명령이 통과했다는 표시다(그 전에는 &lt;code&gt;starting&lt;/code&gt;). 다만 이 체크는 HTTP 응답 한 곳만 보므로, 안의 부품 하나가 죽어 있어도 healthy로 보일 수 있다. 그래서 컨테이너 안은 따로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmEuT6/dJMcadXgEUO/irW4m2bKCvvc08q2uny54k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmEuT6/dJMcadXgEUO/irW4m2bKCvvc08q2uny54k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmEuT6/dJMcadXgEUO/irW4m2bKCvvc08q2uny54k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmEuT6%2FdJMcadXgEUO%2FirW4m2bKCvvc08q2uny54k%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;674&quot; height=&quot;280&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부품 아홉이 전부 &lt;code&gt;run&lt;/code&gt; 상태다. &lt;code&gt;gitlab-shell&lt;/code&gt;이 목록에 없는 것은 상주 데몬이 아니어서다 &amp;mdash; SSH 연결이 올 때마다 sshd가 그때 호출한다. &lt;code&gt;logrotate&lt;/code&gt;는 로그 파일을 잘라 보관하는 유틸로, 패키지에 함께 담겨 온 것이다. &lt;code&gt;down:&lt;/code&gt;이 찍힌 부품이 있으면 &lt;code&gt;gitlab-ctl tail &amp;lt;부품&amp;gt;&lt;/code&gt;으로 그 로그를 본다(실물은 &lt;code&gt;/var/log/gitlab/&amp;lt;부품&amp;gt;/&lt;/code&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;데이터 계층(redis&amp;middot;postgresql) &amp;rarr; 앱(puma&amp;middot;sidekiq) &amp;rarr; 전송&amp;middot;입구(workhorse&amp;middot;nginx&amp;middot;gitaly)&lt;/b&gt; 순으로 벌어져 있다. 이 순서를 만드는 것은 reconfigure다 &amp;mdash; reconfigure가 부품을 이 순서로 활성화하고, runit은 활성화된 것을 띄운 뒤 죽으면 다시 세우는 일만 한다(의존 순서 개념이 없다).&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;데스크탑을 껐다 켜도 손댈 것은 없다. Windows 로그인 &amp;rarr; Docker Desktop 자동 시작(Settings &amp;rarr; General에서 켠다) &amp;rarr; &lt;code&gt;restart: unless-stopped&lt;/code&gt;가 컨테이너를 다시 세우고, 볼륨이 컨테이너 밖이라 데이터도 그대로다. GitLab이 내려가 있는 동안은 sync가 멈추지만 뜬 워크로드는 돌고, 켜면 ArgoCD가 따라잡는다.&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;작업 중 Windows가 메모리 압박으로 VS Code와 Docker Desktop 창을 정리했을 때도 GitLab은 계속 돌았다. 창과 엔진이 다른 프로세스이고, 컨테이너는 WSL2 VM 안에서 돌아 호스트 쪽 정리 대상이 아니어서다.&lt;/p&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;&lt;b&gt;3.7 첫 로그인과 잠그기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지에는 비밀번호를 넣을 수 없다 &amp;mdash; 모두가 같은 이미지를 받는다. 그래서 첫 기동 때 무작위 비밀번호를 만들어 컨테이너 안에 파일로 남기고, &lt;b&gt;24시간이 지난 뒤 처음 도는 reconfigure에서 지운다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;docker exec gitlab cat /etc/gitlab/initial_root_password&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;code&gt;Password:&lt;/code&gt; 줄의 값으로 사용자명 &lt;code&gt;root&lt;/code&gt; 로그인. 안내 화면이 둘 뜨는데 &lt;b&gt;둘 다 Skip한다&lt;/b&gt; &amp;mdash; 그룹&amp;middot;프로젝트 마법사는 공개 범위와 README 초기화를 고를 칸이 없어 다음 절에서 직접 만들고, 프로필 설정은 메일 발송(SMTP) 설정이 없어 이메일을 바꿔도 확인 메일이 안 온다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;① 비밀번호 교체&lt;/b&gt; &amp;mdash; &quot;&lt;code&gt;/-/user_settings/password/edit&quot;&lt;/code&gt;&lt;/h5&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; &amp;mdash; 초기값 파일이 다음 재시작에서 지워진다. 그 전에 안 바꾸면 로그인 경로를 컨테이너 안에서 직접 되살려야 한다.&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;② 가입 차단&lt;/b&gt; &amp;mdash; &quot;&lt;code&gt;/admin/application_settings/general&quot;&lt;/code&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;New user account restrictions&lt;/b&gt; &amp;rarr; &lt;code&gt;Allow new user accounts&lt;/code&gt; 해제 &amp;rarr; Save changes. Admin area 위에 뜬 경고 배너의 &lt;b&gt;Deactivate&lt;/b&gt; 버튼도 같은 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜&lt;/b&gt; &amp;mdash; 켜져 있으면 &lt;code&gt;/users/sign_up&lt;/code&gt; 주소를 아는 사람은 누구나 계정을 만든다. 이 서버는 곧 인프라 코드의 원본이 되고 클러스터가 여기서 배포 정의를 읽어 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;끄면 안 되는 것&lt;/b&gt; &amp;mdash; 같은 페이지 Sign-in restrictions의 &lt;code&gt;Allow password authentication for Git over HTTP(S)&lt;/code&gt;. 다음 절의 push가 이 인증을 쓴다.&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;③ Web IDE 단일 출처 대체 끄기&lt;/b&gt; &amp;mdash; 같은 페이지 아래 &lt;b&gt;Web IDE&lt;/b&gt; 섹션&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Enable single origin fallback&lt;/code&gt; 해제 &amp;rarr; Save changes. Admin area의 둘째 경고 배너가 이것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜&lt;/b&gt; &amp;mdash; 웹 편집기의 확장 자산은 본체와 분리된 출처에서 받아오는데, 그 주소가 안 닿으면 GitLab 서버가 대신 내주는 설정이다. 제3자 코드가 이 서버 주소로 서빙되면 브라우저가 같은 출처(프로토콜&amp;middot;호스트&amp;middot;포트로 스크립트 권한을 가르는 기준)로 취급해 이 서버의 쿠키&amp;middot;세션에 닿는다. 코드는 로컬에서 고쳐 push하므로 웹 편집기를 쓸 일이 없다.&lt;/p&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.8 저장소 이전 &amp;mdash; 그릇과 프로토콜&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab은 섰고 비밀번호도 잠갔다. 이제 저장소를 github에서 gitlab으로 옮긴다.&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;GitHub의 &lt;code&gt;사용자/저장소&lt;/code&gt;에 대응하는 GitLab의 단위는 &lt;b&gt;&lt;code&gt;그룹/프로젝트&lt;/code&gt;&lt;/b&gt;다. 그룹은 폴더가 아니라 &lt;b&gt;권한&amp;middot;설정&amp;middot;러너를 물려주는 단위&lt;/b&gt;여서, 그룹에 등록한 러너는 &lt;b&gt;아래 프로젝트 전부가 쓴다&lt;/b&gt;. 그래서 개인 계정 밑이 아니라 그룹 &lt;code&gt;cgv&lt;/code&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;공개 범위는 세 단계다 &amp;mdash; private(멤버만) / internal(로그인한 사용자 전부) / public(로그인 없이도 read). &lt;b&gt;지금은 private로 만든다.&lt;/b&gt; ArgoCD가 자격 없이 읽으려면 public이어야 하지만, 여는 것은 설정 하나고 자격 방식은 다음 절의 [결정]이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;그릇 만들기&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;New group으로 &lt;code&gt;cgv&lt;/code&gt;(private)를 만들고&lt;/b&gt;, 그 안에서&lt;b&gt; New project&lt;/b&gt; &amp;rarr; &lt;b&gt;Create blank project&lt;/b&gt;로 &lt;b&gt;&lt;code&gt;cgv-infra&lt;/code&gt;&amp;middot;&lt;code&gt;cgv-onprem&lt;/code&gt;&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;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;Initialize repository with a README&lt;/b&gt; &amp;mdash; 화면 설명이 그대로 말한다: *&quot;Skip this if you plan to push up an existing repository.&quot;* 체크하면 첫 커밋이 생겨서 밀어 넣을 히스토리와 갈라진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Enable SAST&lt;/b&gt;(Static Application Security Testing) &amp;mdash; 소스를 정적으로 훑어 취약점을 찾는 검사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Enable Secret Detection&lt;/b&gt; &amp;mdash; 둘 다 CI로 도는 검사다. 러너가 없어 돌 수 없고(10부), 켜면 파이프라인 정의 파일인 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;이 커밋으로 들어가 README와 같은 문제가 생긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들고 나면 빈 저장소 안내 화면에 clone 주소가 뜬다 &amp;mdash; &lt;code&gt;ssh://git@192.168.0.167:2424/...&lt;/code&gt;. 기동 파일에 넣은 &lt;code&gt;gitlab_shell_ssh_port = 2424&lt;/code&gt;가 화면에 그대로 반영된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;프로토콜 &amp;mdash; SSH 대신 HTTP&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 저장소에 들어가는 문이 둘이라 안내 화면에도 &lt;code&gt;SSH&lt;/code&gt;&amp;middot;&lt;code&gt;HTTPS&lt;/code&gt; 탭이 나란히 있다. remote 주소의 앞부분이 어느 문으로 들어갈지를 정한다.&lt;/p&gt;
&lt;table style=&quot;height: 135px; width: 851px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;th style=&quot;width: 102px; height: 16px;&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th style=&quot;width: 379px; height: 16px;&quot;&gt;HTTP&lt;/th&gt;
&lt;th style=&quot;width: 370px; height: 16px;&quot;&gt;SSH&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;width: 102px; height: 35px;&quot;&gt;&lt;b&gt;주소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 379px; height: 35px;&quot;&gt;&lt;b&gt;&lt;code&gt;http://192.168.0.167:8929/cgv/cgv-infra.git&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 370px; height: 35px;&quot;&gt;&lt;b&gt;&lt;code&gt;ssh://git@192.168.0.167:2424/cgv/cgv-infra.git&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 102px; height: 21px;&quot;&gt;&lt;b&gt;포트&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 379px; height: 21px;&quot;&gt;&lt;b&gt;8929&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 370px; height: 21px;&quot;&gt;&lt;b&gt;2424&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 102px; height: 21px;&quot;&gt;&lt;b&gt;인증&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 379px; height: 21px;&quot;&gt;&lt;b&gt;사용자명 + 비밀번호(또는 토큰)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 370px; height: 21px;&quot;&gt;&lt;b&gt;공개키를 서버에 미리 등록&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 102px; height: 21px;&quot;&gt;&lt;b&gt;받는 부품&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 379px; height: 21px;&quot;&gt;&lt;b&gt;nginx &amp;rarr; workhorse&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 370px; height: 21px;&quot;&gt;&lt;b&gt;sshd &amp;rarr; gitlab-shell&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 102px; height: 21px;&quot;&gt;&lt;b&gt;전송&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 379px; height: 21px;&quot;&gt;&lt;b&gt;평문(HTTPS면 암호화)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 370px; height: 21px;&quot;&gt;&lt;b&gt;항상 암호화&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;remote 재배선&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소마다 세 줄이다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git remote rename origin github
git remote add origin http://192.168.0.167:8929/cgv/cgv-infra.git
git push -u origin main&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xyVUk/dJMcafgwIj0/6wIt01sJCSdFcjmE3tTPOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xyVUk/dJMcafgwIj0/6wIt01sJCSdFcjmE3tTPOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xyVUk/dJMcafgwIj0/6wIt01sJCSdFcjmE3tTPOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxyVUk%2FdJMcafgwIj0%2F6wIt01sJCSdFcjmE3tTPOK%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;446&quot; height=&quot;99&quot; data-origin-width=&quot;446&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;origin = GitLab.&lt;/b&gt; GitHub는 &lt;code&gt;github&lt;/code&gt;라는 이름으로 남기고 직접 push하지 않는다.&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;첫 push는 자격 단계에서 막혔다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;fatal: Unencrypted HTTP is not recommended for GitLab.
Ensure the repository remote URL is using HTTPS ...&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;GitLab이 아니라 &lt;b&gt;Windows의 Git 자격 관리자&lt;/b&gt;(Git for Windows에 함께 깔려 저장된 자격을 git에 넣어 주는 도구)&lt;b&gt;가 거부한 것&lt;/b&gt;이다. 평문으로는 자격이 그대로 흘러서다. 이 서버는 공유기 망 안에 있고 인터넷에 열려 있지 않아 그 위험을 수용하되, &lt;b&gt;전역 허용 대신 이 주소에만 예외&lt;/b&gt;를 준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global &quot;credential.http://192.168.0.167:8929.allowUnsafeRemotes&quot; true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS는 클러스터를 밖으로 열 때(14부) 인증서와 함께 들어온다. 그때 이 예외도 걷힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S1qul/dJMcacYl5K4/6kRCKRKafS2G01Xsnaq53k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S1qul/dJMcacYl5K4/6kRCKRKafS2G01Xsnaq53k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S1qul/dJMcacYl5K4/6kRCKRKafS2G01Xsnaq53k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS1qul%2FdJMcacYl5K4%2F6kRCKRKafS2G01Xsnaq53k%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;436&quot; height=&quot;369&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외를 준 뒤 다시 push하면 자격 창이 뜬다. 기본이 &lt;b&gt;Token 탭&lt;/b&gt;인데 개인 액세스 토큰을 아직 안 만들었으므로 &lt;b&gt;Password 탭&lt;/b&gt;으로 바꿔 &lt;code&gt;root&lt;/code&gt; + 새 비밀번호를 넣는다. 같이 뜨는 &lt;code&gt;missing OAuth configuration&lt;/code&gt; 경고는 자격 관리자가 비밀번호 대신 브라우저 로그인으로 자격을 받아 오는 경로(OAuth)를 쓰려 했는데 이 서버에 그 앱 등록이 없다는 안내라, 비밀번호로 넣으면 무관하다. 한 번 인증하면 Windows 자격 관리자가 저장해 다음부터는 안 묻는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h5&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GKc81/dJMcadbNfC3/tfg56hoPXRXV5rhZiof9n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GKc81/dJMcadbNfC3/tfg56hoPXRXV5rhZiof9n0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GKc81/dJMcadbNfC3/tfg56hoPXRXV5rhZiof9n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGKc81%2FdJMcadbNfC3%2Ftfg56hoPXRXV5rhZiof9n0%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;781&quot; height=&quot;211&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&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;535 오브젝트&lt;/b&gt;(커밋&amp;middot;디렉터리&amp;middot;파일 하나하나가 오브젝트다) &amp;mdash; 받는 쪽이 빈 저장소라 겹치는 것이 없어 히스토리 전부가 통째로 전송됐다. 빈 그릇으로 만든 이유가 이것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Resolving deltas&lt;/code&gt;&lt;/b&gt; &amp;mdash; 보내는 쪽은 오브젝트를 통째로가 아니라 이전 버전과의 차이(delta)로 압축해 보내고, 받는 쪽이 그걸 원본으로 되살린다. 그 일을 한 부품이 gitaly다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;set up to track 'origin/main'&lt;/code&gt;&lt;/b&gt; &amp;mdash; 이 저장소의 기본 push 대상이 GitLab이 됐다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cgv-onprem&lt;/code&gt;도 같은 세 줄로 옮긴다(주소만 바꿔서). 자격은 이미 저장돼 있어 묻지 않는다.&amp;nbsp;&lt;/p&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.9 GitHub는 쇼케이스로 &amp;mdash; push 미러&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;repository mirroring(push 방향)은 GitLab이 push를 받으면 그 내용을 다른 저장소로 밀어 주는 기능&lt;/b&gt;이다. 미는 일은 응답과 분리된 뒷일이라 &lt;b&gt;sidekiq(미러링, 웹훅 전문)&lt;/b&gt;이 큐에서 꺼내 처리하고, 그래서 GitHub에 뜨는 시점은 push 시점과 조금 어긋난다. 미러는 마지막 push까지만 복제한다 &amp;mdash; 백업과 같지 않고, 강제 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;[GitHub] 토큰 발급:&lt;/b&gt; Settings &amp;rarr; Developer settings &amp;rarr; Personal access tokens(비밀번호 대신 쓰는 접근 자격) &amp;rarr; &lt;b&gt;Fine-grained tokens&lt;/b&gt; &amp;mdash; 대상 저장소와 권한 항목을 하나씩 좁힐 수 있는 쪽이다. 저장소를 &lt;code&gt;cgv-infra&lt;/code&gt;&amp;middot;&lt;code&gt;cgv-onprem&lt;/code&gt; 둘로 한정하고, 권한은 &lt;b&gt;Contents: Read and write&lt;/b&gt; 하나만 준다. 이 토큰은 GitLab 서버에 저장되므로, 새어 나갔을 때의 범위가 곧 이 권한 범위다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[GitLab] &lt;/b&gt;&lt;code&gt;&lt;b&gt;cgv-infra&lt;/b&gt;&lt;/code&gt; &amp;rarr; Settings &amp;rarr; Repository &amp;rarr; &lt;b&gt;Mirroring repositories&lt;/b&gt;. URL은 &lt;code&gt;https://github.com/&amp;lt;계정&amp;gt;/cgv-infra.git&lt;/code&gt;로 넣는다 &amp;mdash; &lt;b&gt;사용자명을 URL에 넣지 않는다&lt;/b&gt;(화면 안내가 그렇게 말한다). 사용자명은 Username 칸에, 토큰은 Password 칸에, Direction은 &lt;b&gt;Push&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;Keep divergent refs&lt;/b&gt; &amp;mdash; GitHub 쪽이 갈라졌을 때 덮어쓰지 않고 남기는 옵션이다. 끄면 GitLab이 자기 것으로 덮어쓴다. 이 항목은 미러를 만든 뒤에는 API로만 바꿀 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Mirror only protected branches&lt;/b&gt; &amp;mdash; 보호된 브랜치만 밀지 여부. 작업 브랜치가 생겨도 사본에 같이 보이는 편이 쇼케이스 목적에 맞다.&lt;/li&gt;
&lt;/ul&gt;
&lt;code&gt;cgv-onprem&lt;/code&gt;도 동일하게(주소만 바꿔서, 토큰은 같은 값).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증&lt;/b&gt; &amp;mdash; 커밋 하나를 push하고, 미러 상태(Last successful update)와 GitHub의 커밋 목록이 따라왔는지 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bb22v/dJMcahFjB7P/suLTVOH3DjsZOYxJilYr91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bb22v/dJMcahFjB7P/suLTVOH3DjsZOYxJilYr91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bb22v/dJMcahFjB7P/suLTVOH3DjsZOYxJilYr91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBb22v%2FdJMcahFjB7P%2FsuLTVOH3DjsZOYxJilYr91%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;858&quot; height=&quot;275&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8u6Dn/dJMcaidhSpr/kMMfkHopZiCZdpQSc4tfPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8u6Dn/dJMcaidhSpr/kMMfkHopZiCZdpQSc4tfPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8u6Dn/dJMcaidhSpr/kMMfkHopZiCZdpQSc4tfPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8u6Dn%2FdJMcaidhSpr%2FkMMfkHopZiCZdpQSc4tfPK%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;851&quot; height=&quot;211&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;211&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 data-ke-size=&quot;size16&quot;&gt;여기까지가 설치다. sidekiq이 맡는 뒷일 중 지금 도는 것은 미러 하나고, webhook은 아직 등록하지 않았다. 저장소가 옮겨졌을 뿐 ArgoCD는 여전히 GitHub를 본다 &amp;mdash; 소스를 갈아타고 webhook을 거는 것은 다다음 절에서 진행하려한다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. GitLab의 층과 기능&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버는 섰고 저장소 둘이 그 안에 있다. 남은 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이 제품으로 무엇을 할 수 있고 그중 무엇을 쓸지&lt;/b&gt;다. 메뉴가 방대해서 정해 두지 않으면 화면에 끌려다니게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왼쪽 사이드바는 고정돼 있지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지금 보고 있는 컨텍스트를 따라 통째로 바뀐다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;덮는 범위가 넓은 것부터 좁은 것까지 넷이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;4.1 Admin area   이 서버 전체                       상단바 오른쪽 Admin 버튼
4.2 Your work    그중 내가 걸친 것만 &quot;나&quot; 기준으로     로그인 첫 화면 &amp;middot; 로고 클릭
4.3 그룹 cgv     걸면 아래 프로젝트가 전부 물려받는다   Your work &amp;rarr; Groups &amp;rarr; cgv
4.4 프로젝트      저장소 하나                        그룹 목록에서 클릭&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 Admin area &amp;mdash; 이 서버 전체&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1667&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LqkBH/dJMb99UQ4YF/DhsNl2Cvp3MwH4zvHxOqW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LqkBH/dJMb99UQ4YF/DhsNl2Cvp3MwH4zvHxOqW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LqkBH/dJMb99UQ4YF/DhsNl2Cvp3MwH4zvHxOqW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLqkBH%2FdJMb99UQ4YF%2FDhsNl2Cvp3MwH4zvHxOqW1%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;1667&quot; height=&quot;804&quot; data-origin-width=&quot;1667&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그룹&amp;middot;프로젝트&lt;/b&gt;가&lt;b&gt; &quot;무엇을 만드나&quot;&lt;/b&gt;라면 여기는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;이 서버를 어떻게 굴리나&quot;&lt;/b&gt;다. 아래 층들은 자기 것만 보이는데, 여기서만 소유자와 무관하게 서버에 있는 전부가 보인다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Overview    프로젝트 &amp;middot; 사용자 &amp;middot; 그룹을 전수로
CI/CD       서버 전체가 쓰는 러너 등록
Monitoring  서버 자신의 상태 &amp;mdash; 자원 &amp;middot; 뒷일 &amp;middot; DB 점검
Settings    서버 동작 &amp;mdash; 가입 제한 &amp;middot; 네트워크 &amp;middot; 외관&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 층이 있는 이유는 &quot;누구의 것도 아닌 결정&quot;이 생기기 때문이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;계정을 만들 수 있는지, 서버가 밖으로 요청을 보내도 되는지, 러너를 누가 공유하는지 &amp;mdash; 프로젝트 주인도 그룹 주인도 정할 수 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitLab.com에서는 그 자리가 GitLab이라는 회사이고, 셀프호스팅을 고르면 나에게 온다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 편에서 세 번 온다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가입 차단과 Web IDE 끄기는 설치할 때 했고, 남은 하나는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;webhook이 사설망으로 나가게 허용할 때&lt;/b&gt;(Settings &amp;rarr; Network &amp;rarr; Outbound requests)다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;무료판&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CE&lt;/b&gt;라는 것도 이 층에서 보인다. 유료판(&lt;b&gt;Premium &amp;middot; Ultimate&lt;/b&gt;) 이름이 붙은 기능은 이 서버에서 안 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 Your work &amp;mdash; 내가 걸친 것 전부&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1657&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dn9BkR/dJMcafOpWYN/tmEj2YHykO2S625iUQ9Hs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dn9BkR/dJMcafOpWYN/tmEj2YHykO2S625iUQ9Hs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dn9BkR/dJMcafOpWYN/tmEj2YHykO2S625iUQ9Hs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdn9BkR%2FdJMcafOpWYN%2FtmEj2YHykO2S625iUQ9Hs0%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;1657&quot; height=&quot;590&quot; data-origin-width=&quot;1657&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인하면 서는 첫 화면이다. 특정 서버&amp;middot;그룹&amp;middot;프로젝트를 보는 곳이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내가 닿는 칸 전부를 소속과 무관하게 한 목록으로 펴 놓은 곳&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 층은 층 구조의 대가를 갚는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로젝트와 그룹으로 잘게 나누면 권한과 범위는 깔끔해지는데, 사람은 그 여러 칸에 걸쳐 일한다. 칸마다 찾아다니면 내 차례인 일을 놓친다. 그래서 나눠 놓은 것을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;나&quot;라는 축으로 다시 펴 보는 화면&lt;/b&gt;이 따로 있고, 그중 &quot;내 차례&quot;만 추린 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;To-Do List다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지금은 쓸 것이 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;개수가 전부 0이다 &amp;mdash; 칸이 둘뿐이고 걸쳐 있는 사람이 하나라 값이 안 나온다. 다만 혼자라서 원리적으로 안 쌓이는 것은 아니다. 파이프라인 실패나 내가 저자인 MR의 충돌은 남이 없어도 발화한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;첫 MR이 서고 10부에서 러너가 붙으면 채워지기 시작한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.3 그룹 cgv &amp;mdash; 아래로 물려주는 단위&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;571&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWLhwv/dJMcajb2Rgp/XtQmLOaTVgPJ7419UgfK4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWLhwv/dJMcajb2Rgp/XtQmLOaTVgPJ7419UgfK4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWLhwv/dJMcajb2Rgp/XtQmLOaTVgPJ7419UgfK4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWLhwv%2FdJMcajb2Rgp%2FXtQmLOaTVgPJ7419UgfK4k%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;1668&quot; height=&quot;571&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;571&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장소를 옮기며 만든 그릇이다. GitHub의&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자/저장소에 대응하는 GitLab 단위가&lt;span&gt;&amp;nbsp;&lt;/span&gt;그룹/프로젝트라, 개인 계정 밑이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;cgv&lt;span&gt;&amp;nbsp;&lt;/span&gt;그룹에 저장소 둘을 담았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상위 층인데 메뉴가 프로젝트보다 적다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로젝트에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Secure&amp;middot;Monitor&amp;middot;Analyze가 여기엔 아예 없고, 남은 묶음도 안이 얇다. 이유는 하나다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그룹은 코드를 담지 않는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;저장소가 프로젝트에 있으니&lt;span&gt;&amp;nbsp;&lt;/span&gt;Code에는 여러 프로젝트에 걸친 MR 목록만 남고, 파이프라인이 도는 곳도 프로젝트라&lt;span&gt;&amp;nbsp;&lt;/span&gt;Build에는 러너를 등록하는 자리만 남는다. 스캔&amp;middot;관측&amp;middot;분석은 코드 실물에 붙는 것이라 자리가 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 남는 것이 곧 이 층의 정의다 &amp;mdash; 아래로 물려줄 것들.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;멤버 권한 &amp;middot; 러너 &amp;middot; CI/CD 변수 &amp;middot; 액세스 토큰 &amp;middot; 패키지 레지스트리&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;팀에서 입사자를 프로젝트가 아니라 팀 그룹에 넣는 이유가 이것이고, 퇴사도 그룹에서 한 번 빼면 하위 접근이 한꺼번에 끊긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;눈여겨볼 것 둘&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Settings에 Webhooks가 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그룹 webhook은 유료판 전용이라 메뉴 자체가 나타나지 않는다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유료 기능은 잠긴 채 보이는 게 아니라 사라진다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그래서 webhook은 프로젝트마다 건다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Build에 Runners가 독립 메뉴로 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로젝트에서는 Settings 안에 묻혀 있는 것이 여기서는 묶음 하나를 통째로 차지한다 &amp;mdash; 러너가 이 층의 주 용도라는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.4 프로젝트 cgv-infra &amp;middot; cgv-onprem &amp;mdash; 저장소가 사는 곳&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o1jiN/dJMcadv5BEJ/i1EbnYIDuCJ1YH0pmhDhA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1jiN/dJMcadv5BEJ/i1EbnYIDuCJ1YH0pmhDhA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1jiN/dJMcadv5BEJ/i1EbnYIDuCJ1YH0pmhDhA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1jiN%2FdJMcadv5BEJ%2Fi1EbnYIDuCJ1YH0pmhDhA0%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;1684&quot; height=&quot;708&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;뒤이어 손대는 작업 대부분이 이 층에서 일어난다. 묶음이 일곱에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;아홉으로 늘고&lt;/b&gt;, 가운데가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;계획 &amp;rarr; 코드 &amp;rarr; 빌드 &amp;rarr; 검사 &amp;rarr; 배포 &amp;rarr; 운영 &amp;rarr; 관측 &amp;rarr; 분석&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;순으로 늘어선다. GitLab이 자신을 Git 호스팅이 아니라 &quot;DevOps 플랫폼&quot;이라 부르는 근거가 이 배열이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;그룹에도 있던 것    Manage &amp;middot; Plan &amp;middot; Deploy &amp;middot; Operate &amp;middot; Settings

여기서 처음        Code      저장소 실물 &amp;mdash; 파일 &amp;middot; 브랜치 &amp;middot; 커밋 &amp;middot; 태그 &amp;middot; 두 지점 비교
                 Build     파이프라인을 정의하고 돌리고 결과를 본다
                 Secure    이 코드에 보안 스캔을 건다
                 Monitor   이 코드가 낸 에러와 알림
                 Analyze   파이프라인 성공률 &amp;middot; 기여자 &amp;middot; 코드 통계&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;새로 생긴 다섯이 전부 한 가지에서 나온다 &amp;mdash; 코드가 여기 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;거꾸로 그룹에만 있는 것(공유 러너&amp;middot;외부 이미지 캐시&amp;middot;멤버 배지)은 전부 여러 프로젝트에 걸치는 것들이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 두 층은 상하 관계이면서 역할이 갈린다 &amp;mdash; 그룹은 물려주고, 프로젝트는 만든다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;눈여겨볼 것&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Auto DevOps enabled가 켜져 있다. 저장소에&lt;span&gt;&amp;nbsp;&lt;/span&gt;.gitlab-ci.yml이 없을 때 GitLab이 준비된 기본 파이프라인을 대신 돌리는 기능인데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;러너가 없어 지금은 아무 일도 하지 않는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;10부에서 러너를 붙이는 순간&lt;span&gt;&amp;nbsp;&lt;/span&gt;cgv-onprem에서 스스로 돌기 시작하므로, 그때는 러너보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;.gitlab-ci.yml이 먼저인 순서가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내가 쓸 것&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Code의 Merge requests&amp;middot;Branches,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Settings &amp;rarr; Repository의 보호 브랜치와 미러,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Settings &amp;rarr; Webhooks.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5. GitLab을 운영 원본으로&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ArgoCD의 repoURL은 아직 GitHub를 가리킨다. GitLab에 push해도 클러스터에는 아무 일도 일어나지 않는다. 이 절이 끝나면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD가 GitLab을 원본으로 본다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;push ─▶ GitLab ─▶ ArgoCD ─▶ 클러스터
          └─ 미러 ─▶ GitHub          (쇼케이스)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;순서가 서로를 문다. 자격 없이 주소만 바꾸면 App 열아홉이 한꺼번에 저장소를 못 읽는다. 길 내기가 맨 앞인 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;repoURL을 바꾸는 커밋이 그 길로 지나가는 첫 손님&lt;/b&gt;이어서다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;길을 낸다
  5.1 변경이 지날 길     브랜치 &amp;middot; main 보호 &amp;middot; MR 문턱
  5.2 합치는 방식        대상 브랜치에 무엇이 남나

옮긴다
  5.3 기계의 자격        ArgoCD가 저장소를 읽을 자격      [결정]
  5.4 시선 전환          repoURL 을 한 커밋으로           [결정]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반영이 폴링 3분에 걸려 있는 것과 ArgoCD가&lt;span&gt;&amp;nbsp;&lt;/span&gt;port-forward로만 열리는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네트워크 쪽 문제&lt;/b&gt;라 6장에서 따로 다룬다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 변경이 지날 길 &amp;mdash; 브랜치와 보호&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브랜치 구조를 정한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cgv-infra는 main 하나로 둔다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;ArgoCD가 보는 브랜치가 곧 &quot;배포된 상태의 정의&quot;인데 클러스터가 하나뿐이라, 브랜치를 둘로 나눠도 나머지 하나를 볼 대상이 없다. 환경 구분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;workloads/environments/{dev,stg,prd}&lt;span&gt;&amp;nbsp;&lt;/span&gt;디렉터리가 이미 하고 있어서, 브랜치에 환경 이름을 또 쓰면 같은 것을 두 축이 가리킨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cgv-onprem에는 dev를 만든다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앱 코드는 ArgoCD가 읽지 않는다. 빌드 대상이라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;브랜치가 곧 이미지 태그를 정하는 축&lt;/b&gt;이 되고(10부), 거기서는 브랜치가 실제 대상을 가리킨다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;git switch -c dev
git push -u origin dev&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;push 응답에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Total 0 (delta 0)이 찍힌다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;커밋은 이미 서버에 있고 이름표(ref) 하나만 새로 만들어져서&lt;/b&gt;, 실제로 전송된 것이 없다는 뜻이다. 같은 응답에 MR을 만들 주소도 함께 오는데, 앞으로 작업 브랜치를 올릴 때마다 이 줄이 뜬다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 브랜치를 dev로 바꾼다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 브랜치가 정하는 것이 넷이다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;clone 하면 체크아웃되는 브랜치
MR 을 만들 때 대상(target)으로 잡히는 브랜치
저장소 첫 화면에 뜨는 브랜치
새로 만든 브랜치에 보호가 자동으로 걸리는 대상&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;작업 흐름이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;작업 브랜치 &amp;rarr; MR &amp;rarr; dev&lt;/b&gt;라 기본이 dev여야 한다. 안 바꾸면 MR마다 대상을 손으로 고쳐야 하고, 한 번 놓치면 main으로 들어간다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitHub 쪽도 같이 바꾼다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; main이 당분간 멈춰 있게 되는데, 쇼케이스의 첫 화면이 그 낡은 상태로 굳는 것을 피한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;GitLab   Settings &amp;rarr; Repository &amp;rarr; Branch defaults &amp;rarr; Default branch
GitHub   Settings &amp;rarr; General &amp;rarr; Default branch &amp;rarr; 연필 아이콘&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;b&gt;보호를 걸고 실제로 밀어 본다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Settings &amp;rarr; Repository &amp;rarr; Protected branches에서 cgv-infra의&lt;span&gt;&amp;nbsp;&lt;/span&gt;main, cgv-onprem의&lt;span&gt;&amp;nbsp;&lt;/span&gt;dev&amp;middot;main에 같은 값을 건다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Allowed to push and merge   No one         브랜치에 직접 쓰는 것을 누가 하나
Allowed to merge            Maintainers    MR 을 이 브랜치로 합치는 것을 누가 하나
Allow force push            꺼짐&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이름이 비슷하지만 거는 대상이 다르고, 둘이 같이 있어야 MR이 유일한 길이 된다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Allowed to push and merge를 역할로 열어 두면 뒤에 붙일 파이프라인 게이트를 직접 push로 우회할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;혼자 쓰는 동안 실제로 일하는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Allowed to push and merge&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나다. root가 Owner라&lt;span&gt;&amp;nbsp;&lt;/span&gt;Allowed to merge에 무엇을 넣어도 결과가 같다 &amp;mdash; 사람이 늘 때를 위해 미리 좁혀 두는 자리다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설정만 보고 넘어가지 않고 빈 커밋 하나로 밀어 본다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;git commit --allow-empty -m &quot;protection test&quot;
git push origin main&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;remote: GitLab: You are not allowed to push code to protected branches on this project.
 ! [remote rejected]      main -&amp;gt; main (pre-receive hook declined)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;세 가지가 확인된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버가 막는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;pre-receive hook declined&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 클라이언트 설정으로는 우회되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Owner도 예외가 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;역할 기준 차단이라 소유자라고 따로 열린 길이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;막힌 지점이 ref 갱신이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;git은 커밋 객체를 먼저 보내고 브랜치 이름표를 옮기는 단계를 훅이 검사한다. 객체는 이미 서버에 갔고, 주인 없이 남은 것은 나중에 정리된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;거부됐으니 로컬만 되돌리면 끝난다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;git reset --hard HEAD~1&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;b&gt;그 길에 세울 수 있는 문턱&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;길은 하나가 됐다. 그 위에 무엇을 세울 수 있는지가 남는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회사 수습 때 Azure DevOps에서 일하던 흐름을 그대로 세워 보려 했다.&lt;/p&gt;
&lt;pre class=&quot;&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;① 작업 브랜치를 판다
② MR 을 올린다
③ 리뷰를 받는다              &amp;larr; 빠진다
④ 승인자가 승인한다           &amp;larr; 빠진다
⑤ 머지한다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;③④가 두 가지 이유로 빠진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수행할 사람이 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;혼자라 리뷰할 사람도 승인할 사람도 나뿐이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조건으로 걸 수도 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;GitLab 무료판에도 승인(approve) 버튼은 있는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;눌러도 머지 가능 여부가 안 바뀐다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;승인 N명을 받아야 머지된다는 규칙 자체가 유료판 기능이라, 승인을 문턱으로 세울 방법이 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사람이 서는 자리가 비었으니 남은 것은 자동 검사인데, 그것도 아직 못 켠다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;러너가 없어 돌릴 파이프라인이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;보호 브랜치                이미 걸었다.  머지를 막는 게 아니라 MR 을 거치게 만든다 &amp;mdash; 길이다
승인                      문턱이 될 수 없다.  무료판이라 조건으로 못 건다
Pipelines must succeed    유일하게 문턱이 되는 것.  러너가 서면 켠다 (10부)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 지금은 머지를 막는 것이 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;길만 하나로 고정돼 있는 상태다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 혼자 올리고 혼자 머지하며 이 길로만 간다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;변경 전체를 한 화면 diff로 확인하고 넘어가려는 것&lt;/b&gt;이고, 커밋이 한 덩어리로 기록되게 하려는 것이며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;10부에 그 문턱을 붙일 자리를 미리 세워 두려는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.2 합치는 방식 &amp;mdash; 대상 브랜치에 무엇이 남나&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;길은 냈지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 길을 지난 변경이 대상 브랜치에 어떻게 남을지는 아직 안 정했다.&lt;/b&gt;&lt;span&gt; 앞으로 계속&lt;/span&gt; MR을 보낼 것이므로 그 전에 정해 둔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;작업 브랜치에 커밋 셋을 쌓았다고 하자.&lt;/p&gt;
&lt;pre class=&quot;tp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;main   A ── B
            └── X ── Y ── Z      작업 브랜치&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;머지하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;main에 무엇이 남는가.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;설정에 따라 결과가 이렇게 갈린다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;그냥 Merge commit        A ── B ── X ── Y ── Z ── M      커밋 4개
스쿼시 + Merge commit     A ── B ── Z' ── M              커밋 2개
스쿼시 + Fast-forward     A ── B ── Z'                   커밋 1개      &amp;larr; 고른 것&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 줄이 넷인 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;X&amp;middot;Y&amp;middot;Z가 그대로 올라오고 거기에&lt;span&gt;&amp;nbsp;&lt;/span&gt;M이 더해져서다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;'가 붙은 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원본과 다른 새 커밋&lt;/b&gt;이라는 표시다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;합치는 방식이 둘이고, 그 앞에 붙는 스쿼시가 따로 하나다.&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;하는 일&lt;/td&gt;
&lt;td style=&quot;width: 25.1163%;&quot;&gt;대가&lt;/td&gt;
&lt;td style=&quot;width: 17.907%;&quot;&gt;쓰는 자리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4186%;&quot;&gt;&lt;b&gt;Merge commit&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;부모가 둘인 새 커밋&lt;span&gt;&amp;nbsp;&lt;/span&gt;M을 만들어 두 갈래를 잇는다. 자기 변경은 없다&lt;/td&gt;
&lt;td style=&quot;width: 25.1163%;&quot;&gt;작업 커밋이 다 올라와 목록이 지저분해진다&lt;/td&gt;
&lt;td style=&quot;width: 17.907%;&quot;&gt;오래 살아 있는 브랜치를 합칠 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4186%;&quot;&gt;&lt;b&gt;Fast-forward&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;새 커밋을 안 만든다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;main이라는 이름표를&lt;span&gt;&amp;nbsp;&lt;/span&gt;Z'로 옮기는 것이 전부다&lt;/td&gt;
&lt;td style=&quot;width: 25.1163%;&quot;&gt;갈래가 있으면 안 되고 &quot;언제 합쳤나&quot;가 안 남는다&lt;/td&gt;
&lt;td style=&quot;width: 17.907%;&quot;&gt;작업 브랜치가 짧고 자주 합쳐질 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4186%;&quot;&gt;&lt;b&gt;스쿼시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;X&amp;middot;Y&amp;middot;Z의 변경을 다 담은 새 커밋&lt;span&gt;&amp;nbsp;&lt;/span&gt;Z'를 만든다. 셋을 지우는 게 아니라 원본은 작업 브랜치에 남는다&lt;/td&gt;
&lt;td style=&quot;width: 25.1163%;&quot;&gt;커밋 단위 이력이 사라진다&lt;/td&gt;
&lt;td style=&quot;width: 17.907%;&quot;&gt;작업 브랜치를 MR 단위로 다룰 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스쿼시는 어느 방식으로 붙이든 먼저 일어난다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;위의 결과 세 줄이&lt;span&gt;&amp;nbsp;&lt;/span&gt;합치는 방식 &amp;times; 스쿼시 여부로 갈린 것이 그래서다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스쿼시 + Fast-forward를 고른 이유&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MR 하나 = 커밋 하나가 되는 조합이다. 혼자 쓰고 MR이 작업 단위와 1:1이라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;main의 커밋 목록이 그대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;무엇을 했나&quot;의 목록&lt;/b&gt;이 된다. 되돌릴 때도&lt;span&gt;&amp;nbsp;&lt;/span&gt;git revert &amp;lt;sha&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나로 그 MR 전체가 돌아간다 &amp;mdash; 커밋이 흩어져 있으면 어디부터 어디까지가 그 작업인지 찾아 여러 개를 되돌려야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대가는 표에 적은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;커밋 단위 이력&lt;/b&gt;인데, 없어지는 것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;main 히스토리       안 보인다.  스쿼시된 커밋 하나만 들어간다
GitLab             MR 화면의 Commits 탭에 원본 그대로.  브랜치를 지워도
                   refs/merge-requests/&amp;lt;번호&amp;gt;/head 로 보관된다
로컬 작업 브랜치      git branch -D 로 지우기 전까지 남는다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;잃는 것은 &quot;main의 커밋 목록으로 좇는 길&quot;&lt;/b&gt;이고, 무엇을 왜 바꿨는지는 MR이 갖고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Settings &amp;rarr; Merge requests에서 넷을 정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZ2nSB/dJMcag0HnDr/b9ZvDX8WYxGMSkGDGXKOO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZ2nSB/dJMcag0HnDr/b9ZvDX8WYxGMSkGDGXKOO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZ2nSB/dJMcag0HnDr/b9ZvDX8WYxGMSkGDGXKOO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZ2nSB%2FdJMcag0HnDr%2Fb9ZvDX8WYxGMSkGDGXKOO1%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;974&quot; height=&quot;762&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Squash commits when merging     Require              작업 커밋을 하나로 누른다
Merge method                    Fast-forward merge   그 하나를 이름표 이동으로 붙인다
                                + 자동 rebase        붙이기 전에 작업 브랜치를
                                                     대상 브랜치 위에 다시 얹는다
Enable &quot;Delete source branch&quot;   켬                   붙인 뒤 작업 브랜치를 지운다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Merge method의 기본값은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Merge commit이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그대로 두면 스쿼시를 켜도 그 위에 머지 커밋이 한 겹 더 얹혀 커밋이 둘 들어간다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 축이라 둘 다 바꿔야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Squash는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Allow&amp;middot;Encourage로도 켤 수 있는데 그 둘은 MR 화면에서 끌 수 있다. 한 번 잊으면 그 MR만 작업 커밋이 통째로 올라가므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;Require로 둔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 화면 아래쪽의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;커밋 메시지 템플릿&lt;/b&gt;이 커밋 제목과 본문을 정한다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;%{title} (%{reference})        &amp;rarr;  ArgoCD가 GitLab 저장소를 읽을 자격 추가 (cgv/cgv-infra!2)
%{description}                 &amp;rarr;  MR 본문이 커밋 메시지 본문으로&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;!2가 그 프로젝트의 MR 번호다. GitLab은 이슈(#)와 MR(!)이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기호도, 번호를 세는 통도 따로&lt;/b&gt;라 기호만 보고 갈리고, 커밋에서 MR로 서로 넘어갈 수 있게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자동 rebase&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 갈래가 생겼을 때&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;넷 중&lt;span&gt;&amp;nbsp;&lt;/span&gt;자동 rebase만 이름으로 안 보인다. GitLab이 체크박스 아래에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;머지하기 전에 작업 브랜치를 대상 브랜치 위에 자동으로 다시 얹는다&quot;&lt;/b&gt;고 적어 둔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Fast-forward가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;대상 브랜치가 내 커밋의 조상일 때만&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;붙기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;브랜치를 딸 때       대상 브랜치가 내 커밋의 조상이다            &amp;rarr;  붙는다
그 사이 머지가 남     대상 브랜치가 앞서 나가 조상이 아니게 된다    &amp;rarr;  안 붙는다
                                                          MR 에 N commits behind 로 뜬다
자동 rebase        작업 브랜치를 최신 대상 브랜치 위에 다시 얹는다   &amp;rarr;  다시 붙는다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 편에서는 그것으로 다 지나갔고 손으로 푼 적이 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;다만 충돌이 나면 자동 rebase가 거기서 멈춘다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.3 기계의 자격 &amp;mdash; ArgoCD가 저장소를 읽을 자격&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ArgoCD가 GitLab의 저장소를 읽어야 하는데 지금은 못 읽는다. GitHub 쪽이 도는 것은 그 저장소가 공개라 자격 없이 읽히기 때문이고, GitLab으로 옮긴 저장소는 private다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그리고 ArgoCD는 사람이 아니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;브라우저를 열어 로그인할 수 없고, 미리 쥐여준 자격 문자열 하나만 제시할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 할 일이 둘이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;① 자격을 만든다     GitLab 에서 저장소를 읽을 수 있는 문자열 하나를 발급한다
② 자리에 둔다       ArgoCD 가 집어 갈 수 있는 곳 = argocd 네임스페이스의 Secret
                  거기에 url &amp;middot; username &amp;middot; password 를 넣어 두면
                  ArgoCD 가 url 이 맞는 저장소를 읽을 때 그것을 쓴다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;②는 그 Secret을 봉인해서 git에 올리는 일이라 뒤에서 다룬다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;먼저 ①인데, GitLab이 내주는 자격이 다섯 종류라 무엇을 고르느냐가 그대로 새어 나갔을 때의 범위를 정한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;root 비밀번호를 넣으면 전권이 넘어가고, 비밀번호를 바꾸는 순간 배선이 끊긴다. GitLab에는 사람과 기계의 자격이 종류로 갈려 있다.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;자격&lt;/td&gt;
&lt;td&gt;주인&lt;/td&gt;
&lt;td&gt;범위&amp;middot;수명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal access token&lt;/td&gt;
&lt;td&gt;사람 계정&lt;/td&gt;
&lt;td&gt;그 사람의 권한 전부 &amp;middot; 최대 365일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project/Group access token&lt;/td&gt;
&lt;td&gt;자동 생성되는 bot 계정&lt;/td&gt;
&lt;td&gt;역할+스코프 지정 &amp;middot; 최대 365일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Deploy token&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;프로젝트/그룹 자체&lt;/td&gt;
&lt;td&gt;스코프만 &amp;middot; 만료 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy key&lt;/td&gt;
&lt;td&gt;SSH 공개키&lt;/td&gt;
&lt;td&gt;읽기(쓰기 선택) &amp;middot; SSH 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI job token&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 앞에 갈림길이 하나 있다.&lt;/p&gt;
&lt;pre class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;A. public으로 연다        ArgoCD가 자격 없이 clone. 설정 한 번으로 끝
                          대가 &amp;mdash; 이 서버에 닿는 누구나 로그인 없이 인프라 코드를 읽는다
B. private + deploy token 읽기 전용 자격 하나를 만들어 ArgoCD에 쥐여준다
                          대가 &amp;mdash; 재구축 절차에 손 단계가 하나 생긴다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공개 범위 셋 중&lt;span&gt;&amp;nbsp;&lt;/span&gt;internal은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로그인한 사용자&lt;/b&gt;를 뜻하므로 ArgoCD에는 해당이 없다. 자격 없이 읽게 하려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;public이어야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B로 간다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사설망 안이라 public의 위험이 지금 당장 크지는 않지만, 외부로 여는 편에서 반드시 되돌려야 하고 &quot;왜 열어뒀나&quot;에 답을 만들어야 한다. 자격 종류는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;deploy token&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 사람 계정과 무관하고, API 스코프가 없어 새어 나가도 저장소 읽기 밖을 못 한다. ArgoCD 공식 문서가 GitLab 연결에서 안내하는 것도 이것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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; 첫 MR&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자격은 쿠버네티스 Secret으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd&lt;span&gt;&amp;nbsp;&lt;/span&gt;네임스페이스에 있어야 한다. 그런데 봉인본을 배달하는 AppProject가 대상 네임스페이스를 셋으로 한정하고 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;destinations:
  - namespace: data
  - namespace: app
  - namespace: observability      # argocd 가 없다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지의 봉인본은 전부&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;워크로드가 쓸 자격&lt;/b&gt;이었다 &amp;mdash; MySQL&amp;middot;Redis&amp;middot;MinIO에 붙으려고. 이번 것은 성격이 다르다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD가 배달해서 ArgoCD 자신이 쓴다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 두 줄이 첫 MR이 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;    - server: https://kubernetes.default.svc
      namespace: argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작은 변경으로 길을 먼저 확인하는 것&lt;/b&gt;이 이 순서의 목적이다. 봉인본을 먼저 올리면 울타리가 거부해서 배달이 실패하고, 그때는 길 문제인지 봉인 문제인지 구분이 안 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;머지하고 한 바퀴가 도는지 본다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl -n argocd get appproject secrets -o jsonpath=&quot;{.spec.destinations[*].namespace}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;haskell&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;data app observability argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로컬 &amp;rarr; 브랜치 &amp;rarr; MR &amp;rarr; 머지 &amp;rarr; GitLab main &amp;rarr; 미러 &amp;rarr; GitHub &amp;rarr; ArgoCD 폴링 &amp;rarr; 클러스터.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앞에서 낸 길이 실제로 돈다는 것이 이 한 줄로 확인된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 MR은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Merge method를 바꾸기 전에 보낸 것&lt;/b&gt;이라 기본값(Merge commit)으로 붙었다. 그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;main에 커밋이 둘 들어갔다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;0742eb2  Merge branch 'chore/...' into 'main'      내용 없는 껍데기
69cb617  AppProject secrets: &amp;hellip;                      실제 변경 (스쿼시된 것)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스쿼시는 걸려 있어 작업 커밋이&lt;span&gt;&amp;nbsp;&lt;/span&gt;69cb617&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나로 눌렸는데, 그 위에 머지 커밋이 한 겹 더 얹혔다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;두 축을 둘 다 맞춰야 한다는 것이 이 한 줄에서 눈으로 확인된다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이걸 보고&lt;span&gt;&amp;nbsp;&lt;/span&gt;Fast-forward merge로 바꿨고, 다음 MR부터는 커밋이 하나만 올라간다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝트 단위로 만든다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그룹에 만들면&lt;span&gt;&amp;nbsp;&lt;/span&gt;cgv-onprem까지 읽을 수 있는데, ArgoCD가 읽는 저장소는&lt;span&gt;&amp;nbsp;&lt;/span&gt;cgv-infra&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나뿐이다. 앱 배포도 이 저장소의 차트가 하므로 앞으로도 늘지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;cgv-infra &amp;rarr; Settings &amp;rarr; Repository &amp;rarr; Deploy tokens에서 발급한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Name      argocd
Scopes    read_repository        &amp;larr; 이것 하나만
만료       비움&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만료를 걸면 그날 이후 ArgoCD가 저장소를 못 읽고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;배포가 조용히 멈춘다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;갱신을 알려줄 장치가 없는 환경이라 만료를 두지 않고, 새어 나가면 손으로 걷는 쪽을 택한다. 발급하면 사용자명(gitlab+deploy-token-{n})과 토큰이 한 번만 표시된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존&lt;span&gt;&amp;nbsp;&lt;/span&gt;seal-secrets.sh는 값 다섯 개를 물어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;10종을 일괄&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만드는 초기 구축용이다. 하나를 더하려고 다시 돌리면 값 다섯 개를 전부 재입력해야 하고, 그중 하나라도 지금 클러스터에 떠 있는 것과 다르면 해당 워크로드가 붙지 못한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 낱개용을 따로 둔다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;./bootstrap/seal-one.sh &amp;lt;이름&amp;gt; &amp;lt;네임스페이스&amp;gt; [라벨 key=value ...]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설계는 넷이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;값을 인자가 아니라 프롬프트로     셸 히스토리에 안 남는다
값 입력이 화면에 안 보인다        어깨너머&amp;middot;스크롤백 방지
평문 Secret 파일을 만들지 않는다  kubectl &amp;rarr; kubectl label &amp;rarr; kubeseal 파이프로만 흐른다
컨트롤러 이름을 고정              차트 기본값과 달라 매번 지정해야 하는 것을 실수로 빠뜨리지 않게&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;라벨을 인자로 받는 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD 저장소 자격처럼 라벨이 있어야 인식되는 Secret&lt;/b&gt;이 있어서다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;./bootstrap/seal-one.sh argocd-repo-cgv-infra argocd \
  argocd.argoproj.io/secret-type=repository&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;키: type       값: git
키: url        값: http://192.168.0.167:8929/cgv/cgv-infra.git
키: username   값: gitlab+deploy-token-1
키: password   값: (토큰)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;url은 뒤에서 바꿀 repoURL과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;글자까지 같아야 한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;ArgoCD가 그 문자열로 자격과 저장소를 짝짓는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나온 봉인본의 구조가 이렇다.&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kind: SealedSecret
metadata:
  namespace: argocd
spec:
  encryptedData:
    password: AgAFBbWac80q&amp;hellip;      값이 전부 암호문
    type: AgCoETqHf+PX&amp;hellip;
    url: AgDYnvD7HZ6v&amp;hellip;
    username: AgCoAU6Oq7ih&amp;hellip;
  template:
    metadata:
      labels:
        argocd.argoproj.io/secret-type: repository    &amp;larr; 풀려서 만들어질 Secret에 붙을 라벨
      name: argocd-repo-cgv-infra
      namespace: argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;template이 핵심이다. 컨트롤러가 이것을 풀 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 안의 metadata를 그대로 가진 Secret&lt;/b&gt;을 만들고, 라벨이 여기 실려 있어야 ArgoCD가 저장소 자격으로 알아본다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 봉인본에는 성질이 하나 더 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD가 GitLab을 읽는 데 쓸 자격인데, 그 자격 자체가 GitLab 저장소 안에 들어 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;ArgoCD 가 GitLab 을 읽으려면      자격 Secret 이 있어야 한다
그 Secret 은 어디서 오나          git 의 봉인본을 ArgoCD 가 읽어 배달한다
그 git 을 읽으려면               자격 Secret 이 있어야 한다     &amp;larr; 제자리로 돌아온다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문을 여는 열쇠가 그 문 안에 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이대로면 아무도 못 들어간다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ArgoCD가 아직&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitHub&lt;/b&gt;를 보고 있어서다. github 저장소는 public이라 자격 없이 읽힌다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;로컬        평문 Secret
   │ kubeseal 이 공개키로 잠근다
   ▼
git        SealedSecret (암호문)  &amp;rarr;  GitLab main  &amp;rarr;  미러  &amp;rarr;  GitHub
   │                                                            │
   │                                   ArgoCD 가 여기를 읽어 apply  &amp;larr; 공개라 자격이 필요 없다
   ▼
클러스터    SealedSecret 리소스     아직 암호문
   │ 컨트롤러가 개인키로 푼다
   ▼
클러스터    Secret                 ArgoCD 가 이것을 자기 자격으로 쓴다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ArgoCD는 개인키를 모르고 알 필요도 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;암호문을 클러스터에 놓는 데까지가 그 일이고, 푸는 것은 컨트롤러다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 순서가 정해진다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자격을 먼저 배달하고, 그다음에 시선을 GitLab으로 옮긴다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;뒤집으면 ArgoCD가 GitLab을 못 읽는 상태가 되는데 열쇠는 그 안에 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클러스터를 처음부터 다시 세우면 repoURL이 이미 GitLab이다. 자격 Secret은 없고, 그것을 담은 봉인본은 GitLab 안에 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;그때 하는 일    이 Secret 하나만 손으로 apply 하고 시작한다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실무의 GitOps도 대부분&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;첫 자격 하나는 사람이 넣는다&quot;&lt;/b&gt;는 지점을 갖는다. 자동화의 시작점은 자동화될 수 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;봉인본과 낱개 봉인 스크립트를 한 커밋으로 묶어 브랜치에 올리고 MR을 연다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Commits 1  &amp;middot;  Changes 4

The source branch is 2 commits behind the target branch.
Source branch will be rebased without pipeline.
1 commit will be added to main.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 브랜치는 첫 MR을 머지하기 전에 딴 것이라 그동안&lt;span&gt;&amp;nbsp;&lt;/span&gt;main이 두 칸 앞섰다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;머지 버튼 하나에 스쿼시&amp;middot;rebase&amp;middot;fast-forward가 순서대로 돌고&lt;/b&gt;, 결과가 마지막 줄이다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;git log --oneline -3 origin/main&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;34e12ca ArgoCD가 GitLab 저장소를 읽을 자격 추가 (cgv/cgv-infra!2)
0742eb2 Merge branch 'chore/secrets-project-argocd-ns' into 'main'
69cb617 AppProject secrets: 배달 대상에 argocd 네임스페이스 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 MR에서 커밋이 둘 들어갔던 자리가 이번엔 하나다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;승인 버튼은 누르기 전에도 뒤에도&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ready to merge!로 같았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬을 맞춘다. 원격 브랜치는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Delete source branch가 이미 지웠고, 로컬 브랜치는 rebase로 sha가 갈라져 git이 머지 안 된 것으로 보므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;-D로 지운다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;git switch main
git pull
git branch -D feat/argocd-gitlab-repo-credential&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;b&gt;클러스터에 닿았다&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl -n argocd get sealedsecret,secret argocd-repo-cgv-infra&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;NAME                                             STATUS   SYNCED   AGE
sealedsecret.bitnami.com/argocd-repo-cgv-infra            True     95m

NAME                           TYPE     DATA   AGE
secret/argocd-repo-cgv-infra   Opaque   4      95m&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 줄이 경로를 그대로 보여준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;SealedSecret   SYNCED True   ArgoCD 가 GitHub 에서 읽어 클러스터에 apply 했다.  아직 암호문
Secret         DATA 4        컨트롤러가 개인키로 풀었다 &amp;mdash; type &amp;middot; url &amp;middot; username &amp;middot; password&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ArgoCD가 만든 것은 암호문까지고, 평문으로 바꾼 것은 컨트롤러다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자격이&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd&lt;span&gt;&amp;nbsp;&lt;/span&gt;네임스페이스에 섰으니&lt;span&gt;&amp;nbsp;&lt;/span&gt;repoURL을 GitLab으로 돌릴 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기에 남길 것이 하나 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시선을 GitLab으로 옮기고 나면 GitLab을 꺼둘 수 없게 된다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;지금은 ArgoCD가 GitHub를 보고 있어 GitLab이 내려가도 클러스터가 돌지만, 전환 뒤에는 sync가 함께 끊긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.4 시선 전환 &amp;mdash; repoURL을 GitLab으로, 한 커밋&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자격은&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd&lt;span&gt;&amp;nbsp;&lt;/span&gt;네임스페이스에 섰지만 ArgoCD는 아직 GitHub를 본다. 바꿀 줄을 세는 것부터 한다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Select-String -Path (Get-ChildItem cgv-infra -Recurse -Include *.yaml) -Pattern &quot;github.com/sss654654&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;argocd/applications/*.yaml            6줄    dashboards &amp;middot; kafka &amp;middot; metallb-pool &amp;middot; mysql &amp;middot; redis &amp;middot; sealed-secrets
argocd/applicationsets/apps.yaml      2줄    generator 와 source 에 각각
argocd/applicationsets/observability  1줄    값 제공용 git 소스
argocd/applicationsets/platform       1줄    값 제공용 git 소스
bootstrap/root-app.yaml               1줄
                                     ──────
                                     11줄&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;업스트림 헬름 차트 저장소 12줄(grafana.github.io&amp;middot;charts.min.io&lt;span&gt;&amp;nbsp;&lt;/span&gt;등)은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;안 건드린다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;바꾸는 것은 &quot;내 인프라 코드를 어디서 읽나&quot;뿐이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;repoURL만 바꾸면 안 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;AppProject가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;읽어도 되는 저장소 목록&lt;/b&gt;을 따로 갖고 있다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# argocd/projects/{apps,infra,secrets}.yaml
sourceRepos:
  - https://github.com/sss654654/cgv-infra.git&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기에 GitLab이 없는 상태로&lt;span&gt;&amp;nbsp;&lt;/span&gt;repoURL을 바꾸면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;App 전부가&lt;span&gt;&amp;nbsp;&lt;/span&gt;repository not permitted in project로 막힌다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그래서 순서가 셋이 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;① 넓힌다    sourceRepos 에 GitLab 을 추가.  GitHub 는 남긴다
           이 시점에는 repoURL 이 아직 GitHub 라 그것도 허용돼 있어야 한다
② 옮긴다    repoURL 11줄을 GitLab 으로
③ 좁힌다    sourceRepos 에서 GitHub 제거.  전환을 확인한 뒤&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;②를 되돌려야 할 때 ①이 살아 있어야 한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그래서 ③은 확인이 끝난 다음이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커밋 둘, MR 하나&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;9818372   AppProject 허용 저장소에 GitLab 주소 추가      3파일 3줄
c0f6ac0   ArgoCD가 읽는 저장소를 GitLab으로 전환         10파일 11줄&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브랜치 하나에 쌓아 MR로 올린다. 스쿼시가 걸려 있어&lt;span&gt;&amp;nbsp;&lt;/span&gt;main에는 하나로 들어가고, MR의&lt;span&gt;&amp;nbsp;&lt;/span&gt;Commits&lt;span&gt;&amp;nbsp;&lt;/span&gt;탭에 둘이 그대로 남는다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;59203d1  AppProject 허용 저장소에 GitLab 주소 추가 (cgv/cgv-infra!3)
         13 files changed, 14 insertions(+), 11 deletions(-)&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;b&gt;root-app은 자기 자신을 배달하지 않는다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;머지해도 클러스터의&lt;span&gt;&amp;nbsp;&lt;/span&gt;root&lt;span&gt;&amp;nbsp;&lt;/span&gt;Application은 안 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# bootstrap/root-app.yaml
source:
  repoURL: &amp;hellip;
  path: argocd            &amp;larr; argocd/ 만 본다.  bootstrap/ 은 안 본다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;root-app이 관리하는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd/&lt;span&gt;&amp;nbsp;&lt;/span&gt;아래이고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자기 정의 파일은 그 밖에 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아무도 그 파일을 배달하지 않으므로 손으로 넣어야 한다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl apply -f bootstrap/root-app.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;applescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;application.argoproj.io/root configured&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;b&gt;그 한 줄이 전환이다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;root-app이&lt;span&gt;&amp;nbsp;&lt;/span&gt;default&lt;span&gt;&amp;nbsp;&lt;/span&gt;AppProject에 속하고, 그 프로젝트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;sourceRepos: [&quot;*&quot;]다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어느 저장소든 읽을 수 있어서 GitLab을 곧장 본다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl apply
  &amp;rarr; root Application 의 repoURL 이 GitLab 으로 바뀐다
    &amp;rarr; root-app 이 GitLab 에서 argocd/ 를 읽는다      &amp;larr; 자격 Secret 을 여기서 처음 쓴다
      &amp;rarr; AppProject 3개 &amp;middot; Application 6개 &amp;middot; ApplicationSet 3개가 갱신된다
        &amp;rarr; 그 App 들도 GitLab 을 보기 시작한다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;미러가 GitHub로 나가기를 기다릴 필요가 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어차피 해야 하는 단계를 앞으로 당긴 것뿐&lt;/b&gt;이고, 그 순간이 곧 전환이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;pre class=&quot;maxima&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl -n argocd get app -o custom-columns='NAME:.metadata.name,REPO:.spec.source.repoURL,SYNC:.status.sync.status,HEALTH:.status.health.status'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;NAME                 REPO                                          SYNC     HEALTH
dashboards           http://192.168.0.167:8929/cgv/cgv-infra.git   Synced   Healthy
kafka                http://192.168.0.167:8929/cgv/cgv-infra.git   Synced   Healthy
mysql                http://192.168.0.167:8929/cgv/cgv-infra.git   Synced   Healthy
redis                http://192.168.0.167:8929/cgv/cgv-infra.git   Synced   Healthy
&amp;hellip;
grafana              &amp;lt;none&amp;gt;                                        Synced   Healthy
mimir                &amp;lt;none&amp;gt;                                        Synced   Healthy&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;none&amp;gt;으로 보이는 열 개는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;멀티소스 App&lt;/b&gt;이다. 업스트림 차트와 값 제공용 git 소스를 함께 쓰므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;.spec.source가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;.spec.sources[*]에 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl -n argocd get app -o custom-columns='NAME:.metadata.name,SOURCES:.spec.sources[*].repoURL'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;NAME       SOURCES
grafana    https://grafana.github.io/helm-charts,http://192.168.0.167:8929/cgv/cgv-infra.git
mimir      https://grafana.github.io/helm-charts,http://192.168.0.167:8929/cgv/cgv-infra.git
minio      https://charts.min.io/,http://192.168.0.167:8929/cgv/cgv-infra.git
traefik    https://traefik.github.io/charts,http://192.168.0.167:8929/cgv/cgv-infra.git&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소스가 둘씩이고, 앞은 업스트림 차트 그대로 뒤만 GitLab이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;관측 스택은 차트를 업스트림에서 받고 환경값만 내 저장소에서 읽는 구조라, 전환이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;내 인프라 코드를 어디서 읽나&quot;에만 걸렸다&lt;/b&gt;는 것이 이 목록으로 확인된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App 20개 전부, 20개 모두&lt;span&gt;&amp;nbsp;&lt;/span&gt;Synced.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;업스트림 차트는 그대로고 값 소스만 GitLab으로 옮겨졌다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Degraded&lt;span&gt;&amp;nbsp;&lt;/span&gt;넷은 이 작업과 무관하다&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;booking &amp;middot; frontend &amp;middot; queue    ImagePullBackOff   AGE 7d23h
root                          자식이 Degraded 라 따라 Degraded&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이미지가 아직 없어서&lt;/b&gt;다. 앱 이미지를 빌드해 레지스트리에 올리는 것이 10부, 그 이미지로 앱이 뜨는 것이 11부다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;SYNC가&lt;span&gt;&amp;nbsp;&lt;/span&gt;Synced인 것이 그 증거다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;git에 적힌 대로 클러스터가 맞춰져 있고&lt;/b&gt;, 다만 그 선언이 요구하는 이미지가 아직 세상에 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전환이 확인됐으니&lt;span&gt;&amp;nbsp;&lt;/span&gt;sourceRepos에서 GitHub를 뺀다. 안 빼도 지금 GitHub를 보고 있지는 않다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;sourceRepos는 &quot;보는 곳&quot;이 아니라 &quot;봐도 되는 곳&quot;&lt;/b&gt;이다. 다만 안 쓰는 문이 열려 있으면, 실수로 GitHub를 가리키는 App이 들어와도 안 막힌다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 커밋은 다음 장 작업과 함께 보낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;6. 울타리와 제어면&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원본은 옮겼는데 구멍이 둘 남았다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;① ArgoCD 자신이 GitOps 밖에 있다     설정 파일은 git 에 있는데 적용은 helm 을 손으로 친다
                                   커밋만 하고 적용을 잊어도 아무 표시가 없다

② root App 만 울타리가 없다          default 프로젝트라 저장소 &amp;middot; 네임스페이스 &amp;middot; 리소스가 전부 &quot;*&quot;
                                   울타리를 세우는 App 이 울타리 밖에 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.1 AppProject &amp;mdash; Application이 할 수 있는 일의 범위&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Application 하나는 반드시 프로젝트 하나에 속하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 프로젝트가 반경을 정한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;sourceRepos                  어느 저장소를 읽어도 되나
destinations                 어느 클러스터 &amp;middot; 네임스페이스에 배포해도 되나
clusterResourceWhitelist     ClusterRole &amp;middot; CRD 같은 클러스터 범위 리소스를 만들어도 되나
namespaceResourceWhitelist   네임스페이스 안 리소스는 어떤 종류까지&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ArgoCD는 클러스터 관리자 권한으로 돈다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;App 하나만 잘못돼도 클러스터에 뭐든 만들 수 있어서&lt;/b&gt;, 그 반경을 미리 좁혀 둔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금 넷을 나란히 놓으면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;프로젝트    저장소                  네임스페이스                          cluster 리소스   ns 리소스
────────────────────────────────────────────────────────────────────────────────────────
default    *                     *                                   *             *
apps       cgv-infra             app                                 없음           *
infra      cgv-infra + 업스트림 6   data &amp;middot; observability &amp;middot; &amp;hellip;  다섯       *             *
secrets    cgv-infra             data &amp;middot; app &amp;middot; observability &amp;middot; argocd  없음        SealedSecret&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;갈린 기준은 네임스페이스가 아니라 &quot;무엇을 하는 App인가&quot;다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;secrets가 그 예다 &amp;mdash; 네 방에 들어가지만 만들 수 있는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;SealedSecret&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나뿐이라, 그 App이 읽는 디렉터리에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Deployment가 섞여도 거부된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;apps가 업스트림 저장소를 안 여는 것도 같은 축이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;앱 차트는 내가 썼으므로 외부에서 받을 일이 없고, 안 열어 뒀으니 그 App이 인터넷에서 무언가 받아 배포하는 길이 막혀 있다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.2 울타리를 좁힌다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;허용 저장소를 GitLab 하나로&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;repoURL이 전부 GitLab을 가리키므로 GitHub 주소는 더 이상 참조되지 않는다. 목록에 남겨 두면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitHub를 가리키는 Application이 실수로 들어와도 프로젝트가 안 막는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;argocd/projects/{apps,infra,secrets}.yaml    각 1줄 삭제&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;root 전용 프로젝트&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;root는&lt;span&gt;&amp;nbsp;&lt;/span&gt;default에 있었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitOps 진입점이 클러스터에서 가장 넓은 권한을 갖는 상태&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# argocd/projects/bootstrap.yaml
sourceRepos:
  - http://192.168.0.167:8929/cgv/cgv-infra.git
destinations:
  - namespace: argocd
clusterResourceWhitelist: []          # cluster-scoped 리소스를 만들지 않는다
namespaceResourceWhitelist:
  - group: argoproj.io
    kind: AppProject / Application / ApplicationSet&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;root가 만드는 것은 선언뿐&lt;/b&gt;이다. 워크로드는 그 선언이 만든 App들이 각자의 프로젝트 안에서 만들므로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;root에는 그 권한이 필요 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;bootstrap/root-app.yaml의&lt;span&gt;&amp;nbsp;&lt;/span&gt;project를 바꾸고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;손으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;apply한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그 파일은&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd/&lt;span&gt;&amp;nbsp;&lt;/span&gt;밖에 있어&lt;span&gt;&amp;nbsp;&lt;/span&gt;root가 배달하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl apply -f bootstrap/root-app.yaml&lt;/code&gt;&lt;/pre&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;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.3 ArgoCD가 자기를 관리한다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;bootstrap/argocd/values.yaml은 git에 있는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;적용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;helm upgrade를 손으로 친다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;커밋만 하고 적용을 잊어도 아무 표시가 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;차트를 Application으로 만든다&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# argocd/applications/argocd.yaml
spec:
  project: argocd
  sources:
    - repoURL: https://argoproj.github.io/argo-helm
      chart: argo-cd
      targetRevision: 10.1.4                       # 설치된 버전과 같게
      helm:
        valueFiles: [ $values/bootstrap/argocd/values.yaml ]
    - repoURL: &amp;lt;GitLab&amp;gt;
      ref: values                                  # 값만 제공.  이 소스는 배포하지 않는다
  syncPolicy:
    automated:
      prune: false
      selfHeal: false&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;targetRevision이 다르면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이 App이 처음 sync하는 순간 ArgoCD가 자기를 업그레이드하며 재시작한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설치된 것과 같게 둔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 스위치를 끄는 이유&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;prune      이 파일이 사라지면 ArgoCD 가 자기 리소스를 지운다.  지우고 나면 되살릴 주체가 없다
selfHeal   sync 가 argocd-server &amp;middot; application-controller 를 재시작시키고,
           그 재시작이 다시 sync 를 유발하는 순환에 들어갈 수 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;끄고 나면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클러스터 쪽 drift 교정은 못 얻는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;남는 것은 git 변경이 자동으로 반영되는 것과, 어긋나면&lt;span&gt;&amp;nbsp;&lt;/span&gt;OutOfSync로 드러나는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전용 프로젝트인 이유&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;argo-cd 차트가 만드는 것    Deployment &amp;middot; Service &amp;middot; ConfigMap &amp;middot; ServiceAccount
                        ClusterRole &amp;middot; ClusterRoleBinding &amp;middot; CRD&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;infra에 합치면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;플랫폼 워크로드 App도 제어면 네임스페이스에 닿을 수 있게 된다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;반경이 다르므로 분리한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최초 설치는 여전히 helm이 한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클러스터가 비어 있을 때는 ArgoCD를 배포할 주체가 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;install.sh가&lt;span&gt;&amp;nbsp;&lt;/span&gt;helm install로 넣고, 이 App이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그렇게 만들어진 리소스를 이어받는다. &lt;/b&gt;&lt;b&gt;&lt;/b&gt;그래서 소유자가 잠시 둘이 된다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;helm      Secret sh.helm.release.v1.argocd.*  이 남는다.  helm list 에 계속 보인다
ArgoCD    같은 리소스를 관리하기 시작한다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ArgoCD의 Helm 지원은&lt;span&gt;&amp;nbsp;&lt;/span&gt;helm template&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식이라 release를 만들지 않는다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;옛 release 기록이 남아 있으면 누가&lt;span&gt;&amp;nbsp;&lt;/span&gt;helm uninstall argocd를 쳤을 때 관리 중인 리소스가 통째로 사라진다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인수인계를 확인한 뒤 그 기록만 지운다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl -n argocd delete secret -l owner=helm,name=argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인수인계 확인&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 목록에서 나이가 갈린다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;$ kubectl -n argocd get appproject
NAME        AGE
apps        8d
argocd      114s      &amp;larr; root 가 방금 만든 것
bootstrap   114s      &amp;larr; root 가 방금 만든 것
default     8d
infra       8d
secrets     8d&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;root가 배달한 결과다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;bootstrap과&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd는 커밋에만 있던 파일인데, 폴링 한 번에 클러스터 객체가 됐다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;$ kubectl -n argocd get app -o custom-columns='NAME:.metadata.name,PROJECT:.spec.project,SYNC:.status.sync.status,HEALTH:.status.health.status'
NAME      PROJECT     SYNC     HEALTH
argocd    argocd      Synced   Healthy    &amp;larr; 새로 선 App
root      bootstrap   Synced   Healthy    &amp;larr; default 에서 옮겨졌다
...       infra &amp;middot; apps &amp;middot; secrets          나머지 19 개 Synced&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;argocd가&lt;span&gt;&amp;nbsp;&lt;/span&gt;Synced라는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;차트를 렌더한 결과와 클러스터에 있는 것이 같다&lt;/b&gt;는 뜻이다. helm이 만들어 둔 리소스를 그대로 이어받았다는 확인이다. 다르게 나왔다면 렌더 결과로 덮어썼을 것이므로, 설치 때 쓴 값 파일과&lt;span&gt;&amp;nbsp;&lt;/span&gt;targetRevision이 같아야 이 결과가 나온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;apps&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로젝트의 셋(booking&amp;middot;frontend&amp;middot;queue)은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Degraded다. 컨테이너 이미지를 아직 안 만들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;ImagePullBackOff에 있고, 이미지는 10부와 11부에서 만든다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Synced는 선언대로 배포됐다는 뜻이고&lt;span&gt;&amp;nbsp;&lt;/span&gt;Healthy는 그것이 실제로 뜬다는 뜻이라, 둘은 따로 움직인다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 옛 소유자를 지운다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;$ kubectl -n argocd delete secret -l owner=helm,name=argocd
secret &quot;sh.helm.release.v1.argocd.v1&quot; deleted
secret &quot;sh.helm.release.v1.argocd.v2&quot; deleted
secret &quot;sh.helm.release.v1.argocd.v3&quot; deleted

$ helm list -n argocd
NAME  NAMESPACE  REVISION  UPDATED  STATUS  CHART  APP VERSION&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;목록이 비었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;helm uninstall argocd로 제어면을 통째로 날릴 경로가 사라졌고, 소유자가 ArgoCD 하나로 남았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;바뀐 것은 값을 고치는 자리다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;bootstrap/argocd/values.yaml을 고치고 커밋하면 그것으로 끝난다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;helm upgrade를 치는 자리가 없어졌고, 커밋했는데 적용을 잊는 경우도 없어졌다. 대신 sync가 실패하면 제어면 자신이 못 고치는 상태가 되므로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;install.sh의&lt;span&gt;&amp;nbsp;&lt;/span&gt;helm install은 그 자리에 남는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;7. 밖에서 안으로 &amp;mdash; 이름과 즉시 반영&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기까지 원본이 옮겨졌다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD가 GitHub 대신 데스크탑의 GitLab을 읽는다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; Application 스물한 개의&lt;span&gt;&amp;nbsp;&lt;/span&gt;repoURL이 전부 그리로 향해 있고, 그 Application들이 할 수 있는 일의 반경도 프로젝트로 갈라 뒀다. ArgoCD는 자기 설정까지 커밋으로 받는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서는 남은 일 두 가지를 한다. 서로 별개의 일이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나, ArgoCD와 Grafana를 여는 방법이 아직&lt;span&gt;&amp;nbsp;&lt;/span&gt;kubectl port-forward뿐이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이름을 붙여 브라우저에서 바로 열리게 한다. 문을 낼 자리를 정하려면 요청이 밖에서 파드까지 가는 길을 전부 알아야 해서, 클러스터 네트워크를 여기서 같이 짚는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘, 커밋하고 나면 여전히 기다린다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;ArgoCD가 저장소를 주기적으로 다시 읽는 방식이라 커밋과 반영 사이에 최대 3분이 낀다. ArgoCD가 어떻게 돌고 있는지부터 보고, GitLab이 커밋을 받는 순간 알려 주도록 webhook을 건다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;7.1  문이 없다             port-forward 뿐 &amp;middot; 파드는 밖에서 안 보인다    ┐
7.2  주소를 전부 짚는다     클러스터 네트워크 &amp;middot; 세 층 &amp;middot; 문을 어디에       ├ 이름
7.3  이름을 붙인다         Ingress 둘 &amp;middot; hosts 두 줄                    ┘
7.4  ArgoCD는 어떻게 도나   루프 &amp;middot; watch 와 폴링 &amp;middot; 넣어 둔 자격의 자리    ┐
7.5  webhook으로 바꾼다    문서 둘로 고른다 &amp;middot; 폴링은 남긴다   [결정]      ├ 즉시 반영
7.6  webhook을 건다        GitLab 허용 &amp;middot; 비밀 &amp;middot; 등록                    ┘&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 일은 별개인데 순서에는 이유가 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;첫째 일이 낸 문을 둘째 일이 그대로 쓴다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;네트워크를 전면으로 다루는 것은 14부다. 여기서는 그때 바뀔 축만 미리 잡아 둔다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.1 문이 없다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ArgoCD 화면을 여는 방법이 지금&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;kubectl port-forward뿐&lt;/b&gt;이다. 8부 내내 그렇게 열었고 Grafana도 같다. 명령 하나가 늘 떠 있어야 하고, 끄면 닫힌다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;막힌 이유는 거리가 아니다. 데스크탑에서 노드까지는 닿는다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.201로 ping도 되고&lt;span&gt;&amp;nbsp;&lt;/span&gt;:6443으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;kubectl도 된다. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD는 노드가 아니라 그 위에서 도는 파드&lt;/b&gt;이고, 파드가 사는 대역은 클러스터 밖에서 보이지 않는다. 데스크탑에는 그리로 가는 길이 아예 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방향에 따라 갈리는 것이 이 구조의 성질이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파드가 밖으로 나가는 것은 된다 &amp;mdash; ArgoCD가 데스크탑의 GitLab을 읽어 가는 것이 그 길이고, 나갈 때는 출발지가 감춰져 있어도 된다. 밖에서 파드로 들어오는 것이 안 된다 &amp;mdash; 들어올 때는 목적지가 드러나 있어야 하는데, 파드 주소는 사설망에 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;port-forward는 이 벽을 문이 아니라 우회로로 넘는다. kubectl이 API 서버(:6443)에 터널을 뚫고 내 PC의 포트를 그 끝에 붙이는 것이라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;kubeconfig(클러스터 전권 자격)가 있어야 하고 명령이 떠 있는 동안만 산다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러니 할 일은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;밖에서 안으로 들어오는 문을 내는 것&lt;/b&gt;이다. 어디에 낼지 정하려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클러스터가 쓰는 주소를 전부 짚어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.2 주소를 전부 짚는다 &amp;mdash; 클러스터 네트워크&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주소가 어디서 오나&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;사설망  192.168.0.0/24     공유기가 정한 대역
          .2 - .199        공유기가 DHCP 로 자동 발급
          .200 - .254      자동 발급 밖 &amp;mdash; 손으로 정한다

  .1     공유기
  .167   데스크탑            DHCP 예약
  .200   노트북 = Proxmox    설치할 때 손으로 지정
           └ vmbr0 브리지가 VM 을 이 사설망에 그대로 참여시킨다
  .201   k3s-1  ┐
  .202   k3s-2  ├ 노드.  게스트 OS 에 고정으로 지정
  .203   k3s-3  ┘
  .240   MetalLB 가 풀(.240-250)에서 뗀 주소.  노드 것이 아니다

  ══════  여기부터 클러스터 안 &amp;mdash; 사설망에서 안 보인다  ══════

10.43.x.x   Service    apiserver 가 만들 때 준다.  그 뒤 안 바뀐다
              traefik              &amp;larr; .240 이 붙은 곳
              frontend &amp;middot; booking &amp;middot; queue
              argocd-server &amp;middot; grafana

10.42.x.x   Pod        Calico 가 뜰 때 준다.  죽으면 회수
              traefik 파드 2 (k3s-1 &amp;middot; k3s-2)
              앱 파드 &amp;middot; argocd-server 파드 &amp;middot; grafana 파드&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;노드와 MetalLB 풀을 자동 발급 구간 밖에 둔 것은 충돌을 막기 위해서다. 공유기가 같은 주소를 다른 장비에 내줄 일이 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 그림이 지금 문제를 다 말한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd-server도&lt;span&gt;&amp;nbsp;&lt;/span&gt;grafana도 클러스터 안에 있고, 그 안은 사설망에서 부를 수 없다. 선을 넘어 서 있는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나뿐이고 그것은 Traefik Service에 붙어 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;밖에서 안으로 들어오는 길은 지금 이것뿐이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style1&quot; /&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그 길이 어떻게 도는가 &amp;mdash; 세 층&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;.240으로 들어온 요청이 파드에 닿기까지 세 층을 지난다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;층마다 볼 수 있는 것이 달라서, 정할 수 있는 것도 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1층 &amp;middot; MAC을 본다 &amp;rarr; 어느 노드가 받나&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;브라우저 ──&quot;192.168.0.240 누구?&quot;──▶ 노드 하나가 자기 MAC 으로 답한다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;.240은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클러스터로 들어오는 입구 주소&lt;/b&gt;다. 클러스터 안의 어떤 Service가&lt;span&gt;&amp;nbsp;&lt;/span&gt;type: LoadBalancer로 &quot;밖에서 닿는 IP를 하나 달라&quot;고 선언했고, MetalLB가 풀에서 떼어 붙였다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 Service가 누구 것인지는 1층의 관심사가 아니다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 여기서 정할 것은 어느 노드가 받느냐까지다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그런데 Service는 etcd에 적힌 선언일 뿐이라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240을 실제로 지고 있는 장비가 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사설망에서 IP로 보내려면 그 IP를 가진 장치의 MAC을 알아야 하는데(그걸 묻는 것이 ARP다) 주인이 없으면 답이 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MetalLB가 대신 답한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;노드마다 파드가 하나씩 떠 있고 그중 하나가 &quot;그 주소는 내 노드&quot;라며 자기 MAC을 내준다. 셋이 떠 있어도 답이 하나인 것은 겨뤄서가 아니라 셋이 같은 계산(노드 이름 + 그 IP를 해시해 정렬)을 해서다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;매번 같은 노드가 받고&lt;/b&gt;, 그 노드가 죽으면 다음 순번이 이어받는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기까지가 MetalLB의 전부다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;ARP에 답할 뿐 요청 자체는 지나가지 않으니, 클라우드의 ELB처럼 트래픽을 나누는 일도 안 한다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://metallb.universe.tf/concepts/layer2/&quot;&gt;MetalLB 문서&lt;/a&gt;도 &quot;does not implement a load balancer&quot;라 적고 하는 일을 failover라 부른다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그런데도 이 자리가 필요한 것은 주소 하나를 고정해 주기 때문이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;MetalLB가 없으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;NodePort뿐이라 부르는 쪽이&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.201:31234처럼 노드를 지목해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1층의 목적은 하나였다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 밖에 있는 요청을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클러스터 안의 어느 노드까지 실어 오는 것&lt;/b&gt;, 그것도 부르는 쪽이 노드를 하나도 모른 채로.&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240이 그 발판이다. 노드 주소도 파드 주소도 아닌,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;클러스터로 들어오는 입구&quot;라는 뜻만 가진 주소&lt;/b&gt;다. 뒤에 노드가 몇 대인지 누가 살아 있는지는 MetalLB가 감춘다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과는 이렇다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;요청이&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3에 도착&lt;/b&gt;했고, 그 안에 적힌 목적지는 아직&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.240:80&lt;span&gt;&amp;nbsp;&lt;/span&gt;그대로다. ARP로 정한 것은 어느 장비로 배달할지였을 뿐, 목적지는 건드리지 않았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자기 IP가 아닌 목적지를 든 요청을 손에 든 상태&lt;/b&gt;다. 보통 노드라면 이런 것을 버린다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2층 &amp;middot; 목적지를 바꾼다 &amp;rarr; 어느 파드가 받나&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;목적지 192.168.0.240:80  ──kube-proxy 규칙──▶  목적지 10.42.207.59&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;k3s-3은 그것을 버리지 않는다. 이유는 곧 나오고, 먼저&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240이 누구 것인지부터 밝혀야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1층에서 이름을 안 밝힌 그 Service가 Traefik의 것이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;여기서 이름 셋이 나오는데 서로 층이 다르다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Traefik            Ingress 를 읽어 실제로 보내는 프로그램.  = Ingress 컨트롤러
Traefik 파드        그 Traefik 이 도는 곳.  지금 둘
Traefik Service    그 파드 앞에 선 고정 주소.  .240 이 붙어 있다
Ingress            &quot;어떤 이름으로 오면 어디로&quot; 를 적어 둔 규칙.  Traefik 이 읽는다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;규칙과 실행이 갈려 있다는 것이 그 이름의 뜻이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress는 적어 둔 문서일 뿐이고, 그것을 읽어 실제로 보내는 쪽이 Traefik이다. 규칙을 읽는 일은 3층에서 하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;2층은 Traefik이 도는 파드까지 요청을 데려다 놓는 데까지&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 2층이 할 일이 정해진다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Traefik은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress에 적힌 규칙 &amp;mdash; 지금 서 있는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api/admission이면&lt;span&gt;&amp;nbsp;&lt;/span&gt;queue로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api면&lt;span&gt;&amp;nbsp;&lt;/span&gt;booking으로, 나머지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;frontend로 &amp;mdash; 을 보고 최종 목적지를 고르는 프로그램이고, 그것이 도는 곳은 파드이지&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240이 아니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;목적지를 그 파드 주소로 바꿔야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3에는 그 파드가 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Traefik 파드를 두 개만 띄워 뒀기 때문이다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 진입점이 하나 죽어도 남은 하나가 받도록 둘을 서로 다른 노드에 강제했고, 노드가 셋이니 한 대는 비게 된다. 그리고 1층에서 답할 노드를 고른 기준은 해시 순서였지 &quot;Traefik이 있는 노드&quot;가 아니었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파드는 죽고 다시 뜰 때마다 다른 주소를 받아서 어디에도 적어 둘 수가 없다. 그래서 부를 이름은 Service 쪽에 두고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 Service 뒤에 지금 살아 있는 파드 목록&lt;/b&gt;을 따로 관리한다. 그 목록이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;엔드포인트&lt;/b&gt;다. Traefik Service의 엔드포인트는 지금 파드 둘 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;10.42.207.59(k3s-1)와&lt;span&gt;&amp;nbsp;&lt;/span&gt;10.42.47.249(k3s-2)다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그런데 엔드포인트도 API에 적힌 목록일 뿐이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그 목록을 읽어 실제로 목적지를 갈아 끼워 줄 것이 따로 있어야 하는데, 그 자리가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;kube-proxy&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;proxy라는 이름은 보통 중간에서 요청을 받아 대신 전달하는 것을 뜻하는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;kube-proxy는 그렇게 하지 않는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청은 kube-proxy를 거치지 않고 노드 커널에서 곧장 처리된다. kube-proxy가 하는 일은 Service와 엔드포인트를 쿠버네티스 API에서 지켜보다가, 그 내용을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;커널이 알아들을 규칙으로 번역해 깔아 두는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;목적지가 192.168.0.240:80 이면
   &amp;rarr; 10.42.207.59  (k3s-1 의 Traefik 파드)
   &amp;rarr; 10.42.47.249  (k3s-2 의 Traefik 파드)
   둘 중 하나로 바꿔라&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kube-proxy는 이 규칙을 노드마다 깐다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Traefik 파드를 하나도 안 가진&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3의 커널에도 똑같이 들어 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그래서 앞의 문제가 풀린다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3이 요청을 받아도 그 노드의 커널이&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-1&amp;middot;k3s-2에 있는 파드 주소를 알고 있으니, 그리로 보내면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;k3s-3이 받아 든 요청의 목적지가 여기서 처음 바뀐다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3의 커널이 두 파드 주소 중 하나를 골라 목적지 칸을 갈아 끼운다. 목적지 쪽을 바꾸는 것이라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DNAT&lt;/b&gt;라 부른다. 커널이 패킷마다 고르므로 요청이 자연히 두 파드로 나뉘고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1층에서 한 노드로 몰렸던 것이 여기서 분배된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;고른 파드가&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3이 아닌 다른 노드에 있으면, 그 패킷은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Calico&lt;/b&gt;가 깔아 둔 파드 네트워크를 타고 한 홉 더 간다. 이때&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3의 커널이 출발지 칸도 바꾼다 &amp;mdash; 응답이 돌아올 길을 남기려면 거쳐 간 노드를 출발지로 적어야 하기 때문이다. 출발지 쪽을 바꾸는 것이라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;SNAT&lt;/b&gt;다. 그 결과 Traefik 파드에는 브라우저 주소가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;k3s-3의 주소가 찍히고, 14부에서 이 값이 다시 나온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2층이 끝난 지점은 이렇다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청의 목적지가&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.240:80에서 Traefik 파드 주소로 바뀌었고, 그 파드에 도착했다. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여기까지 아무도 요청 안을 열어 보지 않았다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;1층은 MAC만, 2층은 IP와 포트만 보고 옮겼다. 어떤 이름으로 접속했는지는 아직 아무도 모른다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3층 &amp;middot; Host와 경로를 본다 &amp;rarr; 어느 서비스로 가나&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Traefik ──Host + 경로를 Ingress 규칙과 대조──▶ 앱 파드&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 Traefik이 처음으로 요청을 열어 안을 읽는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;브라우저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Host&lt;span&gt;&amp;nbsp;&lt;/span&gt;헤더에 &quot;어떤 이름으로 접속했는지&quot;를 적어 보냈고, Traefik은 그 값과 경로를 대조할 규칙을 갖고 있다. 그 규칙이 2층에서 이름만 나왔던&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Ingress&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금 이 클러스터에 서 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress는 하나뿐이다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kind: Ingress
spec:
  ingressClassName: traefik      # 누가 이 규칙을 맡나
  rules:
    - http:                      # host 조건이 없다
        paths:
          - path: /api/admission
            backend: { service: { name: queue } }
          - path: /api
            backend: { service: { name: booking } }
          - path: /
            backend: { service: { name: frontend } }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;host&lt;span&gt;&amp;nbsp;&lt;/span&gt;조건이 없다는 것은 &quot;아무거나 다 받는다&quot;는 뜻이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;rules&lt;span&gt;&amp;nbsp;&lt;/span&gt;항목에&lt;span&gt;&amp;nbsp;&lt;/span&gt;host:를 적으면 그 이름으로 온 요청만 그 규칙이 잡는데, 여기엔 안 적혀 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이름을 아직 안 정했기 때문이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts에도 DNS에도 등록한 것이 없으니 적어 봐야 아무도 그 이름으로 올 수 없고, 규칙 뒤에 설 앱 이미지도 아직 없다(10부&amp;middot;11부).&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ingress는 요청이 지나가는 물건이 아니라 이 문서 자체다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;읽어서 실행하는 것은 Traefik이고, 쿠버네티스 API를 지켜보다가&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress가 생기거나 바뀌면 재시작 없이 자기 라우팅 표를 갱신한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ingressClassName이 첫 칸에 있는 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;한 클러스터에 Ingress 컨트롤러가 여럿 있을 수 있어서&lt;/b&gt;다. 컨트롤러는 설치될 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;IngressClass&lt;span&gt;&amp;nbsp;&lt;/span&gt;리소스를 하나 만들어 &quot;이 이름이 적힌&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress는 내가 맡는다&quot;고 선언하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress는 이 한 줄로 담당자를 고른다. 지금 이 클러스터에 선 Ingress 컨트롤러는 Traefik 한 벌뿐이라 이 줄을 비워도 그 컨트롤러가 맡는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Traefik은 읽은 값을 이&lt;span&gt;&amp;nbsp;&lt;/span&gt;rules와 위에서부터 대조한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청 경로가&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api면&lt;span&gt;&amp;nbsp;&lt;/span&gt;path: /api&lt;span&gt;&amp;nbsp;&lt;/span&gt;줄이 걸리고, 그 줄의&lt;span&gt;&amp;nbsp;&lt;/span&gt;backend에 적힌&lt;span&gt;&amp;nbsp;&lt;/span&gt;booking이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;목적지 Service로 정해진다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress가 주는 답은 여기까지 &amp;mdash; 어느 Service냐다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그다음&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Traefik이 그&lt;span&gt;&amp;nbsp;&lt;/span&gt;booking&lt;span&gt;&amp;nbsp;&lt;/span&gt;Service의 엔드포인트에서 앱 파드 하나를 골라 보낸다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;엔드포인트 목록을 자기가 들고 있어서, 여기서는 커널 규칙을 빌리지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3층이 끝난 지점은 이렇다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Traefik이 요청을 열어&lt;span&gt;&amp;nbsp;&lt;/span&gt;Host와 경로를 읽었고, 규칙에서 짝을 찾아&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;뒤에 있는 앱 파드&lt;/b&gt;(queue&amp;middot;booking&amp;middot;frontend&lt;span&gt;&amp;nbsp;&lt;/span&gt;중 하나)로 보냈다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MetalLB가 ARP에 답해 어느 노드까지 데려오고, kube-proxy가 Service와 엔드포인트를 커널 규칙으로 옮겨 어느 파드인지를 정하고(그 파드가 다른 노드면 Calico가 나른다), 그 파드의 Traefik이&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress를 읽어 어느 서비스인지를 정한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;층이 올라갈수록 볼 수 있는 것이 늘고 정해지는 것이 좁아진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그래서 문을 어디에 내면 되나&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ArgoCD도&lt;span&gt;&amp;nbsp;&lt;/span&gt;10.42.x.x에 있는 파드다. 방금 요청이 닿은&lt;span&gt;&amp;nbsp;&lt;/span&gt;queue&amp;middot;booking&amp;middot;frontend와 처지가 같은데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 셋은 밖에서 닿고 ArgoCD는 안 닿는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;차이는 하나뿐이다 &amp;mdash; 앱 셋에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240에서 자기에게로 오는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress&lt;span&gt;&amp;nbsp;&lt;/span&gt;규칙이 있고, ArgoCD에는 없다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;지금 밖에서 닿는 것   192.168.0.240 &amp;rarr; queue &amp;middot; booking &amp;middot; frontend
그 밖 전부           port-forward 로만 &amp;mdash; ArgoCD &amp;middot; Grafana &amp;middot; MinIO &amp;hellip;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앱 3종이&lt;span&gt;&amp;nbsp;&lt;/span&gt;ImagePullBackOff라 지금&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240을 열면 503이긴 하다. 규칙은 서 있고 뒤가 비어 있는 상태이고, 이미지는 10부, 앱 기동은 11부다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그러니 새 문을 뚫을 일이 아니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;MetalLB에 IP를 더 받을 것도, Service를 새로 만들 것도 아니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240에서 Traefik 파드까지 오는 길은 이미 서 있고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;거기서 ArgoCD로 갈라지는 규칙 한 줄이 없을 뿐이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 한 줄에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;host&lt;span&gt;&amp;nbsp;&lt;/span&gt;조건을 단다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;host와 경로는 둘 중 하나를 고르는 게 아니라 축이 둘이다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;host가 어떤 사이트인지를 가르고, 경로가 그 사이트 안 어디인지를 가른다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앱 셋이 경로로 갈린 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;폴리글랏 MSA라 서비스는 셋이어도 브라우저에서는 한 사이트여서&lt;/b&gt;다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;/는 화면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api는 예매,&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api/admission은 대기열 &amp;mdash; 이름 하나 아래에서 경로로 나누는 게 맞다. ArgoCD와 Grafana는 예매 서비스와 관계없는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;각각 별개의 사이트&lt;/b&gt;이고 사람이 따로 연다. 그러니 경로가 아니라 이름으로 갈라야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 다음 절에서 할 일이 셋으로 정해진다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD와 Grafana에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress를 하나씩 세우고, 각각에&lt;span&gt;&amp;nbsp;&lt;/span&gt;host&lt;span&gt;&amp;nbsp;&lt;/span&gt;조건을 달고, 그 이름을&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240으로 풀어 줄 자리를 만든다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그러면&lt;span&gt;&amp;nbsp;&lt;/span&gt;port-forward&lt;span&gt;&amp;nbsp;&lt;/span&gt;없이 이름으로 열린다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.3 이름을 붙인다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이름 둘을 정한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;argocd.cgv.lan과&lt;span&gt;&amp;nbsp;&lt;/span&gt;grafana.cgv.lan&lt;/b&gt;으로 간다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;.local은 mDNS가 예약해 쓰는 자리라 이름 해석이 엉키고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;.lan은 공인 DNS에 올릴 수 없어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;밖에서는 그 이름이 존재하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MetalLB 풀에&lt;span&gt;&amp;nbsp;&lt;/span&gt;.241부터 열 개가 남아 있어 주소를 하나씩 줄 수도 있다. 그렇게 안 하는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주소가 늘면 그 주소마다 방화벽 규칙과 TLS 인증서가 따로 늘기 때문이다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나가 다 받고 이름으로 가르면 인증&amp;middot;TLS를 한 자리에서 건다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;.240에는 이미&lt;span&gt;&amp;nbsp;&lt;/span&gt;host&lt;span&gt;&amp;nbsp;&lt;/span&gt;조건 없는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress가 하나 있다. 거기에&lt;span&gt;&amp;nbsp;&lt;/span&gt;host를 가진 규칙 둘을 더하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;조건이 있는 쪽이 더 구체적이라 먼저 맞고, 조건 없는 쪽이 나머지를 받는 자리로 남는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기존 규칙은 건드리지 않는다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Host: argocd.cgv.lan     &amp;rarr;  argocd-server
Host: grafana.cgv.lan    &amp;rarr;  grafana
그 밖 전부                &amp;rarr;  frontend  (조건 없는 기존 규칙이 받는다)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;값 두 개를 켠다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 차트 모두&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress를 만드는 템플릿을 원래 갖고 있고 꺼져 있을 뿐이다. 차트를 고칠 일은 없고 값만 넘긴다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# bootstrap/argocd/values.yaml
server:
  ingress:
    enabled: true
    ingressClassName: traefik
    hostname: argocd.cgv.lan
    tls: false&lt;/code&gt;&lt;/pre&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# workloads/charts/observability/grafana/values.yaml
ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - grafana.cgv.lan&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;같은 물건을 만드는데 값의 이름이 서로 다르다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;argo-cd는&lt;span&gt;&amp;nbsp;&lt;/span&gt;hostname으로 문자열 하나를 받고, grafana는&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts로 리스트를 받는다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;tls: false는 argo-cd 차트가&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd-server-tls&lt;span&gt;&amp;nbsp;&lt;/span&gt;시크릿을 참조하지 않게 한다 &amp;mdash; 인증서를 아직 발급하지 않았고, ArgoCD 자신이 평문으로 받는 설정과 짝이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;argocd/&lt;span&gt;&amp;nbsp;&lt;/span&gt;아래에서 이 저장소를 가리키는 소스는 전부&lt;span&gt;&amp;nbsp;&lt;/span&gt;targetRevision: main이라, 브랜치에 push해도 ArgoCD는 보지 않는다. 머지한 뒤 grafana 쪽&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress가 먼저 서고 argocd 쪽은 3분 남짓 뒤에 섰다 &amp;mdash; 같은 커밋인데 시차가 나고, 그동안 argocd App은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Synced로 보였다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;왜 그런지는 다음 절에서 ArgoCD를 뜯을 때 풀린다.&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;$ kubectl get ingress -A

NAMESPACE       NAME            CLASS     HOSTS             ADDRESS         PORTS
app             frontend        traefik   *                 192.168.0.240   80
argocd          argocd-server   traefik   argocd.cgv.lan    192.168.0.240   80
observability   grafana         traefik   grafana.cgv.lan   192.168.0.240   80&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HOSTS가&lt;span&gt;&amp;nbsp;&lt;/span&gt;*인 줄이 조건 없는 기존 규칙이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;셋이 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;ADDRESS를 쓴다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; IP는 하나고 이름으로만 갈린다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설치 안내 세 곳에 남아 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;port-forward도 같이 손본다. 지우지 않고 뒤에 붙인다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;부트스트랩 도중에는 Traefik도&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress도 아직 없어서 그 경로가 실제로 유일하다.&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;※ argocd는 traefik(GitOps) 뜨기 전엔 ingress 없음 &amp;rarr; 초기 접근 port-forward
  traefik이 뜬 뒤 argocd.cgv.lan &amp;middot; grafana.cgv.lan
  두 이름은 접근하는 기기의 hosts 또는 DNS가 traefik 주소로 풀어줘야 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이름을 풀어 줄 자리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여는 기기의&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일에 두 줄을 더한다. OS는 DNS 서버에 묻기 전에 이 파일을 먼저 보고, 여기 적힌 이름은 질의 없이 그 IP로 확정된다. Windows에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;C:\Windows\System32\drivers\etc\hosts이고 관리자 권한이 필요하다 &amp;mdash; 이 파일을 고칠 수 있으면 어떤 이름이든 원하는 IP로 돌릴 수 있어서다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;192.168.0.240  argocd.cgv.lan
192.168.0.240  grafana.cgv.lan&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;같은 이름이 두 군데 나오는데 하는 일이 다르다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts의&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd.cgv.lan은 &quot;이 이름이 어느 IP냐&quot;에 답하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress의&lt;span&gt;&amp;nbsp;&lt;/span&gt;host는 &quot;이 이름으로 온 요청을 어디로 보내냐&quot;에 답한다. 앞의 것이 없으면 출발을 못 하고, 뒤의 것이 없으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240까지 가서 조건 없는 규칙으로 흘러 503이 돌아온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;둘을 잇는 것은 브라우저다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://argocd.cgv.lan&quot;&gt;http://argocd.cgv.lan&lt;/a&gt;을 열면 브라우저는 이름을 IP로 바꿔 연결한 다음,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 이름을 요청 안에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Host:&lt;span&gt;&amp;nbsp;&lt;/span&gt;한 줄로 다시 적어 보낸다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이름이 사라지지 않고 따라오기 때문에 Traefik이 어느 이름으로 왔는지 알 수 있고, 한 IP 위에 사이트를 여럿 얹을 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;넣고 나면 두 이름 모두&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.240으로 풀린다. argocd는&lt;span&gt;&amp;nbsp;&lt;/span&gt;200, grafana는 로그인 화면으로 넘기는&lt;span&gt;&amp;nbsp;&lt;/span&gt;302가 돌아온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;829&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xsrvh/dJMcahZEhLb/EuASfxiksYxkPUZdRgoNu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xsrvh/dJMcahZEhLb/EuASfxiksYxkPUZdRgoNu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xsrvh/dJMcahZEhLb/EuASfxiksYxkPUZdRgoNu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxsrvh%2FdJMcahZEhLb%2FEuASfxiksYxkPUZdRgoNu1%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;1392&quot; height=&quot;829&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;829&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이건 파일을 고친 기기 한 대에서만 통한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다른 기기에서 같은 이름을 열려면 그 기기에도 같은 두 줄을 넣어야 하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240이 바뀌면 넣어 둔 곳을 전부 고쳐야 한다. 이름에 답하는 자리를 기기 밖 한 곳으로 옮기는 것이 DNS이고, 그건 클러스터를 밖으로 열 때(14부) 함께 세운다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;무엇이 바뀌었나&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;kubeconfig를 가진 사람에게만 열리던 우회로가, 자격 없이 열리는 문이 됐다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전에는 쿠버네티스가 터널을 내주기 전에 신원을 확인했고, 이제는&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240에 닿을 수 있으면 누구나 로그인 화면까지 온다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그 자리를 ArgoCD와 Grafana의 로그인이 맡는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.4 ArgoCD는 어떻게 돌고 있나&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이름 붙이는 일은 끝났고, 둘째 일로 넘어간다. 커밋과 반영 사이의 대기를 없애려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 대기가 어디서 생기는지&lt;/b&gt;부터 알아야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파드 다섯이 루프 하나를 돈다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;argocd 네임스페이스에 역할이 다른 파드 다섯이 있다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;argocd-server                     UI 와 API.  방금 이름을 붙여 연 곳
argocd-repo-server                git 을 읽어 매니페스트를 렌더한다
argocd-application-controller     Application 하나마다 렌더 결과와 클러스터를 비교하고 적용한다
argocd-applicationset-controller  한 층 위 &amp;mdash; ApplicationSet 을 읽어 Application 여럿을 만든다
argocd-redis                      렌더 결과와 리소스 상태를 담는 캐시&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞의 셋이 App 스물한 개마다 같은 루프를 돈다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;①  repo-server 가 git 을 읽어 렌더한다             ─ 원하는 상태
②  application-controller 가 클러스터와 비교한다     ─ 현재 상태
③  다르면 적용한다                                ─ automated 라 사람 승인 없이&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 루프에 붙은 이름이 GitOps다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;git이 원하는 상태의 정본이고, 클러스터 안의 ArgoCD가 그것을 읽어 스스로 맞춘다. CI가 밖에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;kubectl apply를 쏘는 방식과 갈리는 지점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클러스터 밖 누구도 클러스터를 건드릴 자격을 갖지 않는다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;① repo-server 가 git 을 읽으려면&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;repo-server는 아무 저장소나 읽는 것이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Application에 적힌 대로&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;읽는다. 어느 저장소인지(repoURL), 어느 리비전인지(targetRevision), 무엇을 어떤 값으로 렌더할지(chart&amp;middot;path&amp;middot;valueFiles)가 전부 그 매니페스트에 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;ApplicationSet이 만든 App이면 그 지시서가 자동으로 여럿 찍혀 나온 것뿐이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 지시서를 들고 도는 ①은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이 루프에서 클러스터 밖으로 나가는 단계 하나뿐&lt;/b&gt;이다. 안을 열면 넷으로 갈린다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;(밖 &amp;mdash; GitLab)   targetRevision 에 적힌 브랜치의 지금 커밋 SHA 를 묻는다
(안 &amp;mdash; redis)    그 커밋의 렌더 결과가 있으면 꺼내 쓰고 끝낸다
(밖 &amp;mdash; GitLab)   없으면 저장소를 받아
(안)              valueFiles 를 겹쳐 helm template 을 돌리고 redis 에 넣는다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;밖으로 나가는 자리가 둘이다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 커밋 SHA를 물을 때, 그리고 저장소를 받을 때. 그 둘이 되려면 경로와 자격이 있어야 하고, 이 편에서 만든 것이 정확히 그 둘이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;경로&lt;/b&gt;는 원래 있었다. 노드와 데스크탑이 같은 사설망&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.0/24&lt;span&gt;&amp;nbsp;&lt;/span&gt;안이라 repo-server가&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.167:8929로 나가면 바로 닿는다. GitHub를 읽던 시절에는 공유기를 지나 인터넷으로 나갔을 뿐, 나가는 방향은 막힌 적이 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자격&lt;/b&gt;은 저장소가 private이라 필요하다. 닿아도 거부당하기 때문이다. deploy token을&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd-repo-cgv-infra&lt;span&gt;&amp;nbsp;&lt;/span&gt;Secret으로 봉인해 넣은 것이 이 자리다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가운데 칸에 나온&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd-redis는 데이터베이스가 아니라 캐시라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;파드가 죽어도 잃을 것이 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다시 렌더하느라 느려질 뿐이다. 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;커밋이 그대로면 렌더를 건너뛴다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 확인이 자주 돌아도 매번 App 스물한 개를 통째로 렌더하지는 않는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;② application-controller 가 견주는 두 값 &amp;mdash; watch 와 폴링&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;경로도 자격도 있으니 루프는 돈다. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;application-controller가 견주는 두 값은 갱신되는 방식이 서로 다르다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나는 repo-server가 렌더해 둔 원하는 상태이고, 하나는 클러스터의 현재 상태다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터의 현재 상태는 즉시 안다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;application-controller가 쿠버네티스 API에 watch를 걸어 두어서, 누가 리소스를 손으로 바꾸면 그 순간 통지가 온다. selfHeal이 손댄 것을 곧바로 되돌리는 것이 이 축이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원하는 상태는 물어봐야 안다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;GitLab은 클러스터 밖의 남의 서버라 watch를 걸 수 없다. 그래서 repo-server가 저장소를 주기적으로 다시 읽는다 &amp;mdash; 이것이 폴링이다. 주기는 argo-cd 차트가 주는&lt;span&gt;&amp;nbsp;&lt;/span&gt;timeout.reconciliation: 120s에 지터&lt;span&gt;&amp;nbsp;&lt;/span&gt;60s가 붙어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;한 번의 대기가 2분에서 3분 사이&lt;/b&gt;이고, 지터는 App 스물한 개의 확인이 한 순간에 몰리는 것을 흩는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Synced가 뜻하는 것도 좁다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;repo-server가 마지막으로 읽은 커밋 기준으로 클러스터가 같다는 뜻이지, 저장소의 최신 커밋 기준이 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기까지가 지금 도는 방식이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이 대기를 없애는 다른 길이 webhook이고, 둘을 견줘 정한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.5 webhook으로 바꾼다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;얻는 것 &amp;mdash; ArgoCD 문서&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #000000; color: #333333; text-align: center;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;Argo CD polls Git/OCI/Helm repositories every three minutes to detect changes to the manifests.&lt;/p&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;To eliminate this delay from polling, the API server can be configured to receive webhook events.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/&quot;&gt;Argo CD, Webhook Configuration&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폴링의 방향을 뒤집는 것이다. ArgoCD가 물으러 가는 대신,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitLab이 push를 받는 순간 ArgoCD의 수신 주소를 불러 &quot;다시 읽어라&quot;를 그 자리에서 당긴다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;지금        커밋 &amp;rarr; (최대 3분 대기) &amp;rarr; 읽는다 &amp;rarr; 비교 &amp;rarr; 적용
webhook 뒤   커밋 &amp;rarr; (알림 즉시)    &amp;rarr; 읽는다 &amp;rarr; 비교 &amp;rarr; 적용&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞당겨지는 건 읽기 시작하는 시점 하나라, 루프는 그대로 두고 대기만 걷어낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;받는 자리를 문서는 API 서버 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd-server&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 로 지정한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;방금 이름을 붙여 연 그 파드다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사람이 브라우저로 들어가는 문을 GitLab도 쓰는 것이라 새로 낼 것이 없고, 두드릴 주소만 정해진다:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://argocd.cgv.lan/api/webhook&quot;&gt;http://argocd.cgv.lan/api/webhook&lt;/a&gt;.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래도 폴링을 남기는 이유 &amp;mdash; GitLab 문서&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #000000; color: #333333; text-align: center;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;Webhooks are temporarily disabled if they fail four consecutive times.&lt;/p&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;Temporarily disabled webhooks are initially disabled for one minute, with the duration extending on subsequent failures up to 24 hours.&lt;/p&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;To re-enable a disabled webhook, send a test request. The webhook is re-enabled if the test request returns a response code in the&lt;span&gt;&amp;nbsp;&lt;/span&gt;2xx&lt;span&gt;&amp;nbsp;&lt;/span&gt;range.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.gitlab.com/user/project/integrations/webhooks/&quot;&gt;GitLab, Webhooks&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실패한 전송을 다시 보내지 않고, 네 번 연속이면 GitLab이 그 webhook을 스스로 끈다. 다시 켜는 조건이 사람이 보내는 테스트 요청이라 아무도 안 보내면 최대 24시간까지 꺼져 있고, 꺼졌다고 알려 주는 장치도 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 조건이 이 홈랩에서 자주 성립한다. 데스크탑과 노트북을 따로 켜므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;노트북(클러스터)이 꺼진 채 push를 몇 번 하면 네 번이 채워진다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;webhook만 믿고 있었다면 그때부터 커밋이 조용히 안 반영된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; webhook 단독으로는 반영이 보장되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 둘을 같이 둔다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;webhook이 평소에 즉시 반영하고(최대 3분 &amp;rarr; 초), 폴링이 webhook이 놓친 것을 줍는다. webhook이 꺼져 있어도 최악 3분이면 따라잡는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폴링을 남기기로 했으니&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주기를 얼마로 둘지&lt;/b&gt;가 결정 하나로 남는다. 흔한 권고는 webhook이 도는 것을 확인한 뒤 15분&amp;middot;1시간으로 늘리라는 것이고, 근거는 규모다 &amp;mdash; App이 수백 개면 짧은 폴링이 repo-server 부하와 프로바이더 rate limit으로 돌아온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App 21개에 Git 서버가 내 데스크탑이면 그 전제가 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;늘려서 얻는 것은 측정되지 않는데, webhook이 꺼져 있을 때의 최악 대기는 3분에서 30분이 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본값을 그대로 둔다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클러스터나 App이 크게 늘면 이 계산이 뒤집힌다 &amp;mdash; 그때는 폴링을 늘리고 &quot;webhook이 살아 있나&quot;를 보는 장치를 따로 붙인다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.6 webhook을 건다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두드릴 주소는 이미 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd.cgv.lan에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress로 이름을 붙였고 브라우저에서 열리는 것도 봤다. 남은 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitLab이 그 주소를 두드리게&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만드는 일이다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;지금       ArgoCD ──&quot;바뀐 거 있어?&quot;──▶ GitLab      폴링.  최대 3분
webhook    GitLab ──&quot;방금 바뀌었다&quot;──▶ ArgoCD      주소가 있어야 성립&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 주소가 있다고 바로 되지 않는다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;넘어야 할 것이 셋이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나 &amp;mdash; GitLab 이 사설 IP 로는 webhook 을 안 보낸다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;webhook은 GitLab이 HTTP 요청을 하나 보내는 것이고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;보낼 주소는 사람이 폼에 적는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;여기서 문제가 생긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;GitLab은 보통 여러 사람이 쓰는 서버이고, 그 서버는 어떤 망 안에 놓여 있다. 그 망에는 밖에서 못 닿는 것들이 있다 &amp;mdash; 관리 페이지, DB, 클라우드 메타데이터 서버. GitLab이 폼에 적힌 주소를 검사 없이 두드린다면 이렇게 된다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;공격자가 프로젝트를 하나 만들고
webhook URL 에  http://10.0.0.5/admin/...  을 적는다
커밋 하나 push
   &amp;rarr; GitLab 서버가 그 주소를 대신 두드린다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공격자는 사내망에 못 들어가는데 GitLab을 시켜서 들어갔다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;서버를 속여 대신 요청을 보내게 하는 이 패턴을 SSRF(Server-Side Request Forgery)라 부른다. 그래서 GitLab은 webhook이&lt;span&gt;&amp;nbsp;&lt;/span&gt;10.x&amp;middot;192.168.x&amp;middot;127.0.0.1&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 사설 대역으로 나가는 것을 기본으로 막는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리 주소가 거기 걸린다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd.cgv.lan은&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.240으로 풀리고 그건 사설망이다. 그대로 등록하거나 Test를 누르면 GitLab이 거부한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;푸는 자리는 Admin &amp;rarr; Settings &amp;rarr; Network &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Outbound requests&lt;/b&gt;다. 전체를 열 수도 있고 허용 목록에 주소 하나만 넣을 수도 있는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;후자로 간다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 여는 범위가 좁다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 목록에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;IP가 아니라 이름을 적는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;GitLab의 허용 목록은 도메인 이름도 받는다. 이름으로 적어 두면 나중에 그 이름이 가리키는 IP가 바뀌어도 이 설정은 손댈 것이 없다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘 &amp;mdash; GitLab 은 그 이름을 못 푼다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데스크탑&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts에 두 줄을 넣어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd.cgv.lan이&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.0.240으로 풀린다. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitLab은 그 데스크탑에서 도는 Docker 컨테이너다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너는 자기 파일시스템을 갖는다. 호스트의&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 안 보고, 컨테이너 안의&lt;span&gt;&amp;nbsp;&lt;/span&gt;/etc/hosts에는 그 두 줄이 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그래서 GitLab이 그 이름을 두드리려 하면 이름을 못 풀어 출발조차 못 한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사설망 차단을 풀어도 이건 따로 막힌다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이름을 푸는 자리는 기기마다 따로 있는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;같은 데스크탑 안이라도 컨테이너는 다른 기기처럼 군다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;푸는 법은 compose에 같은 매핑을 적는 것이다. 컨테이너가 뜰 때 그 줄이 컨테이너의&lt;span&gt;&amp;nbsp;&lt;/span&gt;/etc/hosts에 들어간다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;services:
  gitlab:
    extra_hosts:
      - &quot;argocd.cgv.lan:192.168.0.240&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이름만 적을 수 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일이 하는 일이 이름을 IP로 바꾸는 것이라 IP가 따라 붙는다. 앞의 허용 목록과 성격이 갈린다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;GitLab 허용 목록      &quot;이 이름으로 나가는 건 허용&quot;        이름만
compose extra_hosts   &quot;이 이름은 이 IP 다&quot;              이름 + IP&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 이 줄은 임시다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;데스크탑&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 줄과 같은 처지로, 이름 푸는 자리를 손으로 채운 것이다. 내부 DNS가 서면 컨테이너도 그 DNS에 물어보면 되니 이 줄은 없어진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셋 &amp;mdash; 그 주소는 로그인을 안 묻는다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브라우저로&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd.cgv.lan을 열면 로그인 화면이 뜬다. 그런데 같은 주소라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api/webhook&lt;span&gt;&amp;nbsp;&lt;/span&gt;경로만은 로그인을 안 묻는다. 요청 둘을 나란히 놓으면 보인다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;GET  /applications HTTP/1.1          브라우저가 화면을 열 때
Host: argocd.cgv.lan
Cookie: argocd.token=eyJhbGci...     &amp;larr; 로그인해서 받은 것

POST /api/webhook HTTP/1.1           GitLab 이 알릴 때
Host: argocd.cgv.lan
X-Gitlab-Token: a3f9c2...            &amp;larr; 공유 비밀
{ &quot;ref&quot;: &quot;refs/heads/main&quot;, ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래에는 쿠키가 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitLab에는 ArgoCD 계정이 없어서 받아 둘 쿠키도 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그래서 ArgoCD는&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api/webhook&lt;span&gt;&amp;nbsp;&lt;/span&gt;경로에서 쿠키를 찾지 않는다 &amp;mdash; 찾는다면 GitLab이 보낸 요청이 전부 로그인 화면으로 돌아가고 webhook은 성립하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿠키를 안 보니 그 자리가 열려 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주소를 아는 쪽이면 누구나 같은 POST를 흉내낼 수 있다. 받아서 하는 일은 &quot;저장소를 다시 읽어라&quot; 하나뿐이라 배포 내용이 바뀌지도 저장소 자격이 새지도 않고, 최악이 반복 요청으로 repo-server에 부하를 주는 정도다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그래서 안 걸어도 webhook은 돈다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기에 값 하나를 걸어 좁힌다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;무작위 값을 하나 만들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitLab 폼과 ArgoCD 양쪽에 같은 것을 넣는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;GitLab이 요청마다 그 값을&lt;span&gt;&amp;nbsp;&lt;/span&gt;X-Gitlab-Token에 실어 보내고, ArgoCD는 자기가 가진 것과 같은지만 본다. 쿠키 대신 볼 줄을 하나 정해 둔 셈이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쿠키&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 로그인한 사람. 권한이 따라온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;X-Gitlab-Token&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 아는 쪽에서 온 요청. 권한은 안 따라온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 편에서 만든 자격이 하나 더 있어 헷갈리기 쉬운데, 서로 다른 물건이다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;deploy token       GitLab 이 발급.  ArgoCD 가 저장소를 읽을 때 쓴다
                   ArgoCD &amp;rarr; GitLab

webhook 공유 비밀    우리가 만들어 양쪽에 같게 넣는다
                   GitLab &amp;rarr; ArgoCD&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금 위험이 큰 것은 아니다. 사설망 안이고 요청이 시키는 일도 하나뿐이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그래도 넣는 것은 14부 때문이다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 클러스터를 밖으로 열 때 비밀 없는 열린 엔드포인트가 남아 있으면 그 자리로 다시 돌아와야 한다. 지금 드는 것은 봉인본 한 장과 문서 세 곳이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비밀을 argocd-secret 에 얹는다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;argo-cd 차트에&lt;span&gt;&amp;nbsp;&lt;/span&gt;configs.secret.gitlabSecret&lt;span&gt;&amp;nbsp;&lt;/span&gt;칸이 있어서 값을 넣으면 차트가&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd-secret에 담아 준다. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;values 파일은 평문이고 이 저장소는 GitHub로 미러된다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 칸은 비워 두고 봉인해서 넣는데, 거기서 제약이 하나씩 딸려 나온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;건다&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;1  비밀을 만들어 봉인하고 커밋한다        argocd-secret 에 한 칸
2  GitLab Admin 에서 사설망 차단을 푼다   허용 목록에 이름으로
3  compose 에 extra_hosts 를 넣고 다시 올린다
4  프로젝트 Webhooks 에 등록하고 Test 를 누른다
5  커밋 하나로 반영 시간을 잰다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;8. 걸고, 대조한다&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8.1 다섯 단계를 실제로&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1 &amp;mdash; 비밀을 만들어 봉인한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;값은 아무 난수면 된다. 양쪽에 같기만 하면 되는 것이라 의미 있는 문자열일 필요가 없다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;openssl rand -hex 32&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 값을 GitLab 폼에도 넣어야 하니 봉인이 끝나도 지우지 않는다. 봉인은 낱개 스크립트로 한다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;./seal-one.sh -a sealedsecrets.bitnami.com/patch=true argocd-secret argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;키를 물으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;webhook.gitlab.secret, 값에 위의 난수를 넣는다. 값 칸은 화면에 안 뜨고 글자 수만 나온다. 나온 봉인본을 커밋해 머지하면 GitOps가 배달한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 한 번 걸렸다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;컨트롤러가 거부했다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;failed update: Resource &quot;argocd-secret&quot; already exists and is not managed by SealedSecret&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;patch&lt;span&gt;&amp;nbsp;&lt;/span&gt;애노테이션을 봉인본에 적었는데도 그랬다. 원인은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컨트롤러가 그 애노테이션을 봉인본이 아니라 클러스터에 이미 있는 Secret에서 읽기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;if !IsControlledBy(secret, ssecret) &amp;amp;&amp;amp; !isAnnotatedToBeManaged(secret)
                                    &amp;amp;&amp;amp; !isAnnotatedToBePatched(secret)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;닭과 달걀이다. 봉인본이 Secret을 고쳐야 애노테이션이 붙는데, 애노테이션이 붙어 있어야 고칠 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;argocd-secret은 argo-cd 차트가 먼저 만들어 둔 것이라 여기 걸린다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;처음 한 번만 손으로 붙이면 풀린다.&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;kubectl -n argocd annotate secret argocd-secret sealedsecrets.bitnami.com/patch=true&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한 번 통과하고 나면 봉인본의 애노테이션이 Secret에 남아 이후로는 계속 통과한다. 저장소 자격 한 장을&lt;span&gt;&amp;nbsp;&lt;/span&gt;root-app.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;전에 손으로 apply하는 것과 같은 성격의 선행 단계이고, 계약 문서에 그렇게 적어 뒀다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;붙이고 나서 키가 넷이 됐다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기존 셋이 살아남은 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;patch가 일했다는 증거다.&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;admin.password          &amp;larr; 살아남음
admin.passwordMtime     &amp;larr; 살아남음
server.secretkey        &amp;larr; 살아남음
webhook.gitlab.secret   &amp;larr; 새로 얹힘&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2 &amp;mdash; GitLab 이 사설 IP 로 나가게 허용한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Admin &amp;rarr; Settings &amp;rarr; Network &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Outbound requests&lt;/b&gt;. 여는 방법이 둘인데 범위가 다르다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Allow requests to the local network from webhooks    사설망 전체가 열린다
allowlist 에 주소 하나                                그 주소만 열린다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;뒤를 골랐다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그리고 허용 목록에는 IP가 아니라 이름을 적었다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;argocd.cgv.lan&lt;/b&gt;. 이 목록은 도메인 이름도 받으므로, 나중에 그 이름이 가리키는 IP가 바뀌어도 이 설정은 손댈 것이 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Allow requests to the local network from system hooks는 기본이 켜짐인데 껐다. system hook은 관리자용이라 프로젝트 webhook과 다른 물건이고, 하나도 안 쓰고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnR760/dJMcab6fVFw/IIGZqPtGkLExqGDPiGanzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnR760/dJMcab6fVFw/IIGZqPtGkLExqGDPiGanzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnR760/dJMcab6fVFw/IIGZqPtGkLExqGDPiGanzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnR760%2FdJMcab6fVFw%2FIIGZqPtGkLExqGDPiGanzk%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;1164&quot; height=&quot;535&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3 &amp;mdash; GitLab 컨테이너가 그 이름을 풀게 한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;argocd.cgv.lan은 데스크탑&lt;span&gt;&amp;nbsp;&lt;/span&gt;hosts&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일에만 있는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GitLab은 그 데스크탑에서 도는 컨테이너라 그 파일을 안 읽는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;컨테이너 안의&lt;span&gt;&amp;nbsp;&lt;/span&gt;/etc/hosts에는 그 줄이 없어서 이름을 못 풀고 출발조차 못 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;compose에 같은 매핑을 적으면 컨테이너가 뜰 때 그 줄이 들어간다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;services:
  gitlab:
    extra_hosts:
      - 'argocd.cgv.lan:192.168.0.240'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다시 올리고 컨테이너 안에서 확인한다. 이름이 풀리는지와 그 주소가 실제로 응답하는지는 별개라 둘 다 본다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;docker exec gitlab getent hosts argocd.cgv.lan
#   192.168.0.240   argocd.cgv.lan

docker exec gitlab curl -s -o /dev/null -w &quot;%{http_code}\n&quot; http://argocd.cgv.lan/
#   200&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;200이 나오면 컨테이너에서 Traefik을 지나 argocd-server까지 통한 것이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여기까지 확인하고 등록하면, 실패했을 때 원인이 GitLab 설정 하나로 좁혀진다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4 &amp;mdash; webhook 을 등록한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 &amp;rarr; Settings &amp;rarr; Webhooks.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;URL             http://argocd.cgv.lan/api/webhook
Secret token    1 에서 만든 그 값
Trigger         Push events&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토큰 칸이 둘인데&lt;span&gt;&amp;nbsp;&lt;/span&gt;Secret token을 쓴다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;GitLab은 그 칸에 &quot;not recommended&quot;를 붙여 두고&lt;span&gt;&amp;nbsp;&lt;/span&gt;Signing token을 권한다 &amp;mdash; 값을 그대로 보내지 않고 서명만 보내는 방식이라 더 안전하다. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ArgoCD가 검사하는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;X-Gitlab-Token&lt;span&gt;&amp;nbsp;&lt;/span&gt;헤더 하나뿐이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;tokenHash := sha512.Sum512([]byte(r.Header.Get(&quot;X-Gitlab-Token&quot;)))
if subtle.ConstantTimeCompare(tokenHash[:], hook.secretHash[:]) == 0 {
    return nil, ErrGitLabTokenVerificationFailed
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서명 헤더는 아예 보지 않으므로 선택지가 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;권장되지 않는 쪽을 고른 것이고, 그 이유가 우리 판단이 아니라 받는 쪽의 제약이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Test &amp;rarr; Push events&lt;/b&gt;를 누르면&lt;span&gt;&amp;nbsp;&lt;/span&gt;200이 온다. 값이 틀렸으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;403이므로, 이&lt;span&gt;&amp;nbsp;&lt;/span&gt;200은 토큰까지 맞았다는 뜻이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CmFul/dJMcaafe5T5/3TuANXVNVbHNMHsb3EK7q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CmFul/dJMcaafe5T5/3TuANXVNVbHNMHsb3EK7q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CmFul/dJMcaafe5T5/3TuANXVNVbHNMHsb3EK7q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCmFul%2FdJMcaafe5T5%2F3TuANXVNVbHNMHsb3EK7q0%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;1181&quot; height=&quot;418&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5 &amp;mdash; 반영 시간을 잰다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;커밋 하나를&lt;span&gt;&amp;nbsp;&lt;/span&gt;main에 넣고 ArgoCD 로그를 본다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;22:11:43 +09:00   머지 커밋
13:11:46 UTC      refreshing app 'argocd' from webhook
                  refreshing app 'root'  from webhook
                  ...  App 스물한 개 전부&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3초다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴링이었으면 최대 3분 기다렸을 자리다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그에 하나 더 찍혔다. 머지되면서 소스 브랜치가 삭제됐는데 그때도 이벤트가 왔고, ArgoCD가 이렇게 넘겼다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Received push event repo: .../cgv-infra, revision: fix/sealedsecret-patch-bootstrap,
                          touchedHead: false&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main이 안 바뀌었으니 아무것도 안 한다&lt;/b&gt;는 판단이다. webhook은 push마다 오지만 그중 반영이 필요한 것만 걸러 낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8.2 닫는 조건과 대조&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 편이 열 때 잡은 자리는 둘이었다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;push 마다 최대 3분을 기다린다        &amp;rarr;  3초.  webhook 이 알리고 폴링은 그물로 남는다
ArgoCD 가 port-forward 로만 열린다   &amp;rarr;  argocd.cgv.lan.  Grafana 도 같이 열렸다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;둘 다 닫혔다. 그 사이에 원본도 옮겨졌다 &amp;mdash; Application 스물한 개의&lt;span&gt;&amp;nbsp;&lt;/span&gt;repoURL이 전부 데스크탑 GitLab을 가리키고, ArgoCD는 자기 설정까지 커밋으로 받는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8.3 무엇이 함께 열렸나&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문을 내면 그만큼 열린다. 이 편에서 늘어난 것을 적어 둔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ArgoCD와 Grafana의 로그인 화면이 사설망에 드러났다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전에는 kubeconfig가 없으면 화면을 볼 수도 없었는데, 이제&lt;span&gt;&amp;nbsp;&lt;/span&gt;.240에 닿을 수 있으면 누구나 로그인 화면까지 온다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비밀번호를 반복해 넣어 볼 대상이 생긴 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;신원을 확인하는 주체가 바뀌었다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿠버네티스가 인증서와 RBAC으로 사람마다 권한을 가르던 자리를, ArgoCD와 Grafana의 로컬 계정 하나가 대신한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그 비밀번호가 평문으로 오간다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전 구간이 HTTP라 같은 망에서 패킷을 보면 읽힌다. webhook의 공유 비밀도 매 요청 헤더에 값 그대로 실려 간다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitLab이 사설망으로 나가는 것을 의도적으로 풀었다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다만 전체가 아니라 이름 하나만 열었고, 그 이름은 공인 DNS에 존재하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞의 셋은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;TLS를 붙이면 성격이 달라진다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;14부에서 Traefik 한 곳에 인증서를 올리고, 망을 갈라 관리용 주소를 밖에서 못 닿게 두고, 방화벽으로 들어오는 쪽을 막는다. 지금은 사설망 안이라는 것 하나에 기대고 있는 상태이고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그게 방어의 전부라는 것을 알고 있는 것과 모르고 있는 것이 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;9. 다음&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금 클러스터에서 앱 셋(frontend&amp;middot;booking&amp;middot;queue)만&lt;span&gt;&amp;nbsp;&lt;/span&gt;Degraded다. 매니페스트도 서 있고&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingress&lt;span&gt;&amp;nbsp;&lt;/span&gt;규칙도 있는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이미지가 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;registry.cgv.local을 가리키고 있고 그런 레지스트리가 아직 없어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;ImagePullBackOff에서 멈춰 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;10부가 그 구간을 잇는다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;지금    소스 &amp;rarr; GitLab &amp;rarr; (끊김) &amp;rarr; 클러스터
10부    소스 &amp;rarr; GitLab &amp;rarr; CI 가 빌드 &amp;rarr; 레지스트리 &amp;rarr; 클러스터&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;세울 것은 셋이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;러너&lt;/b&gt;가 CI를 돌릴 자리고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;레지스트리&lt;/b&gt;가 나온 이미지를 둘 자리고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;파이프라인&lt;/b&gt;이 그 둘을 잇는 규칙이다. 레지스트리는 GitLab 내장을 쓴다 &amp;mdash; Harbor를 따로 세우지 않는 것과 같은 이유로, 이미 있는 것을 쓰면 자격과 백업이 한 자리에 모인다.&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/218</guid>
      <comments>https://zed6740.tistory.com/218#entry218comment</comments>
      <pubDate>Tue, 4 Aug 2026 09:49:11 +0900</pubDate>
    </item>
    <item>
      <title>노트북 홈서버 k3s 구축기 - 8. 관측 파이프라인</title>
      <link>https://zed6740.tistory.com/217</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 이어받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7부 끝에서 토대와 워크로드가 다 섰다. 노드 세 대가 &lt;code&gt;Ready&lt;/code&gt;, 파드 69개, 데이터 디스크 열 장이 전부 &lt;code&gt;Bound&lt;/code&gt;. 플랫폼(Calico&amp;middot;MetalLB&amp;middot;Traefik&amp;middot;cert-manager&amp;middot;sealed-secrets&amp;middot;ArgoCD)과 데이터 계층(MySQL&amp;middot;Redis&amp;middot;Kafka), 관측 스택(Mimir&amp;middot;Loki&amp;middot;Tempo&amp;middot;Grafana&amp;middot;Alloy&amp;middot;MinIO)이 올라왔고, 손을 떼고 GitOps에 넘겼다.&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;/b&gt; queue&amp;middot;booking&amp;middot;frontend의 Deployment&amp;middot;Service는 클러스터에 있는데, 이미지를 받아올 레지스트리가 없어 &lt;code&gt;ImagePullBackOff&lt;/code&gt;다. 파드 69개 중 4개가 그 상태고, 레지스트리는 10부에서 세운다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grafana 안이 백지다.&lt;/b&gt; 대시보드가 한 장도 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 채울 건 이렇다 &amp;mdash; &lt;b&gt;관측&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;b&gt;열어 볼 파이프라인&lt;/b&gt;은 이런 모양이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[긁는 쪽]                    [담는 쪽]                    [보는 쪽]

Alloy (노드마다) ─ 메트릭 ──▶ Mimir  ┐
                 ─ 로그   ──▶ Loki   ├─ 몸통은 MinIO(S3)로 ──▶ Grafana
                 ─ 트레이스 ─▶ Tempo  ┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;긁는 쪽&lt;/b&gt;과&lt;b&gt; 담는 쪽&lt;/b&gt;의 구조를 먼저 본다. 담는 쪽을 모르면 &quot;값이 안 보인다&quot;가 수집 문제인지 저장 문제인지 가를 수가 없다. 그리고 그 전에 배포판(k3s)의 생김새부터 본다 &amp;mdash; 뒤에서 메트릭이 그 구조를 그대로 비춘다.&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;다 &amp;mdash; 노드 자원, 컨트롤플레인 상태, 컨테이너 실사용, 그리고 관측 스택이 자기 자신에 대해 내는 지표.&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;7부가 남긴 숙제도 하나 걸려 있다. 노드 자원 예약(&lt;code&gt;kube-reserved&lt;/code&gt;)을 2Gi로 올려 잡기로 했는데 설정 파일에만 반영하고 노드에는 아직 안 넣었다. 반영하려면 노드를 한 대씩 재시작해야 하고, 앱이 올라와 실사용을 다시 잰 뒤 한 번에 하기로 미뤄 뒀다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;1. k3s는 어떻게 생겼나&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 컨트롤플레인 넷이 프로세스 하나 안에 있다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubeadm으로 세운 클러스터는 컨트롤플레인 컴포넌트가 각각 스태틱 파드로 뜬다. apiserver 파드, scheduler 파드, controller-manager 파드, etcd 파드 &amp;mdash; 넷이 서로 다른 컨테이너이자 서로 다른 프로세스다.&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;k3s는 이 넷을 하나로 합쳤다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[프로세스] k3s server   (systemd 유닛 하나)
    ├ apiserver           (고루틴)
    ├ etcd (embedded)     (고루틴)
    ├ scheduler           (고루틴)
    ├ controller-manager  (고루틴)
    ├ kubelet             (고루틴)
    └ kube-proxy          (고루틴)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 노드 전부 server 역할이라 이 프로세스가 노드마다 하나씩, 클러스터에 셋 있다.&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; k3s는 저사양&amp;middot;엣지&amp;middot;단일 바이너리를 노리고 만든 배포판이다. 컴포넌트를 프로세스로 나누면 각각 컨테이너 이미지와 매니페스트가 필요하고, 기동 순서와 인증서 배선을 따로 맞춰야 한다. 그걸 하나로 합치면 바이너리 하나 내려받아 systemd 유닛 하나 띄우는 것으로 클러스터가 선다. 설치가 단순해지는 대신 뒤에 나오는 성질들이 따라온다.&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;kubelet은 자기가 띄운 파드만 셀 수 있다.&lt;/b&gt; kubeadm 쪽 컨트롤플레인은 스태틱 파드라 kubelet이 띄우고 관리하므로 그 사용량이 파드 몫으로 잡히고, 노드의 할당 가능량에서도 그만큼 빠진다.&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;code&gt;k3s server&lt;/code&gt;는 파드가 아니라 systemd 프로세스다. kubelet의 셈에 아예 안 들어온다. 그래서 그 몫을 &lt;code&gt;kube-reserved&lt;/code&gt;로 직접 떼어 두지 않으면, 실제로는 쓰이고 있는데 &lt;b&gt;스케줄러 쪽에서는 비어 있는 공간&lt;/b&gt;으로 남아 파드에게 배정된다. 실측으로 노드당 1.5&amp;ndash;1.8GiB를 쓰고 있어 예약을 2Gi로 잡았다.&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;같은 이유로 PriorityClass로 컨트롤플레인을 보호할 수도 없다. 파드가 아니니 eviction 대상 목록에 없다.&lt;/p&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 나머지는 전부 파드다&lt;/b&gt;&lt;/h4&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;무엇이 떠 있나&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;b&gt;Calico &amp;middot; MetalLB &amp;middot; Traefik &amp;middot; cert-manager &amp;middot; sealed-secrets &amp;middot; ArgoCD&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;노드에 분산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MySQL &amp;middot; Redis 3 &amp;middot; Kafka(Strimzi)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;디스크 라벨로 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관측 &amp;mdash; 수집&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Alloy&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DaemonSet, 노드마다 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관측 &amp;mdash; 저장&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Mimir(인제스터 3 + 컴포넌트 8) &amp;middot; Loki 1 &amp;middot; Tempo 1 &amp;middot; MinIO 1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인제스터는 노드마다 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관측 &amp;mdash; 조회&amp;middot;부가&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Grafana &amp;middot; kube-state-metrics &amp;middot; node-exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;node-exporter는 DaemonSet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;앱&lt;/td&gt;
&lt;td&gt;&lt;b&gt;queue 2 &amp;middot; booking &amp;middot; frontend&lt;/b&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;배치가 아무 데나 앉은 것은 아니다. 노드마다 용도 라벨(&lt;code&gt;cgv.io/data&lt;/code&gt; = db &amp;middot; obs &amp;middot; obj)을 붙여 뒀고, &lt;b&gt;디스크를 쓰는 것은 그 디스크가 있는 노드에 고정&lt;/b&gt;된다. MySQL은 데이터 디스크가 있는 노드로, Loki&amp;middot;Tempo는 각자 WAL 디스크가 있는 노드로, MinIO는 100G 디스크가 있는 노드로 간다.&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;Mimir 인제스터만 다르다. &lt;b&gt;세 대가 노드에 하나씩&lt;/b&gt; 앉는다. 노드마다 인제스터용 WAL 디스크를 한 장씩 잘라 둔 것이 그래서다. 왜 이렇게 흩어 뒀는지는 담는 쪽 구조를 볼 때 이어서 나온다.&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;파드는 각자 프로세스라 자기 메모리를 갖고, 컨테이너 단위로 limit을 걸 수 있고, 하나가 죽어도 옆이 안 죽는다.&lt;/b&gt; 합쳐 놓은 넷은 그 반대다.&lt;/p&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;&lt;b&gt;1.3 합쳐 놓은 대가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go에서 고루틴이 패닉하고 아무도 받지 않으면 프로세스 전체가 죽는다. 그래서 &quot;apiserver만 죽고 kubelet은 산다&quot;가 k3s에는 없다. 죽으면 그 노드의 컨트롤플레인이 통째로 나간다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;kubeadm(스태틱 파드)&lt;/th&gt;
&lt;th&gt;k3s(한 프로세스)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컴포넌트 하나가 패닉&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;그 컨테이너만 재시작&lt;/td&gt;
&lt;td&gt;&lt;b&gt;넷이 함께 종료&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메모리 폭주&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;해당 컨테이너만 OOMKilled&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프로세스 전체 OOM&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컴포넌트별 메모리 limit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;걸 수 있다&lt;/td&gt;
&lt;td&gt;&lt;b&gt;못 건다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;재시작 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;죽은 것 하나&lt;/td&gt;
&lt;td&gt;&lt;b&gt;넷 전부 + etcd WAL 재생&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 관측 쪽으로 바로 걸리는 것이 하나 더 있다. &lt;b&gt;컴포넌트별 자원을 따로 잴 수가 없다.&lt;/b&gt; &lt;b&gt;프로세스가 하나라 &quot;메모리를 apiserver가 먹었나 kubelet이 먹었나&quot;가 안 갈린다.&lt;/b&gt; 프로세스 사용량 지표는 &lt;code&gt;k3s server&lt;/code&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;노드 단위&lt;/b&gt;로 만든다. 세 대를 전부 server로 두고 etcd를 3-member로 묶은 것이 그 대응이다. 한 노드의 &lt;code&gt;k3s server&lt;/code&gt;가 통째로 죽어도 남은 두 대로 쿼럼이 유지되고 apiserver도 계속 답한다. 같은 이유로 설정을 바꿔 재시작할 때 세 노드를 동시에 만지지 않는다 &amp;mdash; 한 대씩 순차로 돌린다. 동시에 내리면 쿼럼이 끊긴다.&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;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;h2 data-ke-size=&quot;size26&quot;&gt;2. Alloy는 무엇을 어떻게 긁나&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 &quot;긁는다&quot;가 무슨 뜻인가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메트릭을 내는 쪽은 HTTP 주소를 하나 열어 둔다. &lt;b&gt;&lt;code&gt;주소:포트/metrics&lt;/code&gt;로 요청하면 그 순간의 값이 텍스트로 돌아온다.&lt;/b&gt; 한 줄이 값 하나다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;apiserver_request_total{verb=&quot;GET&quot;,resource=&quot;pods&quot;,code=&quot;200&quot;} 41823
node_cpu_seconds_total{cpu=&quot;0&quot;,mode=&quot;idle&quot;} 918273.44&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수집기(alloy)&lt;/b&gt;가 하는 일은 하나다. &lt;b&gt;그 주소로 주기적으로 요청을 보내고, 돌아온 텍스트를 저장소로 넘긴다.&lt;/b&gt; 이 한 번의 요청&amp;middot;응답을 &lt;b&gt;스크레이프(scrape)&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;시리즈&lt;/b&gt; &amp;mdash; &lt;b&gt;메트릭 이름 하나에 라벨 조합 하나가 붙은 것.&lt;/b&gt; 위 예에서 &lt;code&gt;verb=&quot;GET&quot;&lt;/code&gt;과 &lt;code&gt;verb=&quot;LIST&quot;&lt;/code&gt;는 서로 다른 시리즈다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;샘플&lt;/b&gt; &amp;mdash; &lt;b&gt;그 시리즈의 한 시점 값 하나.&lt;/b&gt; 시리즈 하나를 15초마다 긁으면 15초마다 샘플이 하나 생긴다.&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;up{~}&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 수집기인 Alloy는 스크레이프마다 값을 하나 &lt;b&gt;직접 만들어 붙인다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;up{job=&quot;kubelet&quot;, instance=&quot;k3s-1&quot;} 1     &amp;larr; 붙는 데 성공
up{job=&quot;kubelet&quot;, instance=&quot;k3s-2&quot;} 0     &amp;larr; 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&lt;b&gt;up&lt;/b&gt;&lt;/code&gt;은 긁히는 쪽이 내는 값이 아니다. 수집기가 성공&amp;middot;실패를 판정해 만드는 값이라, 대상이 죽어도 &lt;code&gt;0&lt;/code&gt;으로 남는다. 그래서 &quot;무엇을 긁고 있고 그중 뭐가 실패하나&quot;를 이 하나로 훑는다.&lt;/p&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;&lt;b&gt;2.2 Alloy는 노드마다 하나씩 있다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Alloy는 DaemonSet&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;clustering&lt;/b&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;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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3 지금 무엇을 긁고 있나&lt;/b&gt;&lt;/h4&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;① Alloy 설정에 주소를 직접 적은 것&lt;/b&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;job&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;kubelet&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;각 노드IP &lt;code&gt;:10250/metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubelet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 (노드마다)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;cAdvisor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;각 노드IP &lt;code&gt;:10250/metrics/cadvisor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cadvisor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;apiserver&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;각 노드IP &lt;code&gt;:6443/metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;k3s-apiserver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MinIO&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minio.observability.svc:9000/minio/v2/metrics/cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&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;노드 주소를 손으로 적지는 않는다. 설정에 적힌 실제 모양이 이렇다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;discovery.kubernetes &quot;nodes&quot; { role = &quot;node&quot; }        // ① 노드 목록을 받아온다

discovery.relabel &quot;apiserver&quot; {                       // ② 그 목록을 스크레이프 대상으로 고쳐 쓴다
  targets = discovery.kubernetes.nodes.targets
  rule {
    source_labels = [&quot;__meta_kubernetes_node_address_InternalIP&quot;]
    replacement   = &quot;$1:6443&quot;
    target_label  = &quot;__address__&quot;                     //    내부 IP 뒤에 :6443을 붙여 주소로
  }
}

prometheus.scrape &quot;apiserver&quot; {                       // ③ 그 주소를 실제로 긁는다
  targets      = discovery.relabel.apiserver.output
  job_name     = &quot;k3s-apiserver&quot;                      //    붙일 job 라벨
  metrics_path = &quot;/metrics&quot;
  scheme       = &quot;https&quot;
  bearer_token_file = &quot;/var/run/secrets/kubernetes.io/serviceaccount/token&quot;
  tls_config { insecure_skip_verify = true }          //    노드 인증서가 자체 서명이라 이름 검증 생략
  forward_to = [prometheus.remote_write.mimir.receiver]   // ④ 받은 값을 Mimir로 보낸다
  clustering { enabled = true }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 단계로 읽으면 &amp;mdash; &lt;b&gt;목록을 받고(①), 주소로 고쳐 쓰고(②), 긁어서 보낸다(③④).&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;b&gt;relabel&lt;/b&gt;이 뒤에서 다시 나온다. 이름 그대로 라벨을 고쳐 쓰는 단계인데, 하는 일이 두 가지다. 하나는 여기처럼 &lt;b&gt;대상 주소를 만드는 것&lt;/b&gt;이고, 다른 하나는 저장소로 보내기 직전에 &lt;b&gt;라벨을 바꾸거나 시리즈를 걸러 내는 것&lt;/b&gt;이다. 수집 단계에서 무언가를 덜어 내려면 손댈 자리가 이 relabel뿐이다.&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;cAdvisor&lt;/b&gt;는 kubelet에 내장된 부분이다. 별도 프로세스가 아니라 kubelet 코드 안에 들어 있고, 컨테이너 런타임이 만든 컨테이너의 실사용을 커널의 cgroup에서 직접 읽어 낸다. 그래서 주소는 kubelet과 같은 &lt;code&gt;:10250&lt;/code&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;파드 stdout &amp;rarr; Loki&lt;/b&gt; &amp;mdash; 컨테이너가 찍는 로그&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿠버네티스 이벤트 &amp;rarr; Loki&lt;/b&gt;(&lt;code&gt;k8s-events&lt;/code&gt;) &amp;mdash; 파드가 왜 죽었는지는 컨테이너 로그가 아니라 이벤트에 남는다. 이벤트는 약 한 시간 뒤 사라지므로 담아 둬야 사후 추적이 된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OTLP 수신 &amp;rarr; Tempo&lt;/b&gt; &amp;mdash; &lt;code&gt;:4317&lt;/code&gt;&amp;middot;&lt;code&gt;:4318&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;&lt;b&gt;② ServiceMonitor&amp;middot;PodMonitor로 자동 발견되는 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쪽은 Alloy 설정에 이름이 없다. 각 컴포넌트가 자기 옆에 ServiceMonitor라는 오브젝트를 두면 Alloy가 그것을 주워 수집 대상으로 바꾼다. 대부분의 차트가 그 오브젝트를 자동으로 같이 배포한다.&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;etcd&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;각 노드IP &lt;code&gt;:2381/metrics&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;node-exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파드(노드마다)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;kube-state-metrics&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파드 1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Redis exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파드 3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Mimir 각 컴포넌트 &amp;middot; Loki &amp;middot; Tempo &amp;middot; Grafana &amp;middot; Alloy 자신&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;각 파드&lt;/td&gt;
&lt;td&gt;각 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;앱 queue &amp;middot; booking&lt;/b&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;&lt;b&gt;etcd만 사정이 다르다.&lt;/b&gt; ServiceMonitor는 Service를 보고 대상을 찾는데, etcd는 파드가 아니라 노드 프로세스라 가리킬 Service가 없다. 그래서 셀렉터 없는 Service를 만들고, 거기 붙는 Endpoints에 노드 IP 셋을 직접 적어 실체 없는 대상을 세웠다.&lt;/p&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;&lt;b&gt;2.4 왜 방법이 갈렸나&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갈림은 대상의 성질 두 가지에서 나온다 &amp;mdash; &lt;b&gt;가리킬 Service가 있는가, 인증이 필요한가.&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;height: 184px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;대상&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;Service&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;인증&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;그래서&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;kubelet &amp;middot; cAdvisor &amp;middot; apiserver&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;없음(노드 프로세스)&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;필요&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;Alloy 설정에 직접&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;etcd&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;없음(노드 프로세스)&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;불필요(&lt;code&gt;:2381&lt;/code&gt; 평문)&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;Service를 손으로 만들어 ServiceMonitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;b&gt;node-exporter &amp;middot; KSM &amp;middot; Redis &amp;middot; LGTM&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;있음(파드)&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;불필요&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;차트가 붙인 ServiceMonitor 그대로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;MinIO&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;있음(파드)&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;불필요&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;차트가 ServiceMonitor 대신 Probe로 내서 직접 지목&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;apiserver가 직접 설정으로 간 이유&lt;/b&gt;를 한 겹 더 본다. &lt;code&gt;:6443/metrics&lt;/code&gt;는 그냥 요청하면 막는다.&lt;b&gt; apiserver는 그 특징으로&lt;/b&gt; &lt;b&gt;요청 헤더에 서비스어카운트 토큰을 실어야 값을 준다.&lt;/b&gt; 파드는 뜰 때 그 토큰을 파일로 받으므로, 수집 설정에 적을 것은 &quot;그 파일을 읽어 헤더에 실어라&quot; 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Alloy 설정에는 그 칸이 있어 한 줄로 끝난다. ServiceMonitor에는 인증을 적는 칸 둘이 다 막혀 있다.&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; 없어지는 방향이라 CRD 버전에 따라 무시되거나 거부된다. ServiceMonitor는 어느 네임스페이스에서나 만들 수 있는 오브젝트라, 거기에 &quot;수집기 파드의 이 경로를 읽어라&quot;를 적게 두면 토큰이 아닌 파일도 읽어 밖으로 내보낼 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Secret을 참조하는 칸&lt;/b&gt; &amp;mdash; 그러려면 Alloy가 그 Secret을 직접 읽어야 하는데, 그 권한은 RBAC에서 빼뒀다. 주면 데이터 계층의 DB 시크릿, 앱 네임스페이스의 앱 시크릿까지 전부 읽힌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Mimir는 어떻게 담나&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 무엇의 개수를 세는가 &amp;mdash; 시리즈와 카디널리티&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mimir가 세고 제한하는 단위가 시리즈다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;node_cpu_seconds_total{node=&quot;k3s-1&quot;, cpu=&quot;0&quot;, mode=&quot;idle&quot;}     &amp;larr; 시리즈 하나
node_cpu_seconds_total{node=&quot;k3s-1&quot;, cpu=&quot;0&quot;, mode=&quot;system&quot;}   &amp;larr; 라벨 하나만 달라도 다른 시리즈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라벨 값의 가짓수가 곱해지며 시리즈가 는다. 노드 3 &amp;times; 코어 4 &amp;times; mode 8이면 이름 하나에 시리즈가 96개다. 이 곱이 부는 정도를 카디널리티라고 부른다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;담는 단위도 하나 짚어 둔다. Mimir는 &lt;b&gt;여러 테넌트를 한 클러스터에 나눠 담도록&lt;/b&gt; 만들어졌다. 모든 요청은 &lt;code&gt;X-Scope-OrgID&lt;/code&gt; 헤더로 &quot;누구 것인가&quot;를 밝히고, 테넌트마다 저장 공간과 상한이 따로 매겨진다. 이 클러스터는 테넌트를 나누지 않아 전부 하나(&lt;code&gt;anonymous&lt;/code&gt;)로 들어간다. 뒤에 나오는 상한이 &quot;테넌트당&quot; 값인 것이 이 때문이고, 테넌트가 하나뿐이라 그 값이 곧 클러스터 전체 값이 된다.&lt;/p&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;&lt;b&gt;3.2 쓰기 경로 &amp;mdash; 들어온 값이 어디에 앉나&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Alloy (노드마다)
   │ remote_write
   ▼
mimir-gateway ──▶ distributor
                      │  시리즈마다 해시를 계산해 해시 링에서 담당 인제스터를 찾고
                      │  RF=3이므로 링에서 이어지는 세 대로 복제 전송
                      ▼
              ingester-0   ingester-1   ingester-2      &amp;larr; 두 대가 받으면 쓰기 성공(quorum)
                      │
                      │  받은 값을 메모리에 들고 있다가 2시간마다 블록으로 만든다
                      ▼
                    MinIO (S3)&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;mimir-gateway&lt;/b&gt; &amp;mdash; 모든 요청이 처음 닿는 입구. 쓰기는 distributor로, 조회는 query-frontend로 넘긴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;distributor&lt;/b&gt; &amp;mdash; 받은 시리즈를 어디로 보낼지 정하는 문지기. 시리즈 이름&amp;middot;라벨로 해시를 내고 &lt;b&gt;해시 링&lt;/b&gt;에서 담당자를 찾는다. 링은 인제스터들이 자기 위치를 등록해 둔 원형 목록이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RF(replication factor) = 3&lt;/b&gt; &amp;mdash; 시리즈 하나를 인제스터 세 대에 복사해 둔다. 쓰기는 그중 &lt;b&gt;두 대가 받으면 성공&lt;/b&gt;으로 친다. 세 대 중 과반이 받았으면 값이 남는다고 보는 것이고, 그래서 한 대가 느리거나 죽어도 쓰기가 멈추지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인제스터&lt;/b&gt; &amp;mdash; 최근 값을 &lt;b&gt;메모리에&lt;/b&gt; 들고 있는 컴포넌트. 2시간마다 그 구간을 블록 파일로 만들어 MinIO에 올린다. 올린 뒤에도 그 블록을 한동안 자기 디스크에 남겨 둔다 &amp;mdash; 조회하는 쪽이 새 블록을 발견하고 색인을 내려받을 시간이 필요해서다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAL&lt;/b&gt; &amp;mdash; 메모리에 있는 동안 프로세스가 죽으면 값이 사라지므로, 들어온 값을 영속 디스크에도 순서대로 적어 둔다. 재시작하면 이 기록을 되읽어 메모리 상태를 복원한다. 그래서 인제스터는 디스크를 붙일 수 있는 StatefulSet으로 뜬다.&lt;/li&gt;
&lt;/ul&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;&lt;b&gt;3.3 읽기 경로 &amp;mdash; 조회가 어디를 보나&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Grafana ──▶ mimir-gateway ──▶ query-frontend ──▶ query-scheduler ──▶ querier
                                                                        │
                                            최근 구간 ────────────────┤──▶ ingester (메모리)
                                            지난 구간 ────────────────┘──▶ store-gateway ──▶ MinIO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회는 한 군데를 보지 않는다. &lt;b&gt;최근 두 시간 남짓은 아직 MinIO에 없고 인제스터 메모리에만 있으므로&lt;/b&gt;, querier가 인제스터와 저장소를 같이 뒤져 합쳐 돌려준다.&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;query-frontend &amp;middot; query-scheduler&lt;/b&gt; &amp;mdash; 들어온 쿼리를 시간 구간으로 쪼개고 줄 세워 querier에게 나눠 준다. 무거운 쿼리 하나가 전체를 막지 않게 하는 층이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;querier&lt;/b&gt; &amp;mdash; 실제로 값을 모아 계산하는 쪽. &lt;b&gt;인제스터와 store-gateway 양쪽에 물어본다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;store-gateway&lt;/b&gt; &amp;mdash; &lt;b&gt;MinIO에 있는 블록들의 색인을 들고 있다&lt;/b&gt;가, 지난 구간을 물으면 어느 블록을 읽어야 하는지 찾아 준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;compactor&lt;/b&gt; &amp;mdash; &lt;b&gt;인제스터 세 대가 2시간마다 각자 올린 블록을 뒤에서 병합&lt;/b&gt;한다. 복제본이 셋이라 같은 구간의 블록도 셋 올라오는데, 그걸 합쳐 조회할 때 뒤질 파일 수를 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기에도 quorum이 걸린다. 인제스터가 재시작하면 멈춰 있던 동안의 값이 그 대에만 비므로, 한 대만 물어보면 구멍 난 답을 받을 수 있다. 그래서 RF가 3이면 읽을 때도 두 대에서 받아 맞춰 본다.&lt;/p&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;&lt;b&gt;3.4 그래서 인제스터만 HA가 필요하다&lt;/b&gt;&lt;/h4&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;어디에 있나&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;MinIO(오브젝트 스토리지)&lt;/td&gt;
&lt;td&gt;저장소가 지킨다. 컴포넌트가 죽어도 데이터는 남는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최근 값&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;인제스터 메모리(+ WAL)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;그 인제스터가 죽으면 그 구간이 빈다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;store-gateway&amp;middot;compactor&amp;middot;querier는 죽어도 MinIO에 있는 것을 다시 읽으면 된다. 상태를 자기가 들고 있지 않다. 반면 &lt;b&gt;인제스터는 아직 아무 데도 안 올라간 최근 구간을 자기 메모리에 들고 있는 유일한 지점&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;WAL이 이 구간을 지키긴 하지만 한계가 분명하다. WAL은 &lt;b&gt;프로세스가 죽었다 살아나는 것&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;그래서 최근 구간의 가용성은 WAL이 아니라 &lt;b&gt;복제&lt;/b&gt;로 확보한다. 이 편에서 계속 나오는 RF=3이 그것이다.&lt;/p&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;&lt;b&gt;3.5 노드마다 인제스터를 하나씩 &amp;mdash; 그리고 왜 monolithic이 아닌가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mimir는 두 가지 배포 모드를 제공한다.&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;monolithic&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모든 컴포넌트를 프로세스 하나로&lt;/td&gt;
&lt;td&gt;시작&amp;middot;개발 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;microservices&lt;/b&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;자원만 보면 dev에는 monolithic이 맞다. 파드 하나면 되고 설정 항목도 적다. 그런데 이 클러스터는 &lt;b&gt;microservices로, 인제스터를 세 대 띄워 노드마다 하나씩&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;monolithic으로는 최근 구간을 복제할 수 없다.&lt;/b&gt; 프로세스 하나에 전부 들어 있으면 그 프로세스가 죽을 때 인제스터도 같이 죽고, 메모리에만 있던 최근 구간이 통째로 빈다. 복제를 하려면 인제스터가 독립된 복제본 여럿으로 존재해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dev를 stg&amp;middot;prd와 같은 구조로 두는 것이 이 홈랩의 목적이다.&lt;/b&gt; 규모만 낮추고 구조는 같게 가져가야, 여기서 잰 값이 위 환경의 스펙을 정하는 근거가 된다. 인제스터를 노드마다 두는 것은 Kafka 브로커나 Redis 복제본을 노드에 흩어 두는 것과 같은 결이다 &amp;mdash; 한 노드를 잃어도 나머지로 버티게 만드는 배치.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대가도 분명하다. 파드 수가 늘고(Mimir만 11개), 그만큼 자원을 더 쓴다. 이 규모에 과한 구성이라는 점은 인정하고 가는 선택이다.&lt;/p&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;&lt;b&gt;3.6 그 구조가 만드는 제약 &amp;mdash; 시리즈 상한&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인제스터가 최근 값을 메모리에 든다는 것은, &lt;b&gt;메모리가 유한 자원이라는 뜻&lt;/b&gt;이다. 카디널리티가 폭주하면 메모리를 끝없이 먹다가 커널에 종료된다(OOMKilled).&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;b&gt;새 시리즈만 거절&lt;/b&gt;하고 기존 시리즈는 계속 받는다. 크래시로 통째로 잃는 대신 새로 들어오려는 것만 막는다. 노드 메모리가 마를 때 커널 OOM 전에 kubelet이 파드를 내보내는 것과 같은 결의 가드레일이다.&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;code&gt;max_global_series_per_user&lt;/code&gt;&lt;/b&gt;이고, 차트에 따로 지정하지 않아 &lt;b&gt;Mimir 기본값 150,000&lt;/b&gt;이 그대로 걸려 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값이 세는 대상은 &quot;복제 전, 테넌트 전체의 고유 시리즈&quot; 수다. 인제스터 한 대가 지는 몫은 이렇게 나온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;인제스터당 = 전역 상한 &amp;times; RF &amp;divide; 인제스터 개수
           = 150,000 &amp;times; 3 &amp;divide; 3
           = 150,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RF가 3인데 인제스터도 정확히 3대라 &lt;b&gt;모든 시리즈가 세 대 전부에 복제된다.&lt;/b&gt; 그래서 전역 상한과 인제스터당 보유량이 같은 값이 된다. 세 대가 15만씩 나눠 갖는 게 아니라, &lt;b&gt;같은 15만을 세 벌 복사해 각자 다 들고 있다.&lt;/b&gt;&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;4. 그런데 절반만 흐른다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱은 하나도 안 떠 있다. 지금 들어오는 것은 인프라 자신이 내는 신호뿐인데, 그것만으로 &lt;b&gt;Mimir의 시리즈 상한을 넘어선다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 증상 &amp;mdash; 도착한 것과 안 온 것이 갈린다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grafana에서 무엇이 들어와 있는지 훑으면 목록이 고르지 않다.&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;kube-state-metrics&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;통째로 없음 (&lt;code&gt;kube_pod_info&lt;/code&gt; 0건)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;node-exporter&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;세 노드 중 한 대분만&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;kubelet &amp;middot; cAdvisor &amp;middot; apiserver&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;각각 한 노드분만&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MinIO&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;원천이 250줄인데 55개만&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;etcd &amp;middot; Redis&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;세 노드분 온전&lt;/b&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;Running&lt;/code&gt;이고 재시작도 없다. 배선이 끊긴 모양은 아니다. 조회를 맡는 &lt;code&gt;mimir-querier&lt;/code&gt;만 93분 사이 25번 재시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 상태에서 &lt;b&gt;화면에 에러가 안 뜬다.&lt;/b&gt; 쿼리는 정상으로 응답하고 값만 없다. 무언가 버려지고 있다는 신호는 수집기 로그에만 남는다.&lt;/p&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;&lt;b&gt;4.2 저장소가 상한에 붙어 있다&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;cortex_ingester_memory_series&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인제스터가 나란히 &lt;b&gt;150000&lt;/b&gt;이다. 이 값은 &quot;최근에 샘플이 있던 시리즈&quot;가 아니라 &lt;b&gt;인제스터가 지금 메모리에 들고 있는 시리즈&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;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;... err-mimir-max-series-per-user (per-user series limit of 150000 exceeded)&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;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;pre class=&quot;lsl&quot;&gt;&lt;code&gt;                     재기동 전      재기동 후
k3s-apiserver        약 5만         약 7만 5천
kubelet              약 7만 7천     약 3만
kube-state-metrics       0          약 6천 5백    &amp;larr; 없다가 전부 들어옴
node-exporter        1노드분        3노드분
────────────────────────────────────────────
인제스터가 든 총량    150,000        150,000      &amp;larr; 총량은 늘 같다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-state-metrics가 고장이었다면 재기동으로 저절로 고쳐질 리 없다. &lt;b&gt;등록 순서가 다시 섞였을 뿐&lt;/b&gt;이다. 차오르는 속도도 그와 맞는다 &amp;mdash; 재기동 뒤 &lt;b&gt;30초 만에&lt;/b&gt; 150,000에 닿고 그 뒤로 계속 붙어 있다.&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; 겉보기가 비슷한 OOMKilled과 갈라 둔다. 처방이 반대라서다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;상한 포화(지금)&lt;/th&gt;
&lt;th&gt;OOMKilled&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;파드 상태&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;1/1 Running&lt;/code&gt;, 재시작 거의 없음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RESTARTS&lt;/code&gt; 상승 &amp;middot; &lt;code&gt;CrashLoopBackOff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;수집기가 받는 응답&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;살아 있는 쪽이 400으로 거절&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;연결 거부 &amp;middot; 타임아웃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;memory_series&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;상한 값에 붙어 있음&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;재시작마다 0에서 다시 오름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOMKilled이면 컨테이너 메모리 limit을 올려 푼다. 지금은 설정 상한에 걸린 거절이라 limit으로 될 일이 아니다.&lt;/p&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.3 그 15만의 절반이 사본이다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;15만을 job별로 쪼갠다 &amp;mdash; 아래 숫자는 재기동 전 스냅샷이다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;topk(20, count by (job) ({__name__=~&quot;.+&quot;, job=~&quot;.+&quot;}))&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubelet          약 7만 7천
k3s-apiserver    약 5만          두 job 합이 15만의 약 85%
cadvisor         약 5천 8백
k3s-etcd         약 5천 3백
나머지 전부       약 1만 2천&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;kubelet과 k3s-apiserver가 시리즈 상한 15만의 대부분을 차지한다. 그런데 &lt;code&gt;job=&quot;kubelet&quot;&lt;/code&gt; 안을 열면 kubelet 지표가 아니다. 두 job을 각각 열어 나란히 놓는다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;topk(12, count by (__name__) ({job=&quot;kubelet&quot;}))
topk(12, count by (__name__) ({job=&quot;k3s-apiserver&quot;}))&lt;/code&gt;&lt;/pre&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;code&gt;job=&quot;k3s-apiserver&quot;&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;job=&quot;kubelet&quot;&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;etcd_request_duration_seconds_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;14,400&lt;/td&gt;
&lt;td&gt;5,760&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_request_duration_seconds_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;11,640&lt;/td&gt;
&lt;td&gt;3,672&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_request_sli_duration_seconds_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8,558&lt;/td&gt;
&lt;td&gt;2,904&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_watch_list_duration_seconds_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4,626&lt;/td&gt;
&lt;td&gt;1,512&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_watch_cache_read_wait_seconds_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3,682&lt;/td&gt;
&lt;td&gt;1,428&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_request_body_size_bytes_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3,072&lt;/td&gt;
&lt;td&gt;1,760&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_response_sizes_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2,336&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_watch_events_sizes_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2,241&lt;/td&gt;
&lt;td&gt;783&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apiserver_admission_controller_admission_duration_seconds_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;770&lt;/td&gt;
&lt;td&gt;&lt;b&gt;770&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;workqueue_queue_duration_seconds_bucket&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;964&lt;/td&gt;
&lt;td&gt;407&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kubelet 주소를 긁었는데 apiserver&amp;middot;etcd&amp;middot;workqueue 지표가 나온다. k3s-apiserver 주소도 마찬가지다&lt;/b&gt; &amp;mdash; 두 목록이 같은 이름을 같은 순서로 늘어놓는다. 개수가 다른 것은 두 job에 착지한 노드분이 달라서고, 같은 노드분끼리 만난 항목은 값까지 똑같다(표에서 &lt;code&gt;admission_controller&lt;/code&gt; 770, 이 둘만 그렇다).&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;두 주소가 같은 것을 내고 있다. &quot;메트릭은 포트에 붙어 있는 게 아니라 프로세스에 붙어 있어&quot;서다.&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;프로그램이 메트릭을 하나 만들면 프로세스 안의 목록에 등록한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이 목록을 &lt;b&gt;레지스트리&lt;/b&gt;라고 부른다.&lt;b&gt; /metrics 요청이 하는 일은 하나다 &amp;mdash; 그 레지스트리를 통째로 텍스트로 찍어 보낸다&lt;/b&gt;. 그러니 포트가 달라도 읽는 레지스트리가 같으면 응답이 같다.&lt;/li&gt;
&lt;li&gt;컨트롤플레인 넷이 한 프로세스(k3s)라 그 레지스트리가 하나다. apiserver&amp;middot;kubelet&amp;middot;scheduler&amp;middot;controller-manager가 전부 그 하나에 등록하고, :6443과 :10250이 각자 같은 그것을 찍어 보낸다.&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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;k3s-1  :10250/metrics    72,093 줄
k3s-1  :6443/metrics     72,093 줄

메트릭 이름 종류      536 / 536
한쪽에만 있는 이름      0            &amp;larr; 이름 집합이 완전히 같다&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;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;process_resident_memory_bytes{job=~&quot;kubelet|k3s-apiserver&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;k3s-3  job=&quot;k3s-apiserver&quot;    1,125 MB
k3s-3  job=&quot;kubelet&quot;          1,130 MB     &amp;larr; 같은 프로세스, 스크레이프 시점만 다르다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 1.1GB는 k3s server 프로세스 하나의 값이다. 노드 예약(2Gi)의 근거로 쓴 1.5&amp;ndash;1.8GiB는 컨테이너 런타임까지 포함한 서비스 단위 실측이라 더 크다.&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;code&gt;job&lt;/code&gt; 라벨이 다르다(내가 alloy에서 다르게 설정했기에).&lt;/b&gt;&amp;nbsp;시리즈는 라벨이 하나라도 다르면 다른 시리즈다. Mimir는 이 둘을 사본으로 알아보지 못하고 &lt;b&gt;각각 세고 각각 저장한다.&lt;/b&gt; 15만의 85%를 차지한 두 job이 실은 한 벌의 데이터다.&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;kubeadm으로 세운 클러스터에서는 이게 정상이고 필요한 설정이였을 것이다.&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;b&gt;한 프로세스에 여러 메트릭 &lt;/b&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;&lt;code&gt;:6443/metrics&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;공용 &amp;mdash; apiserver&amp;middot;kubelet&amp;middot;scheduler&amp;middot;controller-manager&lt;/td&gt;
&lt;td&gt;약 3만&amp;ndash;7만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;:10250/metrics&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;같은 공용 레지스트리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;위와 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;:10250/metrics/cadvisor&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;cAdvisor 전용&lt;/td&gt;
&lt;td&gt;약 5,300&amp;ndash;5,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;:2381/metrics&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;etcd 전용&lt;/td&gt;
&lt;td&gt;약 1,800&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;cAdvisor&lt;/b&gt; &amp;mdash; kubelet 안에 있지만 컨테이너 cgroup을 읽는 별개 수집기라 경로도 레지스트리도 따로다. &lt;code&gt;/metrics&lt;/code&gt;만 긁으면 컨테이너 지표가 아예 안 온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;etcd&lt;/b&gt; &amp;mdash; 쿠버네티스 컴포넌트가 아니라 별개 오픈소스를 품어 돌린 것이라 자기 레지스트리를 쓴다.&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;etcd_request_duration_seconds&lt;/code&gt;는 etcd 레지스트리에 없다. &quot;apiserver가 etcd에 다녀온 시간&quot;이라 &lt;b&gt;apiserver가 재는 값이고 공용 레지스트리에 있다.&lt;/b&gt; &lt;br /&gt;디스크 압력을 보는 쪽은 &lt;code&gt;:2381&lt;/code&gt;의 etcd 전용 레지스트리 매트릭인&amp;nbsp; &lt;code&gt;etcd_disk_wal_fsync_duration_seconds&lt;/code&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;그리고 scheduler&amp;middot;controller-manager는 자기 메트릭 포트(&lt;code&gt;:10259&lt;/code&gt;&amp;middot;&lt;code&gt;:10257&lt;/code&gt;)가 노드 밖으로 안 열려 있는데도 &lt;b&gt;지표가 이미 들어와 있다.&lt;/b&gt; 공용 레지스트리에 함께 등록돼 있어서다. 표준 문서를 따라 그 포트를 여는 인자를 넣으면 같은 레지스트리를 세 번째&amp;middot;네 번째로 긁게 된다.&lt;/p&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.4 원천은 얼마를 내나&lt;/b&gt;&lt;/h4&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;합&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;kubelet &lt;code&gt;/metrics&lt;/code&gt; 3노드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;179,225&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;apiserver &lt;code&gt;:6443&lt;/code&gt; 3노드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;179,225&lt;/b&gt; (같은 레지스트리 사본)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cAdvisor 3노드&lt;/td&gt;
&lt;td&gt;16,937&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;etcd &amp;middot; KSM &amp;middot; node-exporter &amp;middot; MinIO &amp;middot; Redis &amp;middot; Alloy &amp;middot; LGTM 자기 지표&lt;/td&gt;
&lt;td&gt;약 32,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;합계&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;약 407,000&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상한 15만에 원천이 약 40만이다.&lt;/b&gt; 2.7배고, 그중 &lt;b&gt;약 17.9만이 사본&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 고정된 수가 아니다. &lt;code&gt;verb &amp;times; resource &amp;times; scope&lt;/code&gt; 조합은 apiserver가 겪은 요청 종류만큼 생기므로 재기동 직후에는 작고 클러스터가 돌수록 는다. 재기동 40분 뒤에 다시 세니 노드 세 대 합이 118,642였다. &lt;b&gt;원천은 자라는데 상한은 고정&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;b&gt;막힌 것은 Mimir 하나다.&lt;/b&gt; Loki는 인메모리 스트림이 73개로 기본 상한 5,000의 1.5%고 거절이 0이다. 세는 단위가 달라서다 &amp;mdash; Loki가 세는 스트림은 라벨 조합이고 로그 본문은 압축된 청크에 들어가므로, &lt;b&gt;로그는 양이 늘어도 스트림이 안 는다.&lt;/b&gt; Tempo는 보낼 앱이 없어 유입이 0이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;k3s가 컨트롤플레인 넷을 한 프로세스로 합침   &amp;rarr;  메트릭 레지스트리도 하나
Alloy가 :6443 과 :10250 을 각각 긁음        &amp;rarr;  같은 레지스트리를 두 번
Mimir는 라벨이 다르면 다른 시리즈로 저장      &amp;rarr;  사본이 상한의 절반을 차지
──────────────────────────────────────────────────────────
원천 약 40만  vs  상한 15만
   사본을 걷어도 약 22만  &amp;mdash;  그래도 15만보다 크다&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;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;h2 data-ke-size=&quot;size26&quot;&gt;5. 뚫는다&lt;/h2&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; 같은 레지스트리를 두 번 긁는 것을 한 번으로. 약 17.9만이 줄어 원천이 약 22만이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상한을 올린다&lt;/b&gt; &amp;mdash; 그래도 22만이 15만보다 크다. 천장을 올려야 나머지가 들어온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한쪽만으로는 안 된다. 사본만 걷으면 22만이 15만에 여전히 막히고, 상한만 올리면 아무도 안 읽는 사본에 메모리를 지불한다.&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;b&gt;5.1 사본을 어떻게 걷을까&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;:6443/metrics&lt;/code&gt;와 &lt;code&gt;:10250/metrics&lt;/code&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;한쪽 scrape를 통째로 지운다&lt;/b&gt; &amp;mdash; 설정에서 블록 하나를 뺀다. 대신 그 job의 &lt;code&gt;up&lt;/code&gt;도 같이 사라진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;둘 다 두되 한쪽은 &lt;code&gt;up&lt;/code&gt;만 남긴다&lt;/b&gt; &amp;mdash; 보내기 직전 relabel에서 그 job의 메트릭을 걸러 낸다. &lt;code&gt;up&lt;/code&gt;은 수집기가 스스로 만드는 값이라 본문을 다 버려도 남는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;up&lt;/code&gt; 둘이 갈리는 경우가 있다. 같은 프로세스지만 통과하는 경로가 다르다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;up{job=&quot;k3s-apiserver&quot;}   :6443  &amp;rarr; apiserver 인증&amp;middot;flowcontrol을 지난다
up{job=&quot;kubelet&quot;}         :10250 &amp;rarr; kubelet의 단순한 경로를 지난다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apiserver 처리 경로가 막히면 앞엣것만 0이 되고 뒤엣것은 1로 남는다. 다만 그 지경이면 &lt;code&gt;kubectl&lt;/code&gt;도 안 되는 상태라 다른 데서 먼저 드러난다. 그리고 노드 생존은 이미 다른 신호가 본다 &amp;mdash; node-exporter의 &lt;code&gt;up&lt;/code&gt;, KSM이 내는 노드 상태, etcd 리더 유무.&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;결정 &amp;mdash; &lt;code&gt;:6443&lt;/code&gt; scrape를 지우고 &lt;code&gt;:10250&lt;/code&gt;만 남긴다. 그리고 job 이름을 &lt;code&gt;k3s-server&lt;/code&gt;로 바꾼다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;:10250&lt;/code&gt;을 고른 이유는 둘이다. apiserver의 인증&amp;middot;flowcontrol 경로를 지나지 않아 apiserver가 밀릴 때도 응답하고, cAdvisor가 같은 포트의 다른 경로라 설정이 나란히 붙는다.&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;이다. 이 레지스트리에는 apiserver&amp;middot;kubelet&amp;middot;scheduler&amp;middot;controller-manager 지표가 전부 들어 있다. &lt;code&gt;kubelet&lt;/code&gt;이라 부르면 안의 대부분인 apiserver 지표를 가리키지 못하고, &lt;code&gt;k3s-apiserver&lt;/code&gt;라 불러도 반만 맞다. &lt;code&gt;k3s-server&lt;/code&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;컴포넌트 구분은 &lt;code&gt;job&lt;/code&gt;이 아니라 메트릭 이름이 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;apiserver_request_*        apiserver
scheduler_*                스케줄러
workqueue_*                컨트롤러 매니저
kubelet_*                  kubelet
etcd_server_* &amp;middot; etcd_disk_* etcd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;job&lt;/code&gt;은 &quot;어디서 긁었나&quot;를 적는 라벨이고 &quot;무슨 컴포넌트냐&quot;는 이름이 말한다. 그래서 job을 하나로 두어도 대시보드에서 컴포넌트를 나눠 보는 데 지장이 없다.&lt;/p&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;&lt;b&gt;5.2 상한을 얼마로 둘까 &amp;mdash; 실측으로 계산한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사본을 걷으면 원천이 약 22만이 된다. 그래도 15만보다 크니 천장을 올려야 하고, 얼마로 올릴지는 인제스터 메모리가 정한다. 얼마나 더 쓰는지는 추정할 필요가 없다 &amp;mdash; 인제스터가 자기 값을 낸다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;cortex_ingester_memory_series
process_resident_memory_bytes{job=&quot;observability/ingester&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;시리즈 150,000  &amp;harr;  RSS 506&amp;ndash;618 MiB (세 대, 재는 시점에 따라)
                &amp;rarr;  시리즈 하나당 3.5&amp;ndash;4.2 KiB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;값이 한 자리에 머물지 않는다.&lt;/b&gt; 시리즈 수는 15만으로 고정인데 RSS는 계속 오르내린다. Go가 회수한 메모리를 운영체제에 바로 돌려주지 않고, 2시간 블록 주기 안에서 메모리에 든 구간이 자랐다 줄고, 조회가 오면 그만큼 더 쓰기 때문이다. 재기동 직후가 가장 낮고(3.5 KiB) 안정되면 4.2 KiB다. &lt;b&gt;상한을 정할 때는 저점이 아니라 안정 상태 값을 쓴다.&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;Mimir 공식 용량 계획은 &lt;b&gt;시리즈당 약 8.3KB를 잡는다(인메모리 30만당 2.5GB).&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;RF가 3인데 인제스터도 3대라 전역 상한이 곧 인제스터 한 대가 지는 양이다. 그래서 상한이 그대로 노드당 메모리가 된다.&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;실측 4.2KiB 기준&lt;/th&gt;
&lt;th&gt;공식 8.3KB 기준&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;15만 (지금)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;약 0.6 GiB&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;약 1.2 GiB&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25만&lt;/td&gt;
&lt;td&gt;약 1.0 GiB&lt;/td&gt;
&lt;td&gt;약 1.9 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30만&lt;/td&gt;
&lt;td&gt;약 1.2 GiB&lt;/td&gt;
&lt;td&gt;약 2.3 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50만&lt;/td&gt;
&lt;td&gt;약 2.0 GiB&lt;/td&gt;
&lt;td&gt;약 3.9 GiB&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;여기에 걸리는 제약이 둘이다.&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;인제스터 컨테이너 limit&lt;/b&gt; &amp;mdash; 지금 1Gi로 걸어 뒀다. 이 값을 넘기면 컨테이너가 OOMKilled 된다. 25만에서 이미 닿는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노드 여유&lt;/b&gt; &amp;mdash; 노드 8GB에서 자원 예약을 뺀 몫 안에 인제스터 말고도 다 들어가야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &quot;노드 여유&quot;에 값이 두 개 있다. &lt;b&gt;스케줄러가 세는 여유와 실제로 남은 메모리가 서로 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;스케줄러가 세는 여유 = 할당 가능량 &amp;minus; 그 노드 파드들의 request 합
실제 남은 메모리     = 커널이 보는 값 (free)&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;선언한 request만 더한다.&lt;/b&gt; dev라 대부분의 파드가 자기 request보다 적게 쓰므로 두 값이 벌어진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;             스케줄러가 세는 여유    실제 남은 메모리
k3s-1            1,969 MiB          3,532 MiB
k3s-2            1,671 MiB          3,030 MiB
k3s-3            2,427 MiB          4,268 MiB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘이 막는 것이 다르다.&lt;/b&gt; 스케줄러가 세는 여유가 모자라면 파드가 그 노드에 뜨지 못하고(&lt;code&gt;Pending&lt;/code&gt;), 실제 메모리가 모자라면 뜬 파드가 죽는다(OOMKill&amp;middot;eviction). 그래서 request를 올릴 때는 왼쪽으로, limit을 올릴 때는 오른쪽으로 검산한다.&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;결정 &amp;mdash; 상한 30만, 인제스터 limit 2Gi, request 1Gi.&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;상한 30만&lt;/b&gt; &amp;mdash; 사본을 걷은 뒤 실수요는 가동 시간에 따라 약 17만&amp;ndash;22만 사이를 오르내리고(재기동 직후 낮고 돌수록 는다), 앱이 붙으면 더 는다. 25만은 여유가 13%뿐이라 앱이 붙으면 곧 다시 찬다. 30만이면 36% 여유고, 실측 기준 인제스터당 1.2 GiB다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;limit 2Gi&lt;/b&gt; &amp;mdash; 30만이 실측 4.2KiB로 약 1.2 GiB라 1Gi로는 컨테이너가 OOMKilled 된다. limit은 스케줄링에 쓰이지 않으므로 올려도 다른 파드를 밀어내지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;request 1Gi&lt;/b&gt; &amp;mdash; 지금 512Mi인데 실사용이 571&amp;ndash;581 MiB이고 30만이면 1.2 GiB가 된다. request가 실사용보다 작으면 그 초과분 때문에 인제스터가 eviction 우선순위 앞쪽에 놓인다. 상태를 들고 있는 컴포넌트라 그건 피한다. 512Mi를 더 떼도 스케줄러가 세는 여유가 가장 빠듯한 k3s-2에서 1,159 MiB 남고, 실제 메모리는 30만을 다 채워도 2,390 MiB 남는다.&lt;/li&gt;
&lt;/ul&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;&lt;b&gt;5.3 무엇을 고치나&lt;/b&gt;&lt;/h4&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;수집 &amp;mdash; Alloy 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;삭제   prometheus.scrape &quot;apiserver&quot;     :6443 &amp;mdash; 같은 레지스트리의 사본
삭제   discovery.relabel &quot;apiserver&quot;     그 블록만 쓰던 주소 변환
변경   prometheus.scrape &quot;kubelet&quot;       job_name 을 &quot;k3s-server&quot; 로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;discovery.kubernetes &quot;nodes&quot;&lt;/code&gt;는 남긴다 &amp;mdash; 남은 kubelet&amp;middot;cAdvisor scrape가 그 노드 목록을 쓴다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저장 &amp;mdash; Mimir 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;추가   limits.max_global_series_per_user: 300000     기본값 150000 을 덮어씀
변경   ingester.resources  requests.memory  512Mi &amp;rarr; 1Gi
변경   ingester.resources  limits.memory    1Gi   &amp;rarr; 2Gi&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5&gt;&lt;b&gt;적용 &amp;mdash; 롤링 교체와 WAL 재생&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 반영되면 인제스터가 한 대씩 새 파드로 교체된다. StatefulSet의 교체 순서는 번호가 큰 쪽부터다. 30초 간격 기록.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;17:44:00   2번 옛 파드 마지막 값     150,000
17:44:30   2번 새 파드 첫 값        196,456
17:45:30   2번                     297,078
17:47:30   세 대 모두               300,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 파드가 0이 아닌 값에서 시작하는 것은 디스크에 적어 둔 WAL을 되읽어 메모리 상태를 복원하기 때문이다. 196,456은 재생이 끝나기 전에 긁힌 값이고, 1분 뒤 297,078로 올라간다. &lt;b&gt;상태를 들고 있는 컴포넌트에 디스크를 붙이면 교체가 데이터 손실 없이 끝난다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교체 후 인제스터가 든 것은 30만이다. 사본을 걷은 뒤 들어오는 양은 약 18만이므로 12만이 더 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;잔존 &amp;mdash; 라벨을 바꾸면 옛 시리즈가 옆에 남는다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;job 이름은 라벨이고, &lt;b&gt;라벨 조합이 다르면 다른 시리즈&lt;/b&gt;다. 새 이름의 시리즈는 옛 이름의 자리를 대체하지 않고 별도로 생성된다. 옛 시리즈는 샘플이 끊겨도 즉시 사라지지 않는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;새로 들어오는 것    k3s-server 계열               약 18만
아직 안 빠진 것     kubelet &amp;middot; k3s-apiserver 계열   약 12만
──────────────────────────────────────────────
인제스터가 든 것                                 30만&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 적용 직후에는 사용률이 다시 100%가 되고 거절도 이어진다. &lt;b&gt;이름을 바꾸는 변경에는 옛것과 새것을 동시에 들고 있는 구간이 따라온다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;제거 &amp;mdash; head 절단이 시점을 정한다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인제스터가 메모리에 들고 있는 최근 구간을 head라 부른다. 샘플이 끊긴 시리즈를 head에서 덜어내는 동작이 &lt;b&gt;head 절단(truncation)&lt;/b&gt; 이고, 블록 생성과는 다른 동작이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&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;블록 생성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;head의 데이터를 파일로 떠서 오브젝트 저장소로 보낸다&lt;/td&gt;
&lt;td&gt;아니오&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;head 절단&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;head가 담는 시간 구간의 시작점을 앞으로 민다&lt;/td&gt;
&lt;td&gt;&lt;b&gt;마지막 샘플이 그 시작점보다 앞서면 빠진다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 시간 간격으로 도는 절단을 5분 간격으로 기록했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;시각     보유 시리즈   제거 누적   head 절단   거절/초
17:47    300,000          0          0       0.0
18:02    300,000          0          1      26.9   1회차 &amp;mdash; 제거 0
19:58    300,000          0          1      36.7   115분째
20:03    300,000    120,973          2      28.3   2회차 &amp;mdash; 120,973 제거
20:08    181,128    120,973          2       1.4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1회차에 제거가 0인 이유는 절단의 정의에서 나온다. 옛 라벨의 샘플이 끊긴 시각은 17:4x이고, 18:02의 절단이 민 시작점은 아직 그보다 앞이라 옛 시리즈의 마지막 샘플이 head 구간 안에 남아 있었다. 20:03의 절단에서 시작점이 17:4x를 넘어가며 120,973이 한 번에 빠졌다.&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;대기 시간 = 샘플이 끊긴 시각부터 최대 두 주기(약 4시간).&lt;/b&gt; 이번은 17:47에서 20:03까지 2시간 16분이었다. 주기가 두 시간이므로 &lt;b&gt;한 주기 미만의 관찰로는 &quot;안 도는 것&quot;과 &quot;아직 안 온 것&quot;이 갈리지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;확인 &amp;mdash; &lt;code&gt;count&lt;/code&gt;로는 이 12만이 안 보인다&lt;/b&gt;&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛 시리즈가 남아 있는지를 &lt;code&gt;count&lt;/code&gt;로 확인하면 0이 나온다. &lt;code&gt;count&lt;/code&gt;는 최근 몇 분 안에 샘플이 있는 시리즈만 세기 때문이다. 옛 라벨은 샘플이 끊겼으므로 &lt;code&gt;count&lt;/code&gt;에는 잡히지 않고, 인제스터 메모리에는 남아 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;sum(scrape_samples_scraped)          타깃이 지금 생산하는 양   약 18만
max(cortex_ingester_memory_series)   인제스터가 든 양         30만
max(cortex_ingester_active_series)   최근 샘플이 온 시리즈     약 18만&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memory_series&lt;/code&gt;는 &quot;지금 메모리에 들고 있는 시리즈&quot;, &lt;code&gt;active_series&lt;/code&gt;는 &quot;최근 샘플이 온 시리즈&quot;다. &lt;b&gt;자리를 차지하는 것은 &lt;code&gt;memory_series&lt;/code&gt; 쪽이다.&lt;/b&gt; 두 값의 간격이 잔존량이고, 12만 벌어져 있다가 20:08에 붙었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;인제스터 보유    182,397   상한 300,000 의 61%
활성 시리즈      182,397   보유와 같다 = 잔존 0
거절            0/s
실제 메모리      469 &amp;middot; 486 &amp;middot; 479 MiB   limit 2Gi 의 약 24%
파드 재시작      0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사본을 걷어 이 시점 생산량이 약 18만이 됐다 &amp;mdash; 걷기 직전에는 두 벌을 합쳐 약 25만이었다. 진단 때 계산한 &quot;걷은 뒤 약 22만&quot;보다 작은 것은, 재기동 뒤 apiserver 계열 시리즈가 아직 다 자라지 않은 시점이라서다. 상한을 30만으로 올려 12만의 여유를 뒀고, 시리즈당 실측 4.2 KiB로 30만을 다 채워도 1.2 GiB라 limit 2Gi 안에 든다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;6. 계기판 &amp;mdash; Mimir가 정의하는 세 축으로 짓는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 대시보드의 질문은 하나다 &amp;mdash; &lt;b&gt;관측이 도는가.&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;행 구조는 Mimir 공식(mimir-mixin)이 자기 관측을 나누는 세 축을 그대로 쓴다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;쓰기        받아지는가     Alloy &amp;rarr; distributor &amp;rarr; ingester
읽기        답하는가       query-frontend &amp;rarr; querier
저장 여정    내려가는가     ingester &amp;rarr; MinIO / ring &amp;middot; 메모리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 상한 사건에서 나온 신호들(수집 생존&amp;middot;전송 실패&amp;middot;거절&amp;middot;보유 vs 상한)은 전부 쓰기 축 소속이라 첫 행에 모인다. 패널은 12개로 잡는다 &amp;mdash; 빠진 신호가 드러나면 그날 추가한다. 패널 제목은 &quot;컴포넌트 &amp;mdash; 신호&quot; 형식이고, 설명(ⓘ)은 역할 &amp;middot; 패널 &amp;middot; 읽는 법 세 줄이다. 화면만 봐도 어느 컴포넌트의 어느 단계인지 읽히게.&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;대시보드는 git에 푸시하면 Grafana까지 자동으로 배달된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;① git push        대시보드 JSON을 담은 ConfigMap (라벨 grafana_dashboard: &quot;1&quot;)
② ArgoCD          3분 주기 폴링으로 새 커밋 발견 &amp;rarr; sync &amp;rarr; ConfigMap 생성&amp;middot;갱신
③ 사이드카         Grafana 파드 안의 보조 컨테이너가 쿠버네티스 API를 watch &amp;rarr; 라벨 감지
④ 파일 반영        JSON을 공유 볼륨에 파일로 (ConfigMap이 지워지면 파일도 제거)
⑤ 리로드           Grafana 리로드 API 호출 &amp;rarr; 재시작 없이 반영&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;push부터 화면까지는 ②의 폴링 주기가 지배한다 &amp;mdash; 최대 약 3분. Grafana에는 영속 저장소가 없어 UI 수정은 재시작에 사라진다 &amp;mdash; 수정이 남는 경로는 git push 하나다.&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;b&gt;6.1 쓰기 행 &amp;mdash; 받아지는가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 저장소에 앉기까지: Alloy가 긁고, 자기 WAL(전송 전 값을 로컬 디스크에 적어 두는 버퍼)에 적어 배치로 밀고(remote_write), distributor가 해시 링에서 담당 인제스터를 찾아 RF3로 복제하고, ingester가 메모리에 든다. 이 여정의 어디가 끊겨도 결과는 같다 &amp;mdash; 값이 조용히 사라진다. 여섯 패널이 그 여정을 단계 순서로 지킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nhrgc/dJMb991EvNM/FmPeGe1PbrIcmSmN5Lgk81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nhrgc/dJMb991EvNM/FmPeGe1PbrIcmSmN5Lgk81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nhrgc/dJMb991EvNM/FmPeGe1PbrIcmSmN5Lgk81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnhrgc%2FdJMb991EvNM%2FFmPeGe1PbrIcmSmN5Lgk81%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;1033&quot; height=&quot;245&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;Alloy 수집 &amp;mdash; job별 생존 이력&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;min by (job) (up)&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;up&lt;/code&gt;: 수집기가 스크레이프 성공(1)&amp;middot;실패(0)를 판정해 직접 만드는 값 &amp;mdash; 대상이 죽어도 남는 유일한 신호&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min by (job)&lt;/code&gt;: 타깃을 job으로 접기 &amp;mdash; 하나만 죽어도 그 줄이 빨강. 이산 상태라 색 블록(state timeline)&lt;/li&gt;
&lt;li&gt;빨간 두 줄(booking&amp;middot;app/queue) = 이미지 반입 전 앱. 필터로 가리지 않는다 &amp;mdash; 쿼리는 사실만, 맥락은 운영자 몫&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;distributor&amp;middot;ingester &amp;mdash; 거절 (사유별)&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;sum by (reason) (rate(cortex_discarded_samples_total[5m]))&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;두 컴포넌트가 한도&amp;middot;유효성 검사에서 거부한 샘플 &amp;mdash; reason 라벨에 사유가 문자열로 붙는다&lt;/li&gt;
&lt;li&gt;0이 정상. &lt;code&gt;per_user_series_limit&lt;/code&gt; = 시리즈 상한 도달 &amp;mdash; 늦게 온 시리즈부터 선착순으로 밀린다&lt;/li&gt;
&lt;li&gt;상한 사건 때 로그를 뒤져서야 봤던 그 신호의 상시화&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;Alloy remote_write &amp;mdash; 전송 실패&amp;middot;재시도&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;sum by (pod) (rate(prometheus_remote_storage_samples_failed_total[5m]))
sum by (pod) (rate(prometheus_remote_storage_samples_retried_total[5m]))&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;실패 = 재시도 없이 버려진 샘플(4xx) = 곧 유실. 옆 거절 패널과 같은 사건의 양면이다&lt;/li&gt;
&lt;li&gt;재시도 = 일시 오류(5xx&amp;middot;네트워크)로 재전송 중 &amp;mdash; 노드 재시작 때 튀었다가 유실 0으로 회복해야 정상&lt;/li&gt;
&lt;li&gt;Mimir가 통째로 죽으면 거절 카운터는 침묵하지만 여기는 뜬다 &amp;mdash; 백업 신호&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7c8sV/dJMcajb01eh/ArqkpJM2iYcUzvieyQqPek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7c8sV/dJMcajb01eh/ArqkpJM2iYcUzvieyQqPek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7c8sV/dJMcajb01eh/ArqkpJM2iYcUzvieyQqPek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7c8sV%2FdJMcajb01eh%2FArqkpJM2iYcUzvieyQqPek%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;999&quot; height=&quot;194&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;distributor &amp;mdash; 유입 (샘플/초)&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;sum(rate(cortex_distributor_received_samples_total[5m]))&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;모든 쓰기가 처음 닿는 관문이 받은 초당 샘플 &amp;mdash; Mimir Writes 대시보드의 첫 헤드라인&lt;/li&gt;
&lt;li&gt;Alloy 전송 처리량 합과 같아야 정상 &amp;mdash; 유실 0의 교차 증거 (실측 둘 다 약 3,500&amp;ndash;3,650)&lt;/li&gt;
&lt;li&gt;꺼지면 상류(수집&amp;middot;전송)가 멈춘 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;ingester &amp;mdash; 시리즈 보유&amp;middot;활성 vs 상한 300,000&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;cortex_ingester_memory_series            (보유 &amp;mdash; 파드별 세 선)
max(cortex_ingester_active_series)       (활성 &amp;mdash; 한 선)&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;보유 = 메모리에 든 것 &amp;mdash; &lt;b&gt;상한이 세는 대상(각 노드마다).&lt;/b&gt; 활성 = 최근 샘플이 온 것&lt;/li&gt;
&lt;li&gt;두 선의 간격 = 잔존. 라벨 변경 뒤 최대 두 절단 주기(약 4시간)까지 정상 &amp;mdash; 사본 걷을 때 12만이 이 모양&lt;/li&gt;
&lt;li&gt;보유가 파드별 세 선인 이유 = RF3 복제 &amp;mdash; 셋이 같아야 하고, 갈라지면 복제 이상. 집계하면 그 신호를 잃는다&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;Alloy 수집 &amp;mdash; job별 생산 시리즈&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;topk(12, sum by (job) (scrape_samples_scraped))&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;스크레이프 한 번에 시리즈당 샘플 하나 &amp;rarr; 이 값 = 그 타깃의 시리즈 수 &amp;mdash; 저장소를 훑지 않고 세는 방법&lt;/li&gt;
&lt;li&gt;패널 쿼리는 자동 갱신으로 열어 두면 상시 부하가 된다 &amp;mdash; 전 시리즈를 세는 쿼리 하나가 조회 컴포넌트를 42번 죽인 전례가 있어, 무거운 쿼리를 패널에 걸지 않는다&lt;/li&gt;
&lt;li&gt;bar gauge: 숫자 = 절대치, 막대 배경 전체 = 상한 300,000&lt;/li&gt;
&lt;li&gt;누가 상한을 먹는지 &amp;mdash; 상한 튜닝의 근거. k3s-server 하나가 절반 가까이&lt;/li&gt;
&lt;/ul&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;&lt;b&gt;6.2 읽기 행 &amp;mdash; 답하는가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기와 읽기는 독립적으로 고장 난다 &amp;mdash; 상한 사건 때 쓰기가 거절로 막히는 동안, 읽기는 별개로 querier가 93분 사이 25번 재시작하고 있었다. 조회는 query-frontend가 쿼리를 쪼개 querier에게 나눠 주고, querier가 인제스터(최근 2시간)와 store-gateway(그 전, MinIO 블록)를 합쳐 답한다. 컴포넌트를 나눈(microservices) 구성의 격리가 여기서 보인다 &amp;mdash; 읽기가 죽어도 쓰기는 돈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mAVWc/dJMcacRveXq/Hai9dB2MHUESC7ijBbZqGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mAVWc/dJMcacRveXq/Hai9dB2MHUESC7ijBbZqGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mAVWc/dJMcacRveXq/Hai9dB2MHUESC7ijBbZqGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmAVWc%2FdJMcacRveXq%2FHai9dB2MHUESC7ijBbZqGK%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;1017&quot; height=&quot;232&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;query-frontend &amp;mdash; 조회 요청 (상태코드)&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;sum by (status_code) (rate(cortex_request_duration_seconds_count{
  job=&quot;observability/query-frontend&quot;, route=~&quot;prometheus_api.*&quot;}[5m]))&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;조회 입구의 응답 분포 &amp;mdash; 조회 API(route 필터)만 남겨 내부 gRPC&amp;middot;헬스체크 소음 제거&lt;/li&gt;
&lt;li&gt;499 = 기다리다 취소(느림의 증상), 5xx = 고장&lt;/li&gt;
&lt;li&gt;이 대시보드의 패널 쿼리도 여기로 들어온다 &amp;mdash; 계기판 자신이 이 경로의 고객&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;query-frontend &amp;mdash; 조회 지연 p99&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;histogram_quantile(0.99, sum by (le) (rate(
  cortex_request_duration_seconds_bucket{
    job=&quot;observability/query-frontend&quot;, route=~&quot;prometheus_api.*&quot;}[5m])))&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;_bucket&lt;/code&gt;(지연이 le 이하인 요청 수의 구간별 누적)에서 &quot;100건 중 99건이 든 시간&quot;을 역산 &amp;mdash; 백분위의 표준 조합&lt;/li&gt;
&lt;li&gt;오르면 화면 체감이 먼저 느려진다&lt;/li&gt;
&lt;li&gt;최근 조회만 느리면 ingester 쪽, 과거 구간 조회가 느리면 store-gateway(MinIO) 쪽&lt;/li&gt;
&lt;/ul&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;&lt;b&gt;6.3 저장 여정&amp;middot;상태 행 &amp;mdash; 내려가는가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인제스터의 메모리는 종착지가 아니다. 2시간마다 메모리 구간(head)을 블록으로 떠 MinIO에 올리고(shipper), RF3라 세 벌 올라간 블록을 compactor가 병합한다. 이 길이 막히면 겉은 멀쩡한데 로컬 WAL 디스크만 차오른다 &amp;mdash; 증상이 늦게 드러나는 고장이라 축이 &quot;마지막 성공 후 경과&quot;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;231&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OIvpy/dJMcai5e6Wg/LPZfOmeKOQ6fM0eHig9c41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OIvpy/dJMcai5e6Wg/LPZfOmeKOQ6fM0eHig9c41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OIvpy/dJMcai5e6Wg/LPZfOmeKOQ6fM0eHig9c41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOIvpy%2FdJMcai5e6Wg%2FLPZfOmeKOQ6fM0eHig9c41%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;1027&quot; height=&quot;231&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;231&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h5&gt;&lt;b&gt;ingester shipper &amp;mdash; 마지막 블록 업로드 후 경과&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;time() - cortex_ingester_shipper_last_successful_upload_timestamp_seconds&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;&quot;마지막 성공 시각&quot; 게이지를 time()에서 빼면 경과 초 &amp;mdash; 이 류 메트릭의 표준 사용법&lt;/li&gt;
&lt;li&gt;평시 2시간 미만(업로드 주기). 4시간(빨강) = Mimir 공식 알림 기준&lt;/li&gt;
&lt;li&gt;그 너머면 블록이 안 나가 디스크만 차는 중 &amp;mdash; 끝은 쓰기 정지&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;compactor &amp;mdash; 마지막 병합 성공 후 경과&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;time() - cortex_compactor_last_successful_run_timestamp_seconds&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;같은 패턴, 대상만 compactor. 24시간(빨강) = 공식 알림 기준&lt;/li&gt;
&lt;li&gt;멈추면 같은 데이터가 세 벌씩 쌓여 저장&amp;middot;조회 비용이 계속 는다&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;ingester ring &amp;mdash; ACTIVE 수&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;max(cortex_ring_members{name=&quot;ingester&quot;, state=&quot;ACTIVE&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;해시 링 = 인제스터들이 자기 위치를 등록한 원형 명단 &amp;mdash; distributor가 여기서 담당자를 찾는다&lt;/li&gt;
&lt;li&gt;링을 지켜보는 컴포넌트마다 각자 보고하므로 sum은 관찰자 수만큼 부풀려진다(실측 21) &amp;rarr; max = 실제 멤버 수&lt;/li&gt;
&lt;li&gt;3 = 초록. 하나 빠지면 쓰기 quorum(2/3)의 여유가 사라진 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;b&gt;ingester &amp;mdash; 메모리 vs limit 2Gi&lt;/b&gt;&lt;/h5&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;process_resident_memory_bytes{job=&quot;observability/ingester&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;시리즈를 메모리에 드는 대가 &amp;mdash; 시리즈당 실측 약 4.2KiB, 30만 만재 시 약 1.2GiB&lt;/li&gt;
&lt;li&gt;빨간 선 = 컨테이너 limit 2Gi. 닿으면 OOMKilled &amp;mdash; WAL 재생으로 복구되지만 그동안 조회가 빈다&lt;/li&gt;
&lt;li&gt;시리즈 상한(거절)이 이 OOM을 막는 가드레일 &amp;mdash; 두 패널이 짝이다&lt;/li&gt;
&lt;/ul&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;&lt;b&gt;6.4 알림 후보 &amp;mdash; 대시보드에서 역추적한 셋&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드는 열어야 보이고, 알림은 안 봐도 온다. 개별 고장마다 알림을 걸지 않고, 여러 고장이 결국 흘러드는 하류 세 지점에만 건다 &amp;mdash; 울리면 어느 행을 열지도 정해져 있다.&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;th&gt;우산 아래 들어오는 고장&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;데이터 유실&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;거절 &amp;gt; 0 or 전송 실패 &amp;gt; 0&lt;/td&gt;
&lt;td&gt;쓰기&lt;/td&gt;
&lt;td&gt;상한 포화 &amp;middot; 카디널리티 폭발 &amp;middot; Mimir 사망&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인제스터 이탈&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ring ACTIVE &amp;lt; 3&lt;/td&gt;
&lt;td&gt;저장 여정&amp;middot;상태&lt;/td&gt;
&lt;td&gt;인제스터 크래시&amp;middot;OOM &amp;mdash; 최근 구간을 유일하게 든 컴포넌트가 빠짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;저장 정체&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;블록 업로드 경과 &amp;gt; 4시간&lt;/td&gt;
&lt;td&gt;저장 여정&amp;middot;상태&lt;/td&gt;
&lt;td&gt;플러시 정체 &amp;middot; MinIO 이상 &amp;middot; 디스크 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 다음 편으로 넘긴다. Grafana Alerting으로 룰을 ConfigMap에 담으면 대시보드와 같은 사슬로 배달되고, 수신 채널(Discord webhook)의 URL은 시크릿이라 봉인이 필요하다. 그 봉인 작업이 다음 편의 저장소 이전 작업과 같은 결이라 거기 묶는다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;7. 대조, 그리고 되짚기&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.1 시작할 때와 대조&lt;/b&gt;&lt;/h4&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;지금&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;관측이 실제로 흐르는지 확인한 적이 없다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;구조를 열어 저장된 15만의 절반이 사본임을 찾았다 &amp;mdash; 사본을 걷고 상한을 올려 거절 0, 보유 182,397 / 상한 300,000 (61%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Grafana 안이 백지다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파이프라인 자신을 보는 계기판 한 장이 코드로 들어갔다 &amp;mdash; Mimir 3축 12패널&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;앱 세 종이 &lt;code&gt;ImagePullBackOff&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;그대로 &amp;mdash; 이미지는 10부(CI/CD&amp;middot;레지스트리)에서 생긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 이 배포판의 성질 하나가 드러났다. &lt;b&gt;컨트롤플레인을 한 프로세스로 합쳤다는 사실이 자원 계산&amp;middot;장애 반경&amp;middot;메트릭 세 곳에서 같은 모양으로 나타난다.&lt;/b&gt; &lt;code&gt;kube-reserved&lt;/code&gt;를 따로 떼야 했던 것, 하나가 죽으면 넷이 같이 죽는 것, 두 포트가 같은 응답을 내던 것이 전부 같은 원인이다.&lt;/p&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;&lt;b&gt;7.2 남은 것과 걸렸던 것&lt;/b&gt;&lt;/h4&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;middot; 인제스터 이탈 &amp;middot; 저장 정체)과 조건까지 확정했고 구현은 9부다. 그때까지 이 계기판은 &quot;열어봐야 아는&quot; 상태로 남는다 &amp;mdash; 이 공백은 9부까지 안고 간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노드 자원 예약 반영.&lt;/b&gt; 설정 파일에는 2Gi, 노드에는 아직 1Gi다. 세 대 순차 재시작이 필요하고, 앱이 올라와 실사용을 다시 잰 뒤 request 전반과 함께 조정한다. 그 재시작이 복제(RF3)가 실제로 일하는지 보는 첫 시험이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인제스터 request가 지금은 실사용보다 크다.&lt;/b&gt; request 1Gi에 실사용 약 478 MiB &amp;mdash; 30만 만재(약 1.2 GiB)를 내다보고 절충한 값이라 지금은 절반이 놀고, 만재에 실제로 가까워지면 재조정이 다시 필요하다. 앱 이후 실측으로 다시 잡는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MinIO 요청 실패가 초당 0.29로 잡혀서 파봤다 &amp;mdash; 고장이 아니었다.&lt;/b&gt; 실패의 전부가 compactor의 get이고, MinIO 응답은 4xx(없음)다. compactor는 &quot;이 블록의 인덱스 헤더가 이미 있나&quot;, &quot;이 조각(partial) 블록의 메타가 있나&quot;를 조회해 없으면 만들거나 치우는데, 그 &quot;없음(404)&quot; 응답이 실패 카운터에 계수된다. 작업 성공 로그까지 확인했다. &lt;b&gt;실패 카운터가 0이 아닌 것과 고장은 다르다&lt;/b&gt; &amp;mdash; 이 값으로 알림을 걸었다면 상시 오탐이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실측 없이 잡았다가 걸린 자원 값이 이 편에서만 셋 나왔다. 전부 같은 모양이다 &amp;mdash; &lt;b&gt;재보지 않고 정한 값.&lt;/b&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;th&gt;고친 값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인제스터 request&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;512Mi&lt;/td&gt;
&lt;td&gt;실사용이 넘어서 eviction 우선순위가 앞당겨지는 상태&lt;/td&gt;
&lt;td&gt;1Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;대시보드 사이드카 limit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;64Mi&lt;/td&gt;
&lt;td&gt;기동 직후 &lt;code&gt;exit 137&lt;/code&gt; 반복 &amp;mdash; watch 연결과 런타임을 든 컨테이너였다&lt;/td&gt;
&lt;td&gt;192Mi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;조회 컴포넌트 limit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;256Mi&lt;/td&gt;
&lt;td&gt;전 시리즈를 훑는 패널 쿼리에 42회 &lt;code&gt;exit 137&lt;/code&gt;, 다른 패널까지 전부 시간 초과&lt;/td&gt;
&lt;td&gt;512Mi&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;세 번째는 자원만의 문제가 아니었다 &amp;mdash; 계기판에 넣은 쿼리 자체가 저장소를 무너뜨리고 있었고, 쿼리를 가벼운 것으로 바꾼 뒤에야 안정됐다. 계기판을 세우는 일이 그 자체로 저장소에 부하를 얹는다는 것을 여기서 겪었다. 진단 첫머리에 보였던 25회 재시작도 같은 자리다 &amp;mdash; 무거운 조회가 올 때마다 256Mi에 걸려 죽던 것이고, 512Mi로 올린 뒤 재시작은 0이다.&lt;/p&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.3 실무에서 짚이는 것 &amp;mdash; 보안 쪽&lt;/b&gt;&lt;/h4&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; 시리즈 상한은 자원 보호 장치이면서 &lt;b&gt;카디널리티 폭탄에 대한 방어&lt;/b&gt;다 &amp;mdash; 라벨에 요청 ID처럼 값이 무한한 것을 넣는 코드 하나면 저장소가 마비된다. 30만으로 올린 만큼 그 방어선도 뒤로 갔고, 그 자리를 메울 알림이 9부까지 비어 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수집기 권한을 좁혀 둔 것이 여기서 값을 했다.&lt;/b&gt; Alloy에서 Secret 읽기 권한을 뺐기 때문에 apiserver 인증을 ServiceMonitor로 못 걸었고 설정에 직접 쓰는 경로로 갔다. 권한을 넓혔다면 더 편했겠지만, 수집기 하나가 클러스터의 모든 시크릿을 읽는 상태가 됐을 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대시보드 사이드카의 감시 범위도 같은 결이다.&lt;/b&gt; 전역으로 두면 편하지만 RBAC이 전 네임스페이스로 넓어진다. 대시보드를 한 네임스페이스에만 두기로 하고 범위를 거기로 묶었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메트릭 자체가 유출 통로가 될 수 있다.&lt;/b&gt; 라벨에 경로&amp;middot;쿼리스트링이 통째로 들어가면 토큰이 메트릭에 실려 저장소로 간다. 지금은 인프라 지표뿐이라 해당이 없고, 앱이 붙는 편에서 앱이 내는 라벨을 볼 때의 확인 항목이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grafana는 아직 클러스터 밖으로 안 열려 있다.&lt;/b&gt; 노출하는 순간 인증&amp;middot;TLS&amp;middot;접근 제어가 따라온다 &amp;mdash; 9부에서 Traefik으로 열 때의 일이다. 알림에 쓸 webhook URL도 시크릿이라 봉인(SealedSecret) 대상이고, GitLab 저장소 접근 자격과 같은 결로 그때 함께 처리한다.&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.4 다음 &amp;mdash; GitOps 루프부터&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 편에서 대시보드를 고칠 때마다 push 후 최대 3분을 기다렸다. ArgoCD가 저장소를 3분 주기 폴링으로 보고 있어서인데 &amp;mdash; 사실 이 지연을 체감하기 전까지 webhook이라는 선택지를 검토한 적 자체가 없었다. 폴링은 선택한 것이 아니라 기본값이라 그냥 돌던 것이다. 이번에 따져 보니 지금 구조에서는 걸고 싶어도 못 건다 &amp;mdash; GitHub(인터넷)가 ArgoCD에 요청을 보내려면 ArgoCD가 밖으로 열려 있어야 하는데, 노출은 뒤로 미뤄 둔 상태다.&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;9부는 그 루프부터 고친다 &amp;mdash; GitLab을 데스크탑에 세워 GitOps 소스를 옮기면 Git 서버와 ArgoCD가 같은 망에 있게 되고, 노출 없이 webhook이 성립해 push 즉시 sync가 된다. 알림 세 개가 그 위에서 서고, 그다음 인프라 대시보드 &amp;mdash; 클러스터 자체를 보는 눈 &amp;mdash; 를 짓는다.&lt;/p&gt;</description>
      <category>HomeLab</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/217</guid>
      <comments>https://zed6740.tistory.com/217#entry217comment</comments>
      <pubDate>Sun, 2 Aug 2026 17:56:04 +0900</pubDate>
    </item>
    <item>
      <title>노트북 홈서버 k3s 구축기 - 번외. e1000e NIC hang</title>
      <link>https://zed6740.tistory.com/216</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 먼저 &amp;mdash; 내 네트워크 골목의 구조&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홈랩을 진행하던 도중 장애가 발생했다. 데스크탑에서 서버로의 ssh, proxmox web, argocd web이 전부 다운되었다. 그래서 서버가 죽었나 싶어서 닫혀있던 노트북을 열고 root로 진입해보니 pve(내가 만든 proxmox 호스트 - .200)는 잘 살아있었고, pve에서 201,202,203으로도 접근이 잘 되었다. 근데 왜 데스크탑에서 연결이 갑자기 한순간에 다 끊어졌을까.&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;이 장애는 &quot;&lt;b&gt;안에서는 되는데 밖에서만 안 된다&lt;/b&gt;&quot;는 모양이었다. 그래서 원인을 이해하려면, 3부에서 세운 네트워크 구조를 먼저 알아야 한다. &lt;a href=&quot;https://zed6740.tistory.com/211&quot;&gt;3부&lt;/a&gt;에서 깐 배선을 한 장으로 요약하면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                      공유기 (TP-Link, 192.168.0.1)
                          │  (유선)
                     노트북 유선 NIC = nic0 (내장 Intel, 드라이버 e1000e)
                          │
                     ┌──  vmbr0 (Proxmox 가상 스위치)  ──┐
                     │                                    │
             Proxmox 호스트(.200)              k3s VM  .201 &amp;middot; .202 &amp;middot; .203&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 세 가지(3부에서 정한 것):&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;이고, 그게 내장 Intel NIC(&lt;code&gt;nic0&lt;/code&gt;, 드라이버 &lt;code&gt;e1000e&lt;/code&gt;)다. 3부 설치 때 관리 인터페이스로 이걸 고정했다.&lt;/li&gt;
&lt;li&gt;Proxmox는 &lt;b&gt;&lt;code&gt;vmbr0&lt;/code&gt;라는 가상 스위치&lt;/b&gt;를 만들어, 그 위에 &lt;b&gt;호스트 자신(.200)과 VM 세 대(.201~203)를 다 물린다.&lt;/b&gt; 그 가상 스위치의 &quot;바깥 포트&quot;에 물리 NIC(&lt;code&gt;nic0&lt;/code&gt;)을 꽂아 진짜 네트워크로 내보낸다. 그래서 공유기 눈에는 노트북 1대로만 보인다.&lt;/li&gt;
&lt;li&gt;데스크탑(콘솔, &lt;code&gt;192.168.0.x&lt;/code&gt;)은 같은 공유기 골목에 있고, 여기서 &lt;code&gt;kubectl&lt;/code&gt;&amp;middot;SSH&amp;middot;Proxmox 웹UI로 서버를 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 &lt;b&gt;&quot;밖으로 나가는 유일한 길이 nic0&quot;&lt;/b&gt;라는 게 이 사건의 전부다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 증상 &amp;mdash; 갑자기 다 안 닿는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소처럼 데스크탑에서 클러스터를 만지려는데, 전부 막혔다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# kubectl
Unable to connect to the server: dial tcp 192.168.0.201:6443: connect: no route to host

# SSH
ssh: connect to host 192.168.0.201 port 22: Connection timed out&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 노드(.201)만이 아니라 &lt;b&gt;Proxmox 웹UI(&lt;code&gt;https://192.168.0.200:8006&lt;/code&gt;)도 안 열렸다.&lt;/b&gt; 선은 안 건드렸는데.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 머릿속에 스치는 건 최악들이다 &amp;mdash; 서버가 죽었나? 디스크가 나갔나? 침해인가? 근데 여기서 답으로 점프하면 안 된다. &lt;b&gt;&quot;어디가 문제인가&quot;를 한 번에 반씩 좁혀&lt;/b&gt; 내려가야 한다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;2. 반씩 좁히기 (1) &amp;mdash; 클러스터냐, 호스트냐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.201&lt;/code&gt;(k3s 노드)도 &lt;code&gt;.200&lt;/code&gt;(Proxmox 호스트)도 안 된다. 노드 하나만 문제면 다른 노드는 됐을 텐데, &lt;b&gt;호스트까지 안 되니 &quot;한 대&quot;가 아니라 &quot;그 위 전부&quot;가 안 닿는 것&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;여기서 구조(&amp;sect;0)가 값을 한다 &amp;mdash; &lt;code&gt;.200&lt;/code&gt;은 노트북 본체(Proxmox 호스트)고, &lt;code&gt;.201~203&lt;/code&gt;은 그 위 VM이다. &lt;b&gt;호스트가 안 닿으면 그 위 VM도 다 같이 안 닿는다.&lt;/b&gt; 그러니 문제는 k3s가 아니라 노트북 본체 쪽으로 좁혀진다.&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;code&gt;no route to host&lt;/code&gt;라는 에러도 힌트다 &amp;mdash; 이건 &quot;상대가 응답을 거부했다&quot;가 아니라 &lt;b&gt;&quot;상대 기계로 가는 길 자체가 없다&quot;&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;b&gt;&amp;rarr; 좁혀진 것: k3s 문제 아님. 노트북 본체가 통째로 안 닿는다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 반씩 좁히기 (2) &amp;mdash; 호스트가 죽었나, 네트워크만 죽었나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노트북 본체로 걸어가 콘솔에 직접 로그인해 봤다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;root@pve:~#&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;잘 들어가진다.&lt;/b&gt; 서버는 켜져 있고 OS도 멀쩡하다. 그러면 &quot;기계가 죽은&quot; 게 아니라 &lt;b&gt;&quot;네트워크로만 못 닿는&quot; 것&lt;/b&gt;이다. 여기서 최악의 시나리오(디스크&amp;middot;침해)는 일단 배제된다.&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;&amp;rarr; 좁혀진 것: 호스트는 살아 있다. 네트워크 도달성 문제다.&lt;/b&gt;&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;4. 반씩 좁히기 (3) &amp;mdash; 어느 쪽이 끊겼나 (세 번의 ping)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크가 문제면, 끊긴 지점이 &lt;b&gt;호스트 쪽인지 &amp;middot; 데스크탑 쪽인지 &amp;middot; 그 사이(공유기)인지&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;b&gt;① 호스트 &amp;rarr; VM (&lt;code&gt;.200&lt;/code&gt;에서 &lt;code&gt;.201&lt;/code&gt;로 SSH)&lt;/b&gt; &amp;mdash; 된다.&lt;br /&gt;&lt;code&gt;vmbr0&lt;/code&gt;(가상 스위치) 내부는 살아 있다는 뜻이다. 물리 NIC을 거치지 않고 소프트웨어 스위치 안에서만 오가는 길이라, 여기가 되는 건 &quot;내부는 정상&quot;의 증거다.&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;② 호스트 &amp;rarr; 공유기 (&lt;code&gt;.200&lt;/code&gt;에서 &lt;code&gt;.1&lt;/code&gt;로 ping)&lt;/b&gt; &amp;mdash; 안 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;root@pve:~# ping -c2 192.168.0.1
(응답 없음)&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;③ 데스크탑 &amp;rarr; 공유기 (&lt;code&gt;.167&lt;/code&gt;에서 &lt;code&gt;.1&lt;/code&gt;로 ping)&lt;/b&gt; &amp;mdash; 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 결과를 겹치면 답이 나온다:&lt;/p&gt;
&lt;table style=&quot;height: 86px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;경로&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;결과&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;뜻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;호스트 &amp;rarr; VM (vmbr0 내부)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;가상 스위치 내부 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;데스크탑 &amp;rarr; 공유기&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;공유기&amp;middot;데스크탑은 멀쩡&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;호스트 &amp;rarr; 공유기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;X&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;호스트가 밖으로 못 나간다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유기는 살아 있고(데스크탑이 닿으니까), vmbr0 내부도 살아 있는데(호스트&amp;harr;VM 되니까), &lt;b&gt;오직 호스트에서 공유기로 나가는 업링크(상위 망으로 나가는 연결)만 죽었다.&lt;/b&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;Proxmox의 가상 스위치인 vmbr0의 바깥 포트로 물린 nic0이 문제다.&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;&amp;rarr; 좁혀진 것: 호스트의 업링크(nic0 &amp;rarr; 공유기) 구간이 끊겼다.&lt;/b&gt;&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;5. 물리냐 데이터냐 &amp;mdash; carrier와 ARP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업링크가 죽었다니, 제일 먼저 의심할 건 &quot;선이 빠졌나&quot;다. 근데 확인해 보면 선은 멀쩡하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;root@pve:~# ip -br link
lo          UNKNOWN
nic0        UP           &amp;larr; 유선 NIC (밖으로 나가는 길)
wlp0s20f3   DOWN         &amp;larr; WiFi (안 씀)
vmbr0       UP           &amp;larr; 가상 스위치
tap201i0    UNKNOWN      ┐
tap202i0    UNKNOWN      ├ VM 3대가 붙은 가상 포트
tap203i0    UNKNOWN      ┘

root@pve:~# ethtool nic0 | grep -i link
        Link detected: yes&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;ip -br link: 리눅스 네트워크 도구(ip)로 포트(인터페이스) 자체를 보는 것(link). 짧게 한 줄씩(-br).&amp;nbsp;&lt;/li&gt;
&lt;li&gt;ethtool nic | grep -i link: nic 하드웨어에 직접 물어봐서(ethtool nic), link가 든 줄만 검색 - 물리적 선이 연결 되어있나&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;code&gt;nic0&lt;/code&gt;은 &lt;code&gt;UP&lt;/code&gt;, &lt;code&gt;Link detected: yes&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;&lt;b&gt;carrier(링크) &amp;ne; 데이터 흐름&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;carrier&lt;/b&gt; =&lt;b&gt; &quot;선이 전기적으로 연결됐다.&quot;&lt;/b&gt; NIC과 공유기가 서로 &quot;우리 연결됨&quot;을 확인한 상태. &lt;b&gt;랜포트 LED가 켜지는 게 이거다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터가 흐른다&lt;/b&gt; = 실제로 프레임(데이터 조각)이 오간다. carrier가 있어도 이게 따로 안 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전화선 비유를 AI가 들어줬다. &amp;mdash;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;carrier는 수화기를 들었을 때 &quot;뚜&amp;mdash;&quot; 하는 다이얼톤이다(선은 살아 있다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 실제 통화다. &lt;b&gt;다이얼톤은 들리는데 통화가 안 되는 상태&lt;/b&gt; = carrier 있는데 데이터 안 감. 지금이 딱 그거다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 데이터가 실제로 가는지를 봐야 한다. 그 지표가 &lt;b&gt;ARP&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;&lt;b&gt;ARP &amp;mdash; IP를 실제 주소(MAC)로 바꾸는 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;같은 골목(랜-192.168.0.~)에서 IP로 통신하려면,&lt;/b&gt; 그 IP의 &lt;b&gt;MAC 주소&lt;/b&gt;(하드웨어 주소)가 필요하다. IP는 &quot;이름&quot;, MAC은 &quot;실제 집주소&quot;라고 보면 된다. 이름만으론 택배를 못 보낸다 &amp;mdash; 집주소를 알아야 보낸다.&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;그 집주소를 알아내는 게 ARP(Address Resolution Protocol)다. - 주소 해석 프로토콜.&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;&quot;192.168.0.1(공유기) 가진 놈 누구야? MAC 알려줘&quot;&lt;/b&gt;라고 외치고(브로드캐스트), 공유기가 &quot;나야, 내 MAC은 이거야&quot;라고 답하면, 그제서야 프레임을 보낼 수 있다.&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;서버에서 직접 ARP 결과를 봤다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;root@pve:~# ip neigh show 192.168.0.1
192.168.0.1 dev vmbr0 FAILED&lt;/code&gt;&lt;/pre&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;ip neigh show &quot; 192.168.0.1 &quot;: 같은 사설망에 있는 주소(공유기가 제공하는)에 즉, L2에 있는 기기들(공유기, VM, 데스크탑) 중 공유기 주소(192.168.0.1)의 MAC을 보여줘&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;FAILED&lt;/code&gt;&lt;/b&gt; &amp;mdash; 공유기의 MAC을 못 얻었다. &quot;공유기 누구야?&quot;라고 외쳤는데 답이 안 온 것이다. 즉 &lt;b&gt;프레임이 서버에서 공유기까지 건너가지 못한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 &amp;mdash; &lt;b&gt;carrier(불)는 켜졌는데, ARP는 실패한다.&lt;/b&gt; 선은 붙었지만 실제 데이터는 한 발짝도 못 나간다. 모순이 아니라, 이 둘이 서로 다른 층이라서 이렇게 갈린다. 원인은 &quot;선&quot;이 아니라, 그 위 어딘가에서 데이터가 얼어붙은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(브리지 설정도 의심해 &lt;code&gt;ip link show master vmbr0&lt;/code&gt;로 확인했지만, &lt;code&gt;nic0&lt;/code&gt;은 정상적으로 &lt;code&gt;vmbr0&lt;/code&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;&amp;rarr; 좁혀진 것: 물리 선은 정상(carrier yes), 그런데 데이터가 안 감(ARP FAILED). 그 사이 어딘가에서 프레임이 멈춘다.&lt;/b&gt;&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;6. 근본 원인 &amp;mdash; NIC이 멈췄다 (dmesg)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;선은 붙었는데 데이터가 멈춘다&quot;의 답은 커널 로그에 있다. &lt;code&gt;dmesg&lt;/code&gt;를 봤더니 같은 줄이 수십 번 찍혀 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dmesg: 커널이 남긴 로그를 화면에 뿌려주는 명령어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;root@pve:~# dmesg -T | grep -iE &quot;e1000e|nic0|hang|link&quot; | tail
... e1000e 0000:00:1f.6 nic0: Detected Hardware Unit Hang:      (34번 반복)
... e1000e 0000:00:1f.6 nic0: NIC Link is Down
... vmbr0: port 1(nic0) entered disabled state
... e1000e 0000:00:1f.6 nic0: NIC Link is Up 1000 Mbps Full Duplex
... vmbr0: port 1(nic0) entered forwarding state&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;e1000e 0000:00:1f.6 nic0&lt;/code&gt; &amp;mdash; 3부에서 고정했던 그 &lt;b&gt;내장 Intel NIC&lt;/b&gt;(PCI 주소 &lt;code&gt;00:1f.6&lt;/code&gt;, 드라이버 &lt;code&gt;e1000e&lt;/code&gt;)다. 그리고 &lt;code&gt;Detected Hardware Unit Hang&lt;/code&gt;. 이걸 이해하려면 두 개념이 필요하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NIC &quot;hang&quot;이 뭐냐&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NIC 안에는 패킷을 실제로 처리하는 회로(엔진)가 있다.&lt;b&gt; 컴퓨터가 보낼 프레임&lt;/b&gt;들을 &lt;b&gt;NIC&lt;/b&gt;의 &lt;b&gt;송신 큐(TX queue, 보낼 것 대기줄)&lt;/b&gt;에 넣으면, NIC 엔진이 거기서 하나씩 꺼내 선으로 밀어낸다.&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;hang = 그 엔진이 멈춰버린 것.&lt;/b&gt; 큐에서 프레임을 안 꺼내고 얼어붙어, 대기줄에 쌓이기만 하고 밖으로 하나도 안 나간다. NIC은 전원 들어와 있고 링크(carrier)도 붙어 있는데, 처리 회로만 멈춘 상태다.&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;code&gt;Detected Hardware Unit Hang&lt;/code&gt;은 &lt;b&gt;드라이버(e1000e)가 &quot;NIC 처리 회로가 큐를 안 비우고 멈췄다&quot;를 감지&lt;/b&gt;해 찍는 메시지다. 34번 반복 = 34번 감시하는 동안 계속 멈춰 있었다는 뜻. 그래서 ARP 프레임조차 못 나갔고(&amp;sect;5의 FAILED), carrier만 켜진 채 데이터가 정지했던 것이다.&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;AI가 우편 분류기로 비유해줬다. &amp;mdash; NIC은 편지를 in-tray(큐)에서 꺼내 밖으로 보내는 자동 기계다. hang은 그 기계 모터가 잼(jam) 걸려 멈춘 것. 전원은 켜졌는데(불) 편지가 안 나간다.&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;b&gt;왜 하필 이 NIC이 멈췄나 &amp;mdash; 오프로드 버그&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 데이터는 그대로 못 나가고 &lt;b&gt;작은 프레임(약 1500바이트씩)으로 쪼개야&lt;/b&gt; 한다. 이 쪼개는 일을 보통은 CPU가 하는데, 성능을 위해 &lt;b&gt;NIC 하드웨어한테 떠넘기는 기능&lt;/b&gt;이 있다 &amp;mdash; 이걸 &lt;b&gt;오프로드&lt;/b&gt;(TSO: TCP Segmentation Offload 등)라고 한다. &quot;큰 덩어리 줄 테니 네가 쪼개서 보내&quot; 하고 NIC에 통째로 넘기는 것이다.&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;Intel e1000e 계열(I219/I218) 칩의 그 쪼개기 하드웨어에 결함&lt;/b&gt;이 있다는 점이다. 특정 상황에서 큰 덩어리를 쪼개다가 엔진이 잼(hang) 걸린다. 그게 &lt;code&gt;Hardware Unit Hang&lt;/code&gt;의 정체 &amp;mdash; 쪼개다가 멈춘 것이다. 널리 알려지고 문서화된 버그다.&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;(참고로 4부에서 &quot;USB 자동절전 차단&quot;을 해뒀지만, 그건 &lt;b&gt;외장 SSD(USB)&lt;/b&gt;를 위한 거였다. 이 &lt;b&gt;NIC&lt;/b&gt;은 USB가 아니라 &lt;b&gt;노트북 내장 &lt;/b&gt;이라&amp;nbsp;절전과 무관하다. 실제로 확인해도 &lt;code&gt;usbcore.autosuspend&lt;/code&gt;는 &lt;code&gt;-1&lt;/code&gt;(꺼짐), sleep 타깃은 &lt;code&gt;masked&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; 근본 원인: Intel e1000e NIC의 세그멘테이션 오프로드 버그로 인한 TX hang.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 해결 &amp;mdash; 오프로드를 끈다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://forum.proxmox.com/threads/intel-nic-e1000e-hardware-unit-hang.106001/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://forum.proxmox.com/threads/intel-nic-e1000e-hardware-unit-hang.106001/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1785312170683&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[SOLVED] - Intel NIC e1000e hardware unit hang&quot; data-og-description=&quot;This has been discussed many times, is there a solution/workaround yet, as the specified steps are not working for me? I've the following in /etc/network/interfaces auto lo iface lo inet loopback iface eno1 inet manual offload-gso off offload-gro off offlo&quot; data-og-host=&quot;forum.proxmox.com&quot; data-og-source-url=&quot;https://forum.proxmox.com/threads/intel-nic-e1000e-hardware-unit-hang.106001/&quot; data-og-url=&quot;https://forum.proxmox.com/threads/intel-nic-e1000e-hardware-unit-hang.106001/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hULcE/dJMb8SpWbLo/FY4QMZKzDmGYHkJNdxK9h1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/sLd6p/dJMb8TCn0XF/gkCHpJ2J6ND2S74KFN8T90/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://forum.proxmox.com/threads/intel-nic-e1000e-hardware-unit-hang.106001/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://forum.proxmox.com/threads/intel-nic-e1000e-hardware-unit-hang.106001/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hULcE/dJMb8SpWbLo/FY4QMZKzDmGYHkJNdxK9h1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/sLd6p/dJMb8TCn0XF/gkCHpJ2J6ND2S74KFN8T90/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[SOLVED] - Intel NIC e1000e hardware unit hang&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This has been discussed many times, is there a solution/workaround yet, as the specified steps are not working for me? I've the following in /etc/network/interfaces auto lo iface lo inet loopback iface eno1 inet manual offload-gso off offload-gro off offlo&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;forum.proxmox.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;원인이 &quot;NIC이 쪼개다가 멈춘다&quot;이니, 해결은 &lt;b&gt;&quot;NIC 말고 CPU가 쪼개게&quot;&lt;/b&gt; 바꾸는 것이다. 그러면 버그난 쪼개기 회로를 아예 안 쓴다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 문제 오프로드 끄기 (CPU가 쪼개도록)
ethtool -K nic0 tso off gso off gro off&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 CPU가 미리 1500바이트짜리로 쪼개 넘겨주고, NIC은 밀어내기만 한다 &amp;rarr; 불량 쪼개기 회로를 안 쓰니 잼이 안 난다. 대가는 CPU가 쪼개기를 대신 하는 것뿐인데, 기가비트 홈랩에선 체감이 없다.&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;재부팅해도 유지되게 udev 규칙으로 못 박는다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;echo 'ACTION==&quot;add&quot;, SUBSYSTEM==&quot;net&quot;, KERNEL==&quot;nic0&quot;, RUN+=&quot;/sbin/ethtool -K nic0 tso off gso off gro off&quot;' \
  &amp;gt; /etc/udev/rules.d/70-nic0-offload.rules
udevadm control --reload&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;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;root@pve:~# ethtool -k nic0 | grep -iE &quot;tcp-segmentation|generic-segmentation|generic-receive&quot;
tcp-segmentation-offload: off
generic-segmentation-offload: off
generic-receive-offload: off&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋 다 &lt;code&gt;off&lt;/code&gt;. 이 시점부터 hang은 안 난다. 데스크탑에서 &lt;code&gt;kubectl&lt;/code&gt;&amp;middot;SSH&amp;middot;Proxmox 웹UI가 전부 다시 붙었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 정리 &amp;mdash; 이 장애에서 남는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과부터&lt;/b&gt; &amp;mdash; 클러스터&amp;middot;VM&amp;middot;데이터는 한 번도 죽지 않았다. 죽은 건 데스크탑에서 서버로 가는 &lt;b&gt;네트워크 경로 하나&lt;/b&gt;(내장 NIC의 드라이버 버그)뿐이었다. 처음엔 &quot;서버가 나갔나, 침해인가&quot; 싶던 게, 층층이 좁히니 &quot;NIC 드라이버 quirk&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;한 번에 반씩 좁힌다.&lt;/b&gt; 클러스터냐 호스트냐 &amp;rarr; 호스트가 죽었냐 네트워크냐 &amp;rarr; 어느 쪽이 끊겼냐. 답으로 점프하지 않고 매 단계에서 절반을 배제한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아래 층부터 위로 본다.&lt;/b&gt; 물리(선/carrier) &amp;rarr; 데이터(ARP) &amp;rarr; 드라이버(dmesg). 밑이 안 되면 위는 볼 필요도 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;carrier(불) &amp;ne; 데이터.&lt;/b&gt; 랜포트 불이 켜졌다고 통신되는 게 아니다. &lt;code&gt;ethtool&lt;/code&gt;의 &lt;code&gt;Link detected&lt;/code&gt;와 &lt;code&gt;ip neigh&lt;/code&gt;의 상태를 나눠 봐야 &quot;선인지 데이터인지&quot;가 갈린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인은 결국 로그(dmesg)가 말해줬다.&lt;/b&gt; 증상만 쫓지 말고, 커널이 남긴 기록을 봐야 근본이 나온다.&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;NIC(부품) &amp;rarr; 드라이버(그걸 굴리는 SW) &amp;rarr; vmbr0(가상 스위치, 호스트&amp;middot;VM&amp;middot;NIC을 묶음)&lt;/b&gt;. 그래서 밖은 막혀도 호스트&amp;harr;VM은 됐다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;carrier vs 데이터 / IP&amp;middot;MAC&amp;middot;ARP&lt;/b&gt; &amp;mdash; 불 켜졌는데 왜 안 됐나.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;hang(엔진 잼)&amp;middot;오프로드&amp;middot;Intel e1000e 버그&lt;/b&gt; &amp;mdash; 왜 멈췄고 왜 오프로드를 끄면 낫나.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홈랩을 켜놓고 운영한다는 건, 이런 물리&amp;middot;드라이버 층의 quirk를 만나 밑단부터 짚어 내려가는 일이기도 하다. 이번엔 그게 오래된 Intel NIC 하나였다.&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;9. 후속 &amp;mdash; 며칠 후, 같은 자리가 또 끊겼다&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;며칠 후 데스크탑에서 같은 증상이 났다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;kubectl은&lt;span&gt;&amp;nbsp;&lt;/span&gt;no route to host, 공유기(.1)는 응답하고 호스트(.200)&amp;middot;VM(.201&amp;ndash;203)은 전부 무응답 &amp;mdash; 같은 분기다. 이번엔 좁히기가 빨랐다. 콘솔에서 dmesg를 보니 같은 줄이 다시 쌓여 있었다.&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;dmesg:&lt;/b&gt; 커널이 남긴 로그를 화면에 뿌려주는 명령어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;[Sun Aug  2 14:54:04 2026] e1000e 0000:00:1f.6 nic0: Detected Hardware Unit Hang:
(약 5분 동안 164회)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 hang이다. 그런데 오프로드 상태를 확인하니 이야기가 달랐다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;root@pve:~# ethtool -k nic0 | grep -E &quot;tcp-segmentation|generic-segmentation|generic-receive&quot;
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;꺼 뒀던 셋이 전부 도로 켜져 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;uptime이 이유를 말해줬다 &amp;mdash; 하루 전에 재부팅이 있었다.(노트북 전원 나가서 서버도 꺼져버려서 다시킴)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;끈 날        ethtool로 off &amp;rarr; hang 멈춤
하루 전      재부팅 &amp;rarr; 오프로드가 기본값(on)으로 복귀
오늘 14:54   켜진 오프로드로 같은 버그 &amp;rarr; hang 164회&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;hang이 &quot;다시&quot; 난 게 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;처방이 사라진 자리에서 원래 병이 그대로 난 것&lt;/b&gt;이다. 왜 처방이 사라졌는지를 이해하려면, 그때 쓴 도구 둘의 정체부터 갈라야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ethtool이 고치는 것은 파일이 아니라 메모리다&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; ethtool:&lt;/b&gt; 리눅스 환경에서 네트워크 카드(NIC)의 상태를 조회하고 하드웨어 설정을 변경하는 데 쓰는 명령어 도구&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;드라이버에는 조절 손잡이가 여럿 있다 &amp;mdash; 속도, 각종 오프로드의 켜고 끄기, 통계.&lt;span&gt;&amp;nbsp;&lt;/span&gt;ethtool은 그 손잡이를 만지는 명령줄 도구다(-k&lt;span&gt;&amp;nbsp;&lt;/span&gt;소문자는 보기,&lt;span&gt;&amp;nbsp;&lt;/span&gt;-K&lt;span&gt;&amp;nbsp;&lt;/span&gt;대문자는 바꾸기).&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성질이 하나 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ethtool은 지금 떠 있는 드라이버의 상태만 바꾼다. 어떤 파일에도 기록되지 않는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;재부팅하면 드라이버가 새로 뜨면서 공장 기본값 &amp;mdash; 오프로드 전부 켜짐 &amp;mdash; 으로 돌아온다. 오프로드를 끈 그 명령은 살아 있는 드라이버의 값을 바꾼 것뿐이었고, 재부팅과 함께 증발했다. 메모리니까, 재부팅하면 그 값은 유지되지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그 증발을 막으라고 심은 것이 udev 규칙이었다&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; udev:&lt;/b&gt; 리눅스 커널을 위한 장치 관리자. /dev 디렉터리의 장치 파일을 동적으로 생성하고 관리하며, USB 등 장치 연결 이벤트를 처리.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;리눅스에서 장치가 꽂히거나 부팅 중에 발견되면, 커널이 &quot;장치가 나타났다(add)&quot;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이벤트&lt;/b&gt;를 쏜다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;udev는 그 이벤트를 받아 반응하는 상주 서비스고, 반응 내용은 규칙 파일(/etc/udev/rules.d/*.rules)에 &quot;&lt;b&gt;이런 조건의 장치가 이런 이벤트를 내면 &amp;rarr; 이걸 해라&lt;/b&gt;&quot; 꼴로 적는다. &lt;b&gt;USB를 꽂으면 장치 파일이 생기는 것, 이 NIC이&lt;span&gt;&amp;nbsp;&lt;/span&gt;nic0이라는 고정 이름을 갖는 것 &amp;mdash; 전부 udev가 규칙대로 반응한 결과&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 만든 규칙이 &quot;&lt;b&gt;이름이&lt;span&gt;&amp;nbsp;&lt;/span&gt;nic0인&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;장치가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;나타나면(add)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;b&gt; ethtool로 오프로드를 꺼라( &lt;b&gt;NIC 말고 CPU가 쪼개게&lt;/b&gt; )&lt;/b&gt;&quot;였다. 부팅마다 자동으로 다시 꺼지게 하려는 의도였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;불발 &amp;mdash; add 순간엔 nic0이라는 이름이 없다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부팅 로그가 불발의 이유를 말해준다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;root@pve:~# journalctl -b | grep -iE &quot;renamed&quot; | head -2
Aug 01 11:46:08 pve kernel: e1000e 0000:00:1f.6 eno0: renamed from eth0
Aug 01 11:46:08 pve kernel: e1000e 0000:00:1f.6 nic0: renamed from eno0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부팅 때 이&lt;b&gt; NIC은&lt;span&gt;&amp;nbsp;&lt;/span&gt;eth0이라는 임시 이름으로 태어나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;eno0을 거쳐&lt;span&gt;&amp;nbsp;&lt;/span&gt;nic0이 된다.&lt;/b&gt; 이 개명은 이름 고정용 설정(/usr/local/lib/systemd/network/50-pmx-nic0.link&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 3부에서 이름을 고정할 때 생긴 파일)을 읽은 udev가,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;add 이벤트를 처리하는 과정에서&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;수행한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 순서가 갈린다. add 이벤트가 접수되는 순간의 이름은&lt;span&gt;&amp;nbsp;&lt;/span&gt;eth0이고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;nic0은 그 이벤트&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;처리의 결과물&lt;/b&gt;이다. 그런데 규칙의 조건은 &quot;이름이&lt;span&gt;&amp;nbsp;&lt;/span&gt;nic0인 장치가 나타나면&quot;이었다 &amp;mdash; 나타나는 순간엔 그 이름이 아직 없다. 조건이 성립하는 순간 자체가 존재하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;[add 이벤트]   이름 = eth0
    ├─ 심어둔 규칙: &quot;nic0이면 실행&quot; &amp;rarr; eth0이라 불일치, 통과
    ├─ .link 처리: &quot;nic0으로 개명&quot; 결정
[처리 끝]      이름 = nic0        &amp;larr; 이제야 nic0인데, 이벤트는 끝났다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개명됐다고 add 이벤트가 다시 오지도 않는다 &amp;mdash; 접수는 한 번이다. 그래서 이 규칙은 문법이 맞았을 뿐&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단 한 번도 실행된 적이 없다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;hang이 없던 나흘은 규칙이 지켜준 게 아니라, 손으로 끈 라이브 상태가 재부팅 전까지 살아 있었던 것뿐이다. &quot;적용했다&quot;와 &quot;재부팅을 견딘다&quot;는 서로 다른 검증인데, 끈 날에는 앞엣것만 하고 뒤엣것을 했다고 믿었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;교체 &amp;mdash; 이름이 확정된 뒤에 도는 층으로&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 명령을, 실행이 보장되는 자리로 옮긴다. Proxmox가 네트워크를 올릴 때 읽는 설정(/etc/network/interfaces)의 해당 인터페이스 구절에 건다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;iface nic0 inet manual
        post-up /sbin/ethtool -K nic0 tso off gso off gro off&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;post-up은 &quot;이 인터페이스를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;올린 직후&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;실행&quot;이다. 네트워크를 올리는 것은 장치 접수&amp;middot;개명이 모두 끝난 뒤의 일이라, 그 시점엔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;nic0이라는 이름이 확정&lt;/b&gt;돼 있다. 같은 ethtool 한 줄인데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어느 층의 어느 시점에 거느냐&lt;/b&gt;가 실행 여부를 갈랐다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번엔 심고 끝내지 않고 실행되는 것까지 확인한다 &amp;mdash; 재부팅 없이 설정 재적용으로 태워 본다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;root@pve:~# ifreload -a
root@pve:~# ethtool -k nic0 | grep -E &quot;tcp-segmentation|generic-segmentation|generic-receive&quot;
tcp-segmentation-offload: off
generic-segmentation-offload: off
generic-receive-offload: off&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;안 먹는 게 실증된 udev 규칙은 지운다(rm /etc/udev/rules.d/70-nic0-offload.rules)&lt;/b&gt; &amp;mdash; 남겨두면 다음에 또 &quot;돼 있겠지&quot;로 속는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;남은 검증이 하나 있다 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실제 재부팅 통과.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이번 일의 교훈이 정확히 &quot;같겠지를 검증으로 치지 마라&quot;이므로, 다음에 호스트를 재부팅할 일이 있을 때 이 세 값이 off로 올라오는지 확인하는 것까지가 마무리다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 후속에서 남는 것:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;검증에는 층위가 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;지금 적용됐다&quot; &amp;rarr; &quot;올라올 때마다 적용된다&quot;(ifreload) &amp;rarr; &quot;재부팅을 넘는다&quot;(실부팅). 셋은 각각 따로 확인해야 하는 다른 사실이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건이 맞는 설정과 실행된 설정은 다르다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정을 넣었으면 실행된 흔적(여기서는 부팅 직후의 ethtool 상태)까지 봐야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이름에도 층이 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;eth0 &amp;rarr; eno0 &amp;rarr; nic0&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 한 장치가 부팅 중 이름을 두 번 갈아입고, 어느 층의 이름으로 조건을 걸었느냐가 규칙의 생사를 갈랐다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>HomeLab</category>
      <category>Detected Hardware Unit Hang</category>
      <author>hsb_02</author>
      <guid isPermaLink="true">https://zed6740.tistory.com/216</guid>
      <comments>https://zed6740.tistory.com/216#entry216comment</comments>
      <pubDate>Wed, 29 Jul 2026 17:08:12 +0900</pubDate>
    </item>
  </channel>
</rss>