<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>도치만의 기억 아카이브</title>
    <link>https://doccimann-memory-archive.tistory.com/</link>
    <description>무궁무진한 시행착오, 그리고 무궁무진한 성장</description>
    <language>ko</language>
    <pubDate>Thu, 6 Aug 2026 06:21:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Brian, Doccimann</managingEditor>
    <image>
      <title>도치만의 기억 아카이브</title>
      <url>https://tistory1.daumcdn.net/tistory/5902869/attach/f72ce12eee4e4a4b9139f06a02f23aea</url>
      <link>https://doccimann-memory-archive.tistory.com</link>
    </image>
    <item>
      <title>서비스 장애 잘 이해하고 대비하기</title>
      <link>https://doccimann-memory-archive.tistory.com/16</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;글을 작성하기에 앞서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 3주 전에 한빛세미나에서 주최한 &lt;b&gt;서비스 장애 잘 이해하고 대비하기&lt;/b&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;글의 순서는 아래와 같은 순서로 전개할 예정인데, 세미나의 순서와 아예 동일하지만 중간중간에 제 사견도 섞어서 말씀드리도록 하겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어디까지 장애라고 볼 수 있을까?&lt;/li&gt;
&lt;li&gt;장애는 어떻게 잘 대응할 수 있을까?&lt;/li&gt;
&lt;li&gt;장애를 예방할 수는 있을까?&lt;/li&gt;
&lt;li&gt;장애 대응의 두 가지 사례 소개&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어디까지 장애라고 볼 수 있을까? (장애의 정의)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 장애가 무엇인지 정의하기에 앞서서, 장애에는 어떠한 구성요소가 존재하는지부터 알아볼 필요가 있습니다. 장애에는 아래의 두 가지 구성요소가 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;민감도 (범위)&lt;/li&gt;
&lt;li&gt;심각도&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;민감도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감도에는 장애를 경험하는 &lt;b&gt;사용자의 범위&lt;/b&gt;, 그리고 장애가 발생한 &lt;b&gt;기능의 범위&lt;/b&gt;로 구성이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 사용자 범위는, &lt;b&gt;장애/오류를 경험하는 사용자의 수 또는 범위&lt;/b&gt;를 의미하고, 기능 범위는 &lt;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;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;A라는 서비스는 구독 회원에 한해서 특정 상품군을 우선 노출해주는 기능을 제공하고 있는데, 오류로 인해서 특정 상품군이 없다고 뜨는 현상이 발생하였습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두번째 예시&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;B라는 서비스는 신규 기능 런칭 기념으로 선착순 이벤트를 진행하였는데, 이벤트 개시가 일어나자마자 예측하지 못할 정도의 트래픽이 발생하여 해당 이벤트로 랜딩할 때 호출하는 API에 Gateway Timeout이 발생하였고, 해당 서버의 CPU 사용률이 100%를 찍으면서 해당 서버를 경유하는 모든 기능들에 Gateway Timeout이 발생하였습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 예시로 든 장애의 경우, 구독 회원에 한정해서 특정 기능만 장애가 발생하였다고 볼 수 있고, 두번째 예시의 경우 사용자 범위는 고객 전체이고, 서비스 전면 장애라고 볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;심각도&lt;/h4&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;손해액, 배상액, 시간, 그리고 LTV (고객 생애 주기)&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;h3 data-ke-size=&quot;size23&quot;&gt;장애는 어떻게 잘 대응할 수 있을까?&lt;/h3&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;h4 data-ke-size=&quot;size20&quot;&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;장애가 고객의 생애주기에 영향을 미치거나 물질적 피해를 야기할 가능성이 존재한다고 판단되는 경우, 차라리 장애가 발생하였다는 사실을 고객이 먼저 감자하기 전에 알리는 것이 물질적 피해를 줄이는 하나의 방법이 될 수도 있기 때문입니다.&lt;/p&gt;
&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;하여야 하는데요, 장애의 대처는 비단 개발팀 내에서 뿐만이 아니라 연관된 CS파트 라던지, 혹은 마케팅 부서에서도 대비를 할 필요가 있기 때문입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;빠르게 복구&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 장애가 발생하면 완벽하게 복구를 목표로하는 것이 아니라, 우선 &lt;b&gt;임시방편으로라도 고객이 장애를 인지하지 못하게 한다거나, 서비스의 다운이 발생하였다면 DownTime을 어떻게든 최대한 줄이는 것이 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;가령, API 서버의 증설 속도가 고객이 유입되는 속도를 따라잡지 못한다면, desired state를 강제로라도 늘려서 배포를 하는 것이 방법이 될 수도 있을 것이고, 미리 준비된 스탠바이 서버를 미리 배포를 해주는 것 또한 방법이 될 수 있을겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실제로도 서비스가 K8S 환경에서 운영이 된다고 가정한다면, 파드 개수를 늘리는데 있어서 기존에 떠있는 워커노드의 용량을 초과해서 Pod를 스케일아웃 해야하는 상황이 도래한다면, 아무리 워커노드에 오토스케일러를 적용하였다 할지라도 워커노드 자체가 뜨는데도 시간이 필요하기 때문에 미리 busybox를 이용해서 장애를 대비한 standby worker node를 확보해두는 것 또한 장애를 대비하는 하나의 방법이 될 수도 있을겁니다. (제 경험상 워커노드가 새로 하나 뜨려면 4~5분 정도는 필요했던 것 같습니다)&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;회고란, 단순하게 인과관계만을 분석하는 것이 아니라, 상세한 기술적인 원인을 파악하고, 최대한 low level까지 파고들어서 분석&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;h4 data-ke-size=&quot;size20&quot;&gt;일반적인 장애 대응 프로세스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용까지를 정리하자면, 일반적으로 서비스의 장애가 발생한다면 아래의 프로세스를 따라서 진행될겁니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;감지 &amp;rarr; 전파(팀 내에 온콜을 한다거나, 또는 유관부서에 전파) &amp;rarr; 판정(시니어 레벨에서 해당 장애의 심각도와 영향도를 분석) &amp;rarr; 복구(일단 임시적인 조치부터 진행한다) &amp;rarr; 분석(최대한 근거를 가진 상태로 low level까지 분석) &amp;rarr; 공유 &amp;rarr; 회고&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시스템의 가시성 (Observability)&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;명확한 근거를 수집할 수 없을 가능성이 높다&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;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;blockquote data-ke-style=&quot;style3&quot;&gt;시스템 -&amp;gt; 모니터링(APM, ELK, Prometheus) -&amp;gt; 전파 시스템(온콜, 슬랙, etc...)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  어플리케이션의 로그를 쌓는 것도 매우 중요하지만, 애플리케이션의 로그는 그저 시계열 데이터이기 때문에 시간에 따라 흘러갈 뿐입니다. 따라서 로그만 쌓는 것 보다는, &lt;b&gt;이슈트래커를 도입하여 특정 에러가 발생하면 어떠한 로그를 가지고 발생하였는지, 또는 연관된 다른 지표를 관측할 수 있는 시스템&lt;/b&gt;을 갖춰야합니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;최근에 발생한 코드의 변화 또는 배포를 추적&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;가 필요합니다. (예를 들어서, 해당 장애를 발생시킬 가능성이 존재하는 요소 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 또한 시스템의 전면 장애가 발생하였을 때 시니어 혹은 회사 내에서 제일 오래 근무하였던 동료에게 찾아가서 도움을 구했던 적이 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애를 예방할 수는 있을까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 장애를 예방한다는 것은, 장애가 일어나지 않도록 안정적인 시스템을 구축한다는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;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;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;시스템 지표에는 크게 가용성에 대한 지표(Usage), 그리고 응답 성능에 대한 지표(Latency), 그리고 오류 지표 가 존재할겁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가용성에 대한 지표: Connection의 개수, CPU, MEM, POD 개수, Pool, 스레드 개수 등..&lt;/li&gt;
&lt;li&gt;응답 성능에 대한 지표: Write/Read IO, Network Latency, Application Latency, Slow Query 등..&lt;/li&gt;
&lt;li&gt;오류 지표: Error Rate (5xx), DeadLock, Cache Lock, Exception Rate (4xx가 얼마나 자주 발생하는가)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 응답 성능에 대한 지표의 경우, Latency들이 다소 포함이 되어있는데, 평균 응답 성능보다는 상위 5%의 레이턴시의 측정치를 주요 지표로 삼는 것을 일반적으로 추천드리고, 가용성 지표또한 평균치보다는 최대치를 주요 측정치로 가져가는 것을 추천하는 바입니다. (&lt;b&gt;해당 값들이 최대치를 찍게되면 일반적으로 클라이언트는 클레임을 제기하게 되어있습니다&lt;/b&gt;)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;연사님이 소개해주신 몇 가지 요소들만 간략하게 요약하고 넘어가겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추적 가능한 배포 프로세스: 배포 기능의 명확한 추적 및 릴리즈 노트 작성을 해야 장애 파악에 용이해진다&lt;/li&gt;
&lt;li&gt;적절한 테스트 코드, 코드 품질을 유지하는 개발 문화: 복잡한 기능 배포의 경우 TDD를 지향하는 조직이 아니라고 할지라도 테스트코드를 써야만 개발 의도가 공유되기 때문에 적절한 테스트 코드는 필요하다고 생각합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;High-Tolerance 가 보장된 시스템의 설계&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스의 실패: 데이터베이스의 실패를 대비한 데이터베이스 이중화 (일반적으로 Master/Replication이 이에 해당합니다)&lt;/li&gt;
&lt;li&gt;서버 간 통신의 실패: fallback 메소드 구현 (예를 들어 WebSocket 서버가 실패한다면 API Polling을 하더라도 시스템을 견고하게 운영할 수 있어야한다는 뜻입니다)&lt;/li&gt;
&lt;li&gt;급격한 트래픽의 대응 실패: &lt;b&gt;파킹 서버, 서킷 브레이커 마련 등이 방법이 될수는 있음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;배포 실패: 빠른 롤백, 블루그린 및 카나리 배포&lt;/li&gt;
&lt;li&gt;인프라 실패: 각 인프라의 이중화, 임시 백업 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글 마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 또한 한빛미디어 세미나를 다녀오면서 많은 깨달음을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;p data-ke-size=&quot;size16&quot;&gt;그런 저이기에 해당 세미나는 저에게 크게 와닿았는데요, 그렇기에 해당 세미나를 듣고나서 사내에 해당 세미나 내용을 공유해드렸으며, 많은 동료분들이 이에 공감하여 시스템의 가시성 확보를 위한 여정에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 깨달음을 얻게된다면 블로그 포스팅을 통해서 공유드리도록 하겠습니다.&lt;/p&gt;
&lt;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;</description>
      <category>devOps</category>
      <category>DevOps</category>
      <category>세미나</category>
      <category>장애</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/16</guid>
      <comments>https://doccimann-memory-archive.tistory.com/16#entry16comment</comments>
      <pubDate>Sat, 6 Jul 2024 21:23:54 +0900</pubDate>
    </item>
    <item>
      <title>Redis OOM 장애</title>
      <link>https://doccimann-memory-archive.tistory.com/15</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;거의 1년만에 아티클을 작성하게 되었는데, 오랜만에 작성하게된 아티클의 주제는 제가 직접 겪어본 &lt;b&gt;Redis OOM&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;Redis OOM이 일어나게 된 배경&lt;/li&gt;
&lt;li&gt;Redis 아키텍처 돌아보기&lt;/li&gt;
&lt;li&gt;단기적인 장애 대응 방법&lt;/li&gt;
&lt;li&gt;최종적인 장애 대응 방법, 그리고 회고&lt;/li&gt;
&lt;li&gt;글 마무리&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis OOM 장애가 일어나게 된 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 운영하던 백엔드의 API 일부 중에는 Redis를 적극 활용하여 대용량 데이터를 프로세싱하여 클라이언트에게 추천 데이터를 반환해주는 로직이 하나 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 API 서버는 ECS 환경에서 운영되어 트래픽이 늘어나거나, 혹은 CPU, MEM 사용량을 관측하여 일정 임계치를 넘어가면 태스크의 개수를 스케일 아웃하도록 설정이 되어있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 트래픽이 늘어나더라도 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;그러나.... 갑자기 어느날 오후 10시 40분 정도부터 아래의 에러가 레포트로 날아오기 시작했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;OOM command not allowed when used memory than 'maxmemory'&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #232f3e; text-align: start;&quot;&gt;해당 레포트가 날아오고, Redis를 사용하는 모든 API에 장애가 발생하기 시작하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #232f3e; text-align: start;&quot;&gt;장애가 발생하였을 때에는 머리가 새하얘지면서, 아래의 생각만이 들었습니다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #232f3e; text-align: start;&quot;&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;Redis에 캐시 추가 SET을 할 일이 별로 없는데 도대체 왜...?&lt;/li&gt;
&lt;li&gt;누가 Redis에 고의적으로 이상한 커맨드를 날린거 아니야?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;color: #232f3e;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;결론적으로 말씀드리면, 위의 두가지는 실제 장애의 원인과는 거리가 먼 상상들이었고, 실제 Redis의 장애 원인은 Redis에 들어간 과도하고, 그리고 하나하나가 헤비한 요청들이 쌓여서 문제를 일으켰던 것이었습니다. 정확하게 말씀드리면&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;API 서버에 트래픽이 몰리면서 서버의 응답 자체가 많이 느려졌다.&lt;/li&gt;
&lt;li&gt;Redis와 커넥션을 맺은 클라이언트의 성능이 낮아지다보니, &lt;b&gt;클라이언트가 Redis로부터 데이터를 수신받는데 지연이 발생하기 시작&lt;/b&gt;하였다.&lt;/li&gt;
&lt;li&gt;추천 데이터 프로세싱을 위한 Redis 데이터 하나하나의 크기는 어느 정도 큰 편인데 (매우 큰 정도는 또 아님), &lt;b&gt;해당 데이터가 수신 버퍼에 쌓이고 쌓여 OOM을 일으켰다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;color: #232f3e;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;이런 순서가 될 것 같습니다. 천천히 설명드리도록 하겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #232f3e;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Redis 아키텍처 간단하게 돌아보기&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: #232f3e;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Redis가 데이터를 프로세싱 하는 구조를 돌아보기 보다는, 데이터가 Redis 내부에서 프로세싱된 이후의 절차를 위주로 돌아보려고합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #232f3e;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Redis는 클라이언트와 커넥션을 맺게되면 커넥션마다 네트워크 송수신의 성능을 향상시키기 위해 &lt;b&gt;송수신 버퍼&lt;/b&gt;라는 것을 두게됩니다.&lt;/span&gt;&lt;/span&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;그리고 Redis는 클라이언트로부터 GET 요청 등의 조회 요청이 날아오면 아래의 절차를 밟아서 클라이언트에게 데이터를 송신합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis가 Key를 통해서 내부의 테이블로부터 데이터를 가져옵니다.&lt;/li&gt;
&lt;li&gt;Redis는 응답 데이터를 준비하여 &lt;b&gt;Connection에 물려있는 송신 버퍼에 데이터를 저장&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;Redis는 네트워크를 통해서 클라이언트에게 송신 버퍼에 있는 데이터를 전송합니다.&lt;/li&gt;
&lt;li&gt;Redis는 클라이언트가 송신 버퍼의 데이터를 수신 받게되면 송신 버퍼로부터 해당 데이터를 제거합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 두번째 절차에 집중할 필요가 있어보입니다. 흔히 Redis에 대해서 생각한다면, 데이터가 저장되는 영역만을 생각하기 쉬운데, 사실 &lt;b&gt;클라이언트에게 데이터를 빠르게 전송하기 위한 클라이언트 버퍼 영역도 생각을 해봐야한다&lt;/b&gt;는 것입니다. (Redis는 HA를 위해서 모든 데이터 공간을 Memory로 처리하기 떄문에, 클라이언트 버퍼 또한 Memory를 할당하여 데이터를 보관합니다)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단기적인 장애 대응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애 대응에는 &lt;b&gt;단기적인 장애 대응&lt;/b&gt;과 &lt;b&gt;장기적인 장애 대응&lt;/b&gt; 이렇게 두 가지로 나뉩니다. 우선 장애가 발생하였다고 인지된 시점부터는 고객이 느끼는 해당 서비스의 DownTIme을 어떻게든 줄이는 것이 고객 경험 및 물리적 손해를 최대한 줄일 수 있기 때문에, 우선 단기적인 장애 대응부터 하는것이 먼저일 것입니다.&lt;/p&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에 OOM이 발생하였구나 정도만 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서비스 다운타임을 최대한 줄이기 위해 레디스 노드들의 스펙을 스케일업하고, 클라이언트의 응답 속도를 어떻게든 향상시켜야했기 때문에 ECS 테스크들의 스펙들 또한 스케일업을 하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장기적인 장애 대응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방법을 통해서 하루 정도의 시간은 벌게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단기적으로 장애 대응을 하였다고는 하지만, 해당 내용이 완벽한 장애 대응이라고 볼 수는 없을 것입니다.&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;원인은 정말 명확했습니다. 일단, Redis로부터 클라이언트로 송신하는 데이터가 최적화 되지 않은 상태였기에 하나하나의 데이터 크기가 비교적 큰 편이기도 했고, 또한 해당 추천 데이터를 호출하는 빈도가 너무 잦았던 것도 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 찾았기 때문에, 해당 문제점을 해결할 때 까지 서비스를 그 이전 시점으로 롤백하였고, API를 더 최적화 한 다음에 재배포 하여 문제를 해결하게 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 장애가 제가 겪어본 서비스 장애 중에 다운타임이 제일 길었던 것 같습니다. 다운타임이 긴 만큼 그 당시의 제 마음또한 엄청 초조하였었는데요, 해당 장애를 겪으면서 &lt;b&gt;시스템의 가시성(Observability)&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;저는 Redis가 알아서 잘 동작해주겠지 라는 마인드로 안일하게 Redis는 굳이 모니터링을 하지 않았었는데요, 해당 장애를 기점으로 Redis또한 모니터링의 대상이 되어야함을 깨달았고, Redis의 어떤 지표를 모니터링 해야하는지를 고민하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 장애에서 제일 아쉬웠던 것은, ElastiCache에는 네트워크 송수신 네트워크의 크기가 시계열성으로 얼마나 되는지를 보여주는 지표가 존재하였는데요, 해당 지표를 일찍 관측하였더라면 어땠을까라는 생각도 해보게 되었습니다.&lt;/p&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를 운영하면서 관측해야할 지표는 아래와 같을겁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 가용 메모리 크기&lt;/li&gt;
&lt;li&gt;Redis 송수신 네트워크 크기&lt;/li&gt;
&lt;li&gt;Redis 송수신 네트워크 패킷 개수&lt;/li&gt;
&lt;li&gt;Redis 클라이언트 Max connections&lt;/li&gt;
&lt;li&gt;Redis Read/Write Operations 개수&lt;/li&gt;
&lt;li&gt;Redis CPU 사용률&lt;/li&gt;
&lt;li&gt;Redis 메모리 단편화 비율&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글 마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성하다보니 글이 조금 길어졌습니다. 이 글을 보시는 여러분들도 저와 같은 장애를 겪지 않도록, 저의 글이 여러분에게 장애를 예방하는 계기가 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;Redis Headaches for DevOps Client Buffers - Redis 공식문서&quot; href=&quot;https://redis.io/blog/top-redis-headaches-for-devops-client-buffers/&quot; rel=&quot;noopener&quot;&gt;https://redis.io/blog/top-redis-headaches-for-devops-client-buffers/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719326940115&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Top Redis Headaches for Devops - Client Buffers | Redis&quot; data-og-description=&quot;Client buffers are an often overlooked aspect of Redis' memory requirements and management. Let's take a closer look at common roadblocks and tips.&quot; data-og-host=&quot;redis.io&quot; data-og-source-url=&quot;https://redis.io/blog/top-redis-headaches-for-devops-client-buffers/&quot; data-og-url=&quot;https://redis.io/blog/top-redis-headaches-for-devops-client-buffers/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c4AGCJ/hyWrVAiys1/bAc15dc4AVNseA4I1wc1W1/img.png?width=920&amp;amp;height=612&amp;amp;face=0_0_920_612&quot;&gt;&lt;a href=&quot;https://redis.io/blog/top-redis-headaches-for-devops-client-buffers/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redis.io/blog/top-redis-headaches-for-devops-client-buffers/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c4AGCJ/hyWrVAiys1/bAc15dc4AVNseA4I1wc1W1/img.png?width=920&amp;amp;height=612&amp;amp;face=0_0_920_612');&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;Top Redis Headaches for Devops - Client Buffers | Redis&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Client buffers are an often overlooked aspect of Redis' memory requirements and management. Let's take a closer look at common roadblocks and tips.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redis.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;Redis 클라이언트 핸들링&quot; href=&quot;https://redis.io/docs/latest/develop/reference/clients/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://redis.io/docs/latest/develop/reference/clients/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719326975646&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Redis client handling&quot; data-og-description=&quot;How the Redis server manages client connections&quot; data-og-host=&quot;redis.io&quot; data-og-source-url=&quot;https://redis.io/docs/latest/develop/reference/clients/&quot; data-og-url=&quot;https://redis.io/docs/latest/develop/reference/clients/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/reference/clients/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redis.io/docs/latest/develop/reference/clients/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Redis client handling&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;How the Redis server manages client connections&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redis.io&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;</description>
      <category>devOps</category>
      <category>DevOps</category>
      <category>Redis</category>
      <category>장애</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/15</guid>
      <comments>https://doccimann-memory-archive.tistory.com/15#entry15comment</comments>
      <pubDate>Tue, 25 Jun 2024 23:52:52 +0900</pubDate>
    </item>
    <item>
      <title>Docker File System (Overlay2)</title>
      <link>https://doccimann-memory-archive.tistory.com/14</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;얼마전 회사에서 트러블슈팅을 진행하면서 docker exec 명령어를 이용해서 container의 bash쉘을 연 다음에, 바로 컨테이너 내부의 파일을 변경한 적이 있었습니다. 그런데 저는 여기서 의아한 부분이 하나 있었습니다. &lt;b&gt;과연 Docker 컨테이너 내부의 파일을 저렇게 바로 변경해도 되는가?&lt;/b&gt; 였습니다. 이에 대해서 공부를 하던 중에, Ubuntu의 docker는 기본적인 파일시스템으로 Overlay2를 채택하고 있었고, 또한 Overlay2 스토리지 시스템의 특징으로 인해서 컨테이너 내부의 파일을 바로 변경한다고 해서, 근간이 되는 image의 레이어가 훼손되지는 않는다는 것을 알게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Overlay2 스토리지 시스템에 대해서 공부한 내용들을 이 포스트를 통해서 공유드려볼까 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 먼저 도커 이미지의 레이어부터 알아봅시다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 nginx를 docker registry로부터 다운받아보는걸로 시작하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1691307423462&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker pull nginx:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래의 inspect 명령어를 입력해보면 아래의 결과를 얻어볼 수 있을 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1691307862091&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker inspect --format='{{json .RootFS.Layers}}' nginx:latest
[
  &quot;sha256:8450f74cd36b8dee83cbb893350e167acb792b321fad0c2180abfd1ef0fcdf55&quot;,
  &quot;sha256:b0ff37385ae938a3bcb5241789f33ddf9e3841cdbb16e51c9f5bf12d556899ab&quot;,
  &quot;sha256:1e451eaa30b322397195118e9c7e2fe22ae01e463d21afe36be988cb82ccd001&quot;,
  &quot;sha256:3c6148273d487a83886a7412441cbd39d142a7ab15b223a5f86cf5753efc53e2&quot;,
  &quot;sha256:482f7b7b1b0385901b7dd61fe06c63a9b8218725edcac24bb19c8eb82c01e495&quot;,
  &quot;sha256:83cb32b689ff60bf5aa47efdd1714c27f0ba4a160524f8e110725ea43461ae1e&quot;,
  &quot;sha256:facf79a780de4bb891d6a7beeb8e160d6201e46703cf7ddcc1ed5edd0760a7e8&quot;
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 커맨드가 시사하는 바는, 도커 이미지는 여러개의 레이어를 조합한 구조로 구성되어있다는 것입니다. 그리고 또한 이러한 레이어들로 구성된 이미지가 도커 명령어를 통해서 dockerd에 컨테이너로 올라가면, 해당 레이어들을 이용하여 컨테이너를 구성한다는 뜻이 되기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이러한 layer들은 모두 /var/lib/docker/overylay2 라는 폴더 내부에 저장이 되는데요, 이제 overlay2 폴더에 대해서 탐구해보는 시간을 가져보도록합시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Union FS에 대해서 알아봅시다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Union FS를 설명하기 이전에 하나의 재밌는 예시를 들어보고 시작하겠습니다.&lt;/p&gt;
&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;첫번째로 mysql + ubuntu 로 결합된 이미지를 하나 컨테이너로 올린다고 합시다. 그리고 첫번째 이미지를 컨테이너로 올린 다음에, mysql + ubuntu + nginx 로 구성된 이미지를 하나 컨테이너로 올린다고 가정해봅시다. 그러면 첫번째 컨테이너는 mysql, ubuntu에 존재하는 layer들을 컨테이너 내부에 가지고 있어야하고, 두번째 컨테이너는 mysql, ubuntu, nginx에 존재하는 layer들을 컨테이너 내부에 가지고 있어야할 것입니다.&lt;/p&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, ubuntu 가 가지고 있는 레이어들이 중복됨에도 불구하고 별개로 관리를 해야하기 때문에 용량 낭비가 발생할 것입니다.&lt;/p&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, ubuntu에서 사용하게되는 layer들이 공통으로 관리되기 때문에 용량이 낭비되는 일은 줄어들것으로 예상이됩니다. 이게 union FS를 설명하기 위한 첫걸음입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Union FS는 &lt;b&gt;상속파일시스템&lt;/b&gt; 으로, 여러개의 파일시스템을 여러개를 쌓고, 밑에서부터 레이어를 쌓아서 새로운 하나의 파일시스템을 구성하는 방식으로 동작합니다. 그러면 이제 Union FS의 특징에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앞서 설명드렸듯이, 여러개의 파일 시스템을 하나로 합쳐서 마운트시킵니다.&lt;/li&gt;
&lt;li&gt;레이어가 쌓이는 순서도 중요합니다. 만일 쓰기가 발생하여 파일이 수정되는 경우, 전 파일과 이후 파일의 이름은 동일하기 때문에 어느게 덮어쓰여지는지가 중요하기 때문입니다.&lt;/li&gt;
&lt;li&gt;유니온 파일시스템은 CoW(Copy on Write), RoW(Redirect on Write) 방식 중에서 CoW를 채택합니다. 즉, 읽기 전용 Lower layer에 대해서 쓰기 명령이 발생 시에 스냅샷을 발행하여 관리합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 CD-ROM 과 HDD의 관계를 비유해보면 매우 쉽습니다. CD-ROM은 한번 구워지는 순간 모든 내용물이 read-only로 관리가 됩니다. 그리고 CD를 컴퓨터에 넣고 CD의 내용물에 수정이 필요한 상황이 발생했다고 가정을 해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD의 모든 내용물은 read-only이기 때문에 수정이 불가능합니다. 그렇기 때문에 대부분의 사람들은 HDD에 CD의 내용을 복제한 다음에, 해당 복제본을 HDD에서 수정하여 사용을 하게 될것입니다.&lt;/p&gt;
&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;CD의 내용물을 Lower layer, HDD를 Upper layer&lt;/b&gt;라고 생각하면 Overlay FS에 대해서 이해하기 쉬워질거라 예상합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 이제 Overlay2 FS에 대해서 알아보면 될것같네요!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은, Union FS의 구현체 중의 하나가 Overlay2 FS입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 저희가 pull 받은 nginx를 먼저 컨테이너로 띄우고, 해당 컨테이너를 inspect 명령어를 이용해서 내부를 분석해보도록합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1691310839751&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -d -p 8080:80 --name my_nginx nginx:latest
130ebbc9a31ab4c305254db974d09d5059ca805e28b8242b6d908777da7724c0

$ docker inspect --format='{{json .GraphDriver.Data}}' my_nginx
{
  &quot;LowerDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b-init/diff:/var/lib/docker/overlay2/3a8dc89d35fc702bb8e734e29b4021fb747e0acf2676b7dcb17150b009307db4/diff:/var/lib/docker/overlay2/31fa1902467ed3d2b5df30df8f2470c3f4490626864cad96c8d713257e4cd5e0/diff:/var/lib/docker/overlay2/b0615c3cce8ef25a67ed47591da92d01b82b82474ac882597f8ba6318de01492/diff:/var/lib/docker/overlay2/78d6b85b181259f3d2e192c3ab3697ff94e4b6aaa324c7955a86596d18105c96/diff:/var/lib/docker/overlay2/9e88426594e2d1f283e16c49057e6d8c985ba56b9bb42dde2c52f916e371a7c1/diff:/var/lib/docker/overlay2/766857ac768b4b06fe325f72eea091a6ff86bd047561b9152ee98cea0e4b878a/diff:/var/lib/docker/overlay2/0fce8340c6fd9f3a79f1966cdb5b31cd0f4087019ed786cd0c3e0478fbce8cdb/diff&quot;,
  &quot;MergedDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b/merged&quot;,
  &quot;UpperDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b/diff&quot;,
  &quot;WorkDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b/work&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 LowerDir, UpperDir, MergedDir에 집중을 하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LowerDIr은 이미지가 가지고있는 레이어에 대한 정보로, 이는 read-only(읽기 전용)으로 관리가 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 컨테이너를 운영하다보면, Lower layer에 존재하는 파일을 수정할 일이 한번 생길수도 있는데요(&lt;b&gt;호스트에서 삭제하는 경우는 생각하지 않기로 합니다&lt;/b&gt;), Lower layer의 파일은 read-only로 운영되며, Overlay2 스토리지 시스템은 CoW로 동작하기 때문에 아래의 방식으로 파일 수정이 이루어집니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 Lower layer에 있는 원본 파일을 복제하여 Upper layer에 싣습니다.&lt;/li&gt;
&lt;li&gt;Upper layer에 적재된 파일을 수정하여 보관합니다. (&lt;b&gt;시작하세요! 도커/쿠버네티스 서적에는 overlayfile 이라는 파일을 이용해서 파일의 수정내역을 보관한다고 하지만, 실제 docker 공식문서를 확인해보면 overlayfile에 대한 언급은 존재하지 않습니다. 참고 부탁드립니다.&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;그리고 Lower layer, Upper layer 두 개의 파일시스템을 합쳐서 마운트하는 방식으로 Merged layer를 구성합니다. 그 과정에서 &lt;b&gt;수정된 파일은 Upper layer의 파일로 덮어씌우는&lt;/b&gt; 과정을 거치고, Merged layer를 실제 container가 사용하게됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Docker가 Overlay2를 이용해서 용량 절약을 이뤄내는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈치가 빠르신 분들은 알아채실 수 있지만, Docker는 overlay2 라는 폴더에 lower-layer에 속하는 레이어 파일들을 여러 컨테이너가 공유하는 방식으로 용량 절약을 이뤄냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 지금까지는 container level만 다뤘는데요, image level에 대해서도 설명을 일부 드려볼까합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 도커 이미지를 다운받아서 inspect 명령어를 통해 분석해보면, 도커 이미지는 여러개의 layer의 조합으로 구성된다고 알려드린 바 있습니다. 그러면 image에 소속된 layer들은 모두 어디에 존재하는걸까요?&lt;/p&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는 이미지에 소속된 layer들을 로컬에 보관하기 위해서 &lt;b&gt;LayerDB&lt;/b&gt; 를 사용하고 있습니다. 해당 디렉토리를 확인하기 위해선 아래의 커맨드를 입력해보면 조회가 될것입니다. (overlay2 FS를 사용한다는 가정 하에)&lt;/p&gt;
&lt;pre id=&quot;code_1691312088237&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ls /var/lib/docker/image/overlay2/layerdb/sha256&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 sha256 폴더 내부에 레이어 정보들이 담긴 여러개의 디렉토리들이 발견되는 모습을 보이는데요, 각 디렉토리에는 cache-id, parent, diff 라는 파일이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 cache-id, parent, diff는 아래를 의미한다고 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;cache-id: 레이어가 저장된 로컬 경로의 id&lt;/li&gt;
&lt;li&gt;parent: 부모 레이어를 가리키는 포인터로, 해당 레이어가 어느 레이어 위로 쌓여야할지 순서를 결정해주는 파일입니다. (&lt;b&gt;docker image또한 inspect로 분석해보면 순서가 존재한다는 것을 알 수 있었을겁니다!&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;diff: 레이어의 식별자입니다. 해당 정보를 통해서 container의 Lower layer에 레이어가 들어갑니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1691312351392&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker inspect --format='{{json .GraphDriver.Data}}' my_nginx
{
  &quot;LowerDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b-init/diff:/var/lib/docker/overlay2/3a8dc89d35fc702bb8e734e29b4021fb747e0acf2676b7dcb17150b009307db4/diff:/var/lib/docker/overlay2/31fa1902467ed3d2b5df30df8f2470c3f4490626864cad96c8d713257e4cd5e0/diff:/var/lib/docker/overlay2/b0615c3cce8ef25a67ed47591da92d01b82b82474ac882597f8ba6318de01492/diff:/var/lib/docker/overlay2/78d6b85b181259f3d2e192c3ab3697ff94e4b6aaa324c7955a86596d18105c96/diff:/var/lib/docker/overlay2/9e88426594e2d1f283e16c49057e6d8c985ba56b9bb42dde2c52f916e371a7c1/diff:/var/lib/docker/overlay2/766857ac768b4b06fe325f72eea091a6ff86bd047561b9152ee98cea0e4b878a/diff:/var/lib/docker/overlay2/0fce8340c6fd9f3a79f1966cdb5b31cd0f4087019ed786cd0c3e0478fbce8cdb/diff&quot;,
  &quot;MergedDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b/merged&quot;,
  &quot;UpperDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b/diff&quot;,
  &quot;WorkDir&quot;: &quot;/var/lib/docker/overlay2/a8b3396ddd52e5caf32f387cd6f177e371310761b70b61bc73577ddcd29f122b/work&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 LowerDir 키를 보시게 되면, 로컬의 diff 라는 파일이 모두 실리는 것을 확인할 수 있습니다.&lt;/p&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는 image의 레이어들과, container의 레이어들을 관리하고 있으며, 이러한 파일시스템 구조를 통해서 용량을 절약하면서 docker engine을 돌릴 수 있다는 것을 확인해볼 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. References (참고 문서)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/storage/storagedriver/overlayfs-driver/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;도커 공식문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001766450&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;시작하세요! 도커/쿠버네티스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.kakaoenterprise.com/171&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[컨테이너 인터널 #2] 컨테이너 파일시스템 - Kakao Enterprise&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;figure id=&quot;og_1691312782078&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;Use the OverlayFS storage driver&quot; data-og-description=&quot; &quot; data-og-host=&quot;docs.docker.com&quot; data-og-source-url=&quot;https://docs.docker.com/storage/storagedriver/overlayfs-driver/&quot; data-og-url=&quot;https://docs.docker.com/storage/storagedriver/overlayfs-driver/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vwX2j/hyTze3OzmR/YIvbfq1Xhrsoi6p48SiuiK/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/e7XiI/hyTx1ZlU20/n3RdXlIMZUgjDBbNKm9am0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://docs.docker.com/storage/storagedriver/overlayfs-driver/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.docker.com/storage/storagedriver/overlayfs-driver/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vwX2j/hyTze3OzmR/YIvbfq1Xhrsoi6p48SiuiK/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/e7XiI/hyTx1ZlU20/n3RdXlIMZUgjDBbNKm9am0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&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;Use the OverlayFS storage driver&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1691312779074&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;시작하세요! 도커/쿠버네티스 | 용찬호 - 교보문고&quot; data-og-description=&quot;시작하세요! 도커/쿠버네티스 | 쿠버네티스와 도커의 기본 사용 방법을 정확히 이해하는 것을 목표로 합니다!도커 컨테이너는 애플리케이션을 배포하기 위한 새로운 패러다임을 제시하는 가상&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001766450&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000001766450&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/coyRqW/hyTxWDJ0jw/Kmn7yHWIk3qeERyddp8bCK/img.jpg?width=458&amp;amp;height=578&amp;amp;face=0_0_458_578,https://scrap.kakaocdn.net/dn/guHFg/hyTziynQ8A/eLhLrBKCf4otLWoFlh8I70/img.jpg?width=458&amp;amp;height=578&amp;amp;face=0_0_458_578,https://scrap.kakaocdn.net/dn/bDQS3X/hyTxMukTVh/KoRCCZWgqxbp0XtjZKBd9k/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001766450&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001766450&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/coyRqW/hyTxWDJ0jw/Kmn7yHWIk3qeERyddp8bCK/img.jpg?width=458&amp;amp;height=578&amp;amp;face=0_0_458_578,https://scrap.kakaocdn.net/dn/guHFg/hyTziynQ8A/eLhLrBKCf4otLWoFlh8I70/img.jpg?width=458&amp;amp;height=578&amp;amp;face=0_0_458_578,https://scrap.kakaocdn.net/dn/bDQS3X/hyTxMukTVh/KoRCCZWgqxbp0XtjZKBd9k/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335');&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;시작하세요! 도커/쿠버네티스 | 용찬호 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;시작하세요! 도커/쿠버네티스 | 쿠버네티스와 도커의 기본 사용 방법을 정확히 이해하는 것을 목표로 합니다!도커 컨테이너는 애플리케이션을 배포하기 위한 새로운 패러다임을 제시하는 가상&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1691312776994&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[컨테이너 인터널 #2] 컨테이너 파일시스템&quot; data-og-description=&quot;시작하며 안녕하세요. 카카오엔터프라이즈에서 검색서비스를 개발하고 있는 검색클라우드기술파트의 Sam(김삼영)입니다. [컨테이너 인터널 #1] 컨테이너 톺아보기에서 예고해 드린 대로 이번 포&quot; data-og-host=&quot;tech.kakaoenterprise.com&quot; data-og-source-url=&quot;https://tech.kakaoenterprise.com/171&quot; data-og-url=&quot;https://tech.kakaoenterprise.com/171&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iQxm4/hyTy99hscO/lS75PJb1ZZp8wKcsV4mYY1/img.png?width=800&amp;amp;height=359&amp;amp;face=0_0_800_359,https://scrap.kakaocdn.net/dn/AiqFP/hyTzaNTBn4/gkCootDmUiXBeMRhEGq3rk/img.png?width=800&amp;amp;height=359&amp;amp;face=0_0_800_359,https://scrap.kakaocdn.net/dn/c3lCV8/hyTzevZmx2/1WDADtyNbzBkFKiVaFnZtK/img.png?width=1600&amp;amp;height=666&amp;amp;face=0_0_1600_666&quot;&gt;&lt;a href=&quot;https://tech.kakaoenterprise.com/171&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.kakaoenterprise.com/171&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iQxm4/hyTy99hscO/lS75PJb1ZZp8wKcsV4mYY1/img.png?width=800&amp;amp;height=359&amp;amp;face=0_0_800_359,https://scrap.kakaocdn.net/dn/AiqFP/hyTzaNTBn4/gkCootDmUiXBeMRhEGq3rk/img.png?width=800&amp;amp;height=359&amp;amp;face=0_0_800_359,https://scrap.kakaocdn.net/dn/c3lCV8/hyTzevZmx2/1WDADtyNbzBkFKiVaFnZtK/img.png?width=1600&amp;amp;height=666&amp;amp;face=0_0_1600_666');&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;[컨테이너 인터널 #2] 컨테이너 파일시스템&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;시작하며 안녕하세요. 카카오엔터프라이즈에서 검색서비스를 개발하고 있는 검색클라우드기술파트의 Sam(김삼영)입니다. [컨테이너 인터널 #1] 컨테이너 톺아보기에서 예고해 드린 대로 이번 포&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.kakaoenterprise.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;</description>
      <category>devOps</category>
      <category>docker</category>
      <category>도커</category>
      <category>파일시스템</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/14</guid>
      <comments>https://doccimann-memory-archive.tistory.com/14#entry14comment</comments>
      <pubDate>Sun, 6 Aug 2023 18:07:34 +0900</pubDate>
    </item>
    <item>
      <title>엘리스 3차 프로젝트 후기 (feat. 엘리스 AI트랙의 끝)</title>
      <link>https://doccimann-memory-archive.tistory.com/13</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 글을 쓰게 되었습니다. 최근에 엘리스 3차 프로젝트를 하느라 글을 못 쓰고 있던 상황이었는데, 얼마 전에 엘리스 3차 프로젝트가 끝남과 동시에 수료를 하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 이번에는 엘리스 3차 프로젝트 후기 및 수료에 대한 소감에 대해서 글을 써보려고합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 3차 프로젝트의 팀 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘리스 AI 트랙의 3차 프로젝트는 4월 15일 부터 5월 20일 까지 진행되었습니다. 팀은 4월 15일 정도에 발표가 되었는데, 정말 다행스럽게도(?) 제가 평소에 &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;저희 팀은 6인 팀으로, 프론트엔드 3명, 백엔드 3명 이렇게 구성이 되었습니다. 프론트엔드에서 한 분이 전체 팀장을 맡아주시기로 하였고, 백엔드의 경우에는 제가 백엔드 개발리드를 맡아서 진행하기로 하였습니다. 그렇게 3차 프로젝트를 향한 여정이 시작되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로젝트 기획&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자의 포지션이 나뉘고나서 프로젝트의 주제를 결정하기로 하였습니다. 저희 팀은 &lt;b&gt;자연어 처리&lt;/b&gt; 프로젝트를 진행해야했기 때문에, 사실 주제를 결정하는게 매우 힘들었습니다. 왜냐면 저희가 정해야할 주제는 크게 아래의 2가지 조건을 만족해야했기 때문입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;진행하려고 하는 주제가 데이터셋이 풍부한가? 그리고 신뢰할만한 데이터셋을 얻어낼 수 있는가?&lt;/li&gt;
&lt;li&gt;목적함수가 명확하게 떨어질 수 있는 주제인가? (AI Model의 목적이 뚜렷한가?)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코치님의 조언과 팀원 간의 논의 끝에 저희 2팀 (효상이네)는 &lt;b&gt;일기를 작성하면 야식을 추천해주는 커뮤니티 프로젝트&lt;/b&gt; 를 하기로 하였습니다. 나름 타당한 주제였던 이유가, 일기를 입력하면 일기에 담긴 감정을 분석하고, 분석된 감정을 토대로 야식을 추천해주는 시스템을 구축한다고 한다면 문장 단위의 감정 분석(sementic analysis)에 대한 데이터셋은 이미 매우 풍부하게 존재했으며, 또한 사전학습된 모델또한 존재했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희의 프로젝트 이름은 EEUM 으로 결정되었습니다. 프로젝트 이름이 결정된 이후에는 &lt;b&gt;EEUM을 만들어가는데 있어서 필요한 기능들이 무엇이 있는지&lt;/b&gt;에 대해서 팀원들과 논의를 주고받았고, 어느 정도 기능이 정해진 다음에 &lt;b&gt;크게 어떠한 기술을 사용해서 프로젝트를 구성할까?&lt;/b&gt; 에 대해서 논의를 진행하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프로젝트 기획까지는 호기롭게 진행되었지만...타스는 다들 처음인걸  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술스택을 논의하는 과정에서 팀원 6명 전원이 &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;저를 제외한 팀원 5명이 타입스크립트를 한번도 사용한 적이 없었으며, 타입스크립트 외에도 객체지향 언어를 한번도 사용한 적이 없는 사람들이었다&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;타입스크립트 경험이 있는 제가 나머지 5명의 팀원들을 대상으로 타입스크립트를 강의하기로&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주 중 첫주는 팀원들 대상으로 타입스크립트를 강의하고, 백엔드를 대상으로는 &lt;b&gt;객체지향&lt;/b&gt; 과 &lt;b&gt;Nest.js&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;2824&quot; data-origin-height=&quot;1560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6byCV/btshi7O9Qcm/IyaUvc5oDkqq0WjCF2LSt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6byCV/btshi7O9Qcm/IyaUvc5oDkqq0WjCF2LSt1/img.png&quot; data-alt=&quot;타입스크립트 발표 자료이다. 상속에 관해서 이야기를 했던 것 같다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6byCV/btshi7O9Qcm/IyaUvc5oDkqq0WjCF2LSt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6byCV%2Fbtshi7O9Qcm%2FIyaUvc5oDkqq0WjCF2LSt1%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;2824&quot; height=&quot;1560&quot; data-origin-width=&quot;2824&quot; data-origin-height=&quot;1560&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;타입스크립트 발표 자료이다. 상속에 관해서 이야기를 했던 것 같다.&lt;/figcaption&gt;
&lt;/figure&gt;
&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;프론트엔드와 백엔드 모두 Docker image를 정의한다.&lt;/li&gt;
&lt;li&gt;운영 환경을 셋팅한다. VM에 필요한 것을 모두 셋팅하거나, 혹은 빌드서버를 구성하는 등의 시스템을 구성하였다.&lt;/li&gt;
&lt;li&gt;Jenkins를 셋팅하고 Jenkins 빌드 파이프라인을 프론트엔드, 그리고 백엔드에 모두 셋팅하였다.&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;3456&quot; data-origin-height=&quot;2234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zEcFS/btshjD1ijH6/81MUBikAhYA01P8vbGvLbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zEcFS/btshjD1ijH6/81MUBikAhYA01P8vbGvLbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zEcFS/btshjD1ijH6/81MUBikAhYA01P8vbGvLbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzEcFS%2FbtshjD1ijH6%2F81MUBikAhYA01P8vbGvLbk%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;3456&quot; height=&quot;2234&quot; data-origin-width=&quot;3456&quot; data-origin-height=&quot;2234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 본격적인 코딩 시작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 팀은 둘째주가 되어서야 본격적으로 코딩을 시작하였습니다. 사실 본격적으로 코딩을 시작하기 이전에 해야할 일이 하나 있었습니다. 바로 &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;프로젝트 전반의 기능 구현에서 유스케이스를 모두 뽑아내고, 유스케이스 내부에서 feature를 모두 뽑아낸 다음 feature 단위로 기간 산정을 수행한다&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;그리고 feature에 들어가는 공수들을 모두 종합한 다음, 남은 기간을 대략 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;그리고 소거된 이후에 남은 유스케이스들을 주 단위 sprint 단위로 나눈 다음 Jira 티켓을 팀원들에게 발행하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LlsYY/btshiHXrW3c/BX7FGwQdMpu0B6vq1rEPOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LlsYY/btshiHXrW3c/BX7FGwQdMpu0B6vq1rEPOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LlsYY/btshiHXrW3c/BX7FGwQdMpu0B6vq1rEPOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLlsYY%2FbtshiHXrW3c%2FBX7FGwQdMpu0B6vq1rEPOk%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;2864&quot; height=&quot;1302&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후부터는 팀원들이 티켓을 처리할 때 마다 &lt;b&gt;팀원 간의 코드 리뷰 이후에 Develop 브랜치에 머지하도록&lt;/b&gt; 함으로써 팀원들이 코드에 대한 이해도를 높여가면서, 그리고 Develop 브랜치에 안정된 코드가 머지되어 배포가 되도록 함으로써 전반적으로 프로젝트의 안정성을 높여갔습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그런데 다 좋다 이거야...그런데 AI는 어떡하지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 프론트엔드, 백엔드는 관성적으로 CRUD를 만들어가면 어느 정도는 되는 일이지만 (물론 중간에 기술적 챌린지가 없다는 것도 아니지만...), AI는 모든 팀원이 겨우 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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프론트엔드가 데이터 수집 및 전처리를 전반적으로 담당한다.&lt;/li&gt;
&lt;li&gt;백엔드가 AI 모델링을 책임진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드가 AI 모델링을 책임지게 된 상태에서, 백엔드 개발리드인 제가 생각하기에 제가 AI 모델링에서 큰 책임을 가지고 해야한다고 생각했습니다. 이유는 아래와 같았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 팀원들은 아직 Nest.js 자체에 대해서도, 그리고 타입스크립트에 대한 이해도도 떨어지는 상황인데, AI 모델링까지 시켰다간 전반적인 프로젝트 생산성이 저하될 것 같다&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;CNN, RNN 까지만 익힌 제가 &lt;b&gt;장문의 감정분석&lt;/b&gt;을 꽤 높은 스코어가 나오게 모델링하는건 좀 힘들겠다고 판단되어, 가능한 공부는 최대한 하고 모델링을 해보기로 하였습니다. 따라서 제가 AI를 모델링 하기 위해 했던 것들은 아래와 같습니다.&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;Attention is all you need&lt;/b&gt; 라는 논문을 2회독 한다. 그리고 필요한 사전지식 (Gradient Highway model, Attention 기법, Batch Normalization 기법 등...)을 충분히 학습한다.&lt;/li&gt;
&lt;li&gt;Pytorch에 대해서 학습한다. 사실 지금까지 배운건 Tensorflow인데, 막상 내가 사용할 모든 모델은 Pytorch 기준으로 되어있더라...&lt;/li&gt;
&lt;li&gt;Huggingface에 있는 Transformer API를 이용하여 모델 하나를 예시로 만들어보자.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 어떻게 모델링을 하기는 했는데...배포는 어떡하죠?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델링을 끝내고, 정확도 대략 70%의 감정 분석 AI 모델을 완성했습니다. 하지만 제 앞에는 &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;Docker로 배포하면 뚝딱 아니야?&lt;/b&gt; 라고 간단하게 생각해봤으나, 결국 AI 모델을 서버에 inference 시켜서 배포해야만 AI 모델을 사용할 수 있었기에 &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;배포 방법에 대해서 코치님께 물어보았지만, 인공지능 코치님은 AI 모델링만 하시는 분이지, MLOps를 하시거나 혹은 백엔드를 해보신 분이 아니기에 &lt;b&gt;음...저희 회사는 Kubeflow를 사용하긴해요&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;하지만 Kubeflow는 첫째로 Kubernetes 기반으로 프로젝트를 운영해야만 사용 가능한 방법인데다, 둘째로 Kubernetes를 사용한다고 하더라도 Airflow를 사용해야한다는 뜻이기에 Kubeflow를 사용할 수는 없었습니다.&lt;/p&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 서버를 배포하는 것은 저희 팀 스스로 해결해야만 했습니다. 결국 선택한 방법은 &lt;b&gt;flask 기반의 서버를 만들고 그 서버를 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;백엔드 팀원 전체가 Python 기반의 서버를 만들어본 적도, 그리고 저조차 파이썬 기반의 서버를 만들어본적이 없었기 때문에 정말 막막했습니다. 그래도 Spring과 Nest.js를 해본 제 입장에선 &lt;b&gt;파이썬이라고 뭐 다르겠냐. 구조는 똑같겠지&lt;/b&gt; 라는 생각으로 어떻게든 flask 백엔드를 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/doccimann-personal-projects/EEUM-AI&quot;&gt;Flask 기반의 EEUM AI 서버 링크&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1684925253164&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - doccimann-personal-projects/EEUM-AI: 엘리스 AI트랙 6기 3차 프로젝트 EEUM의 AI 서버 레포지토리 입니&quot; data-og-description=&quot;엘리스 AI트랙 6기 3차 프로젝트 EEUM의 AI 서버 레포지토리 입니다! Contribute to doccimann-personal-projects/EEUM-AI development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/doccimann-personal-projects/EEUM-AI&quot; data-og-url=&quot;https://github.com/doccimann-personal-projects/EEUM-AI&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xoHGG/hySKvZB10Y/xkbv4KmMBjV9CJRWz3Wnhk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/doccimann-personal-projects/EEUM-AI&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/doccimann-personal-projects/EEUM-AI&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xoHGG/hySKvZB10Y/xkbv4KmMBjV9CJRWz3Wnhk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - doccimann-personal-projects/EEUM-AI: 엘리스 AI트랙 6기 3차 프로젝트 EEUM의 AI 서버 레포지토리 입니&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;엘리스 AI트랙 6기 3차 프로젝트 EEUM의 AI 서버 레포지토리 입니다! Contribute to doccimann-personal-projects/EEUM-AI development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 배포하려고 Docker build를 수행했는데, Docker image의 크기가 10gb나 되었던 것을 발견하였습니다. 게다가 빌드 서버의 HDD 용량은 30GB밖에 안되었기 때문에, 2번의 배포 이후에 빌드 서버 자체가 뻗어버렸습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;No space left on this device&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 어떻게든 Docker 이미지의 용량을 줄여야하는 기술적 챌린지가 발생해버렸는데, 해결 방법은 아래와 같았습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;pip install 과정에서 남는 빌드 캐시를 모조리 제거한다. 이러한 빌드 캐시가 이미지에도 포함되는 현상을 관측하였기 때문이다.&lt;/li&gt;
&lt;li&gt;Docker build가 끝나고 VM에 배포가 끝나면 system prune 명령어를 통해서 남아있는 도커 빌드 캐시를 제거한다. 이를 통해 HDD 용량을 확보한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기술적 챌린지가 여기서 끝나지는 않았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. AI 서버 배포는 끝났는데...그래서 일기 작성 이후에 분석은 어떻게 시키지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 Nest.js 서버를 메인으로 하여 대부분의 엔드포인트를 여기에 두고, 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;그러면 결국 Nest.js 서버를 통해 사용자가 일기를 등록을 하고, 등록된 일기를 어떻게든 AI 서버가 캐치해서 감정 분석을 하고 음식을 추천해서 Database에 등록을 해야하는 상황인데, 이를 어떻게 또 해결할지가 큰 문제였습니다.&lt;/p&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가지 방법을 생각해봤습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Cron Job을 하나 정의해서 AI 서버가 &lt;b&gt;감정 분석이 수행되지 않은 일기들&lt;/b&gt; 을 분석하도록 작업을 정의해준다. 물론 &lt;b&gt;감정분석이 수행되었는지 여부를 알려주는 컬럼&lt;/b&gt;을 하나 추가하고 인덱스를 적용한다면 테이블을 풀스캔하지 않아도 될 수 있다.&lt;/li&gt;
&lt;li&gt;Redis를 도입하고 Redisson 라이브러리 등을 통해 Pub/Sub 구조의 서버간 통신을 구현한다.&lt;/li&gt;
&lt;li&gt;Amazon SQS를 이용해서 메세지 큐 기반의 Pub/Sub 구조의 서버간 통신을 구현한다.&lt;/li&gt;
&lt;li&gt;Axios 통신을 이용해서 서버간 통신을 처리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국엔 세번째 방법을 통해서 문제를 해결하기로 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;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;500~800ms&lt;/b&gt; 사이로 측정이 되었다.&lt;/li&gt;
&lt;li&gt;그런데 매우 장문이면 레이턴시가 &lt;b&gt;2s&lt;/b&gt;를 충분히 넘을 수가 있다고 팀원들이 판단하였다.&lt;/li&gt;
&lt;/ol&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;따라서 SQS 기반의 pub/sub 구조를 가져감으로써 일기 작성 자체만 처리하고 메시지를 발행함으로써 일기 분석에 대한 레이턴시를 일기 작성에 반영하지 않을 수 있게되었고, SQS는 AWS의 매니지드 메시지큐 서비스이기 때문에 특별한 관리포인트가 추가적으로 발생하지는 않았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Elastic APM 적용 및 프로젝트 마무리. 그리고 발표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주차가 되고 화요일 즈음에 거의 모든 기능을 구현하였습니다. 그리고 본격적으로 서버 성능을 측정하기 위해서 APM을 붙일 필요가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 저는 많은 APM 서비스 중에서 Elastic APM을 선정하여 프로젝트에 적용하였고, 그를 통해 서버의 실시간 tpm (transaction per minute)를 측정할 수 있었으며, 동시에 실시간 Latency를 측정할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 만든 서비스는 거의 모든 로직이 평균적으로 40ms 정도의 레이턴시를 보였으며, 로그인 로직의 경우 평균적으로 120~130ms 의 레이턴시를 보이는 것으로 관측되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3806&quot; data-origin-height=&quot;1818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cT73GS/btshh7vKpIa/aJpieq6Q9JqcejRZtk9N60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cT73GS/btshh7vKpIa/aJpieq6Q9JqcejRZtk9N60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cT73GS/btshh7vKpIa/aJpieq6Q9JqcejRZtk9N60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcT73GS%2Fbtshh7vKpIa%2FaJpieq6Q9JqcejRZtk9N60%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;3806&quot; height=&quot;1818&quot; data-origin-width=&quot;3806&quot; data-origin-height=&quot;1818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 팀은 다행스럽게도 누군가가 데브옵스 전반을 책임지고 있었기에 막판에 배포로 인해서 고통을 받지 않았기에 마지막 5주차는 매우 여유로운 상태로 프로젝트를 마무리 지을 수 있었습니다. &lt;s&gt;(물론 그게 접니다)&lt;/s&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;그리고 토요일에 발표 및 수료식을 하고 6개월의 길다면 긴 엘리스 AI트랙 6기 여정을 끝마쳤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 3차 프로젝트, 그리고 엘리스 AI 트랙을 마무리 한 소감&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 3차 프로젝트 소감을 말씀드려보겠습니다. 엘리스 AI트랙에서 진행한 1, 2, 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차 프로젝트에 함께 해주셨던 백엔드 코치님을 통해서 매우 많은것을 깨달았습니다. 개발은 흔히 &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;6개월 간의 엘리스 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;엘리스 AI 트랙에 들어오기 전까지의 저를 비유하자면 &lt;b&gt;메뚜기마냥 신기술에만 젖어있는 Spring 개발자&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;엘리스 AI 트랙을 달리면서 JavaScript, Node.js, TypeScript 등의 기술을 배웠지만, 사실 제가 6개월을 돌이켜보면 그러한 기술스택보다는 소프트 스킬들을 엄청 많이 얻어갔으며, 동시에 &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;MSA를 하지 않으면 너는 실패한거야&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;하지만 엘리스 AI 트랙을 달리다보니 제가 매우 틀렸던 생각을 하고 있었음을 깨닫게 되었습니다. &lt;b&gt;물론 신기술을 아는건 중요하긴 하겠으나, 사실 그 정도의 신기술이 없더라도 문제는 충분히 해결할 수 있으며, 오히려 문제의 본질에 집중하는게 개발자의 역량 중 하나이다.&lt;/b&gt; 라고 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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개월 동안 저를 스쳐갔건, 혹은 저와 함께 무엇을 같이 해보았건, 그러한 모든 분들께 매우 감사하다는 말씀을 드리면서 글을 마치겠습니다.&lt;/p&gt;</description>
      <category>주저리주저리</category>
      <category>docker</category>
      <category>nest.js</category>
      <category>엘리스AI트랙</category>
      <category>프로젝트</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/13</guid>
      <comments>https://doccimann-memory-archive.tistory.com/13#entry13comment</comments>
      <pubDate>Wed, 24 May 2023 19:50:57 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 워커노드의 OOM에 의한 클러스터 장애</title>
      <link>https://doccimann-memory-archive.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Kubernetes 워커노드에 OOM이 발생하여 모든 워커노드가 연쇄적으로 장애를 일으키는 바람에 쿠버네티스 클러스터가 통째로 먹통이된 사건에 대해서 다뤄볼까합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 상황&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Amazon EKS (Kubernetes v1.22)&lt;/li&gt;
&lt;li&gt;각 워커노드는 t3.medium 인스턴스로 운영중에 있었음 (vcpu 2 + 4Gi Memory)&lt;/li&gt;
&lt;li&gt;단일 노드그룹에 워커노드는 min size = 2, desired size = 2, max size = 6 로 설정이 되어있는 상태임&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 엘리스파크의 백엔드 레포지토리 두개를 동시에 빌드를 돌리는 순간, 갑자기 &lt;b&gt;쿠버네티스 클러스터 상에서 운영하던 모든 서비스들이 503 에러를 내뿜으며 뻗어버리는 현상&lt;/b&gt;이 발생하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;503 에러.webp&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3yC3n/btr9nJaYsaN/gAKQ5iRyZyLt73rQOm96Ok/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3yC3n/btr9nJaYsaN/gAKQ5iRyZyLt73rQOm96Ok/img.webp&quot; data-alt=&quot;대충 이런 화면을 마주하게 되었다....ㅠ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3yC3n/btr9nJaYsaN/gAKQ5iRyZyLt73rQOm96Ok/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3yC3n%2Fbtr9nJaYsaN%2FgAKQ5iRyZyLt73rQOm96Ok%2Fimg.webp&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;904&quot; height=&quot;108&quot; data-filename=&quot;503 에러.webp&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;108&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대충 이런 화면을 마주하게 되었다....ㅠ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 원인&lt;/h3&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;우선, 현재 운영중인 파드의 로그를 전부 체크해보기로 결심했었습니다. 첫번째로 확인해보았던 것은 Jenkins 였는데요, Jenkins Pod의 상태를 관찰해보았더니, 매우 당황스러운 상황을 마주하게 되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681152043937&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NAME                           READY   STATUS            RESTARTS      AGE
pod/docker-build-m0q5w-t9034   3/3     Terminating          0          8m11s
pod/jenkins-5ff87ffcf7-prdxk   1/1     Terminating          0          3d4h
pod/jenkins-5ff87ddqwv-qwerf   0/1     ContainerCreating    0          3d4h

NAME                      TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)                          AGE
service/jenkins-service   NodePort   10.100.xxx.xxx   &amp;lt;none&amp;gt;        8080:32000/TCP,50000:32001/TCP   6d12h

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/jenkins   0/1     0            0           3d4h

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/jenkins-5ff87ffcf7   1         0         0       3d4h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다름아닌, 빌드를 위한 동적 파드와 젠킨스 파드가 모두 &lt;b&gt;Terminating&lt;/b&gt;에 놓여있었고, jenkins pod가 죽음에 의해서 새로 생겨나는 파드 또한 &lt;b&gt;ContainerCreating&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;프로메테우스 또한 kubectl 명령어를 통해서 확인을 해보았으나, alertManager를 비롯한 prometheus, grafana 모두 &lt;b&gt;Terminating&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;다음으로 Kubernetes의 워커노드들의 상태를 확인해보았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681152360502&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu@ip-xxx-xx-xx-xx:~$ kubectl get nodes
NAME                                               STATUS      ROLES    AGE    VERSION
ip-xxx-xx-xx-xxx.ap-northeast-2.compute.internal   NotReady    &amp;lt;none&amp;gt;   3d5h   v1.22.17-eks-a59e1f0
ip-xxx-xx-xx-xxx.ap-northeast-2.compute.internal   NotReady    &amp;lt;none&amp;gt;   3d5h   v1.22.17-eks-a59e1f0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 워커플레인을 구성하는 모든 노드들이 NotReady에 빠져있었으며, describe 명령어를 이용해 노드에서 발생한 이벤트를 조회한 결과, 최초로 &lt;b&gt;docker 동적 파드가 생성된 노드에서 동적 파드가 3~4Gi 가량의 메모리를 잡아먹으며 해당 동적 파드에서 크래시가 발생하였으며, 그로 인해서 해당 노드가 OOM (Out Of Memory) 상태에 빠지게 되었습니다&lt;/b&gt;. 그리고 &lt;b&gt;다른 워커노드 또한 동시에 OOM이 발생하게되면서 쿠버네티스 클러스터를 구성하는 모든 워커노드에 장애가 발생하게 된것이었습니다&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 어떻게 문제를 해결할것인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제의 해결 방법은 아주 간단합니다. &lt;b&gt;워커노드를 구성하는 모든 인스턴스를 scale-up 해주면 됩니다&lt;/b&gt;. 하지만 아래의 사항이 고려되어야합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;기존의 워커노드에 올라간 모든 파드들이 정상적으로 다시 동작해야한다.&lt;/b&gt;&lt;/blockquote&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;/li&gt;
&lt;li&gt;기존의 워커노드들을 모두 재시작한다&lt;/li&gt;
&lt;li&gt;기존의 워커노드에 있던 모든 파드들을 새로운 노드그룹에 마이그레이션한다&lt;/li&gt;
&lt;li&gt;기존의 워커노드를 쿠버네티스 클러스터에서 제거한다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 해결 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 EKS를 Terraform 코드로 관리하고 있었기 때문에, Terraform 코드를 수정하여 새로운 워커노드를 추가하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1681153210352&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eks_managed_node_groups = {
    default_node_group = {
      desired_size   = 2
      min_size       = 2
      max_size       = 6
      instance_types = [&quot;t3.medium&quot;]
    }
    // 새로운 노드그룹
    new_node_group = {
      desired_size   = 2
      min_size       = 2
      max_size       = 6
      instance_types = [&quot;t3.large&quot;]
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 테라폼 코드를 배포하여 EKS 클러스터 상에 새로운 워커노드를 등록하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기존의 워커노드들을 EC2 대시보드에서 재시작 해준 다음, 기존의 워커노드들에 대해서 &lt;b&gt;kubectl drain &amp;lt;node_name&amp;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;그리고 기존의 파드들이 정상적으로 동작함을 확인하였다면, 테라폼 코드 상에서 default_node_group을 제거하여 재배포를 합니다. 그렇게 되면 기존의 t3.medium 타입의 워커노드들이 모두 제거되면서 자연스럽게 모든 워커노드를 스케일업 할 수 있게됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 이상한 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 뭔가 이상한 점이 있습니다. &lt;b&gt;분명히 모든 워커노드가 OOM에 빠져있고, EKS를 생성하면 워커노드는 AutoScaling 그룹에 소속되어있기 때문에 새로운 워커노드가 생성되어야하지 않나?&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;a href=&quot;https://341123.tistory.com/8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[EKS] 환경 구축하기3 - AutoScaling 설정 by 피클s&lt;/a&gt; 에서 찾을 수 있었는데요, EKS는 워커노드를 생성 시에 오토스케일링 그룹 단위로 워커노드를 관리한다는 것은 사실이지만, &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;a href=&quot;https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/autoscaling.html#cluster-autoscaler&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AutoScaling - Amazon EKS (공식문서)&lt;/a&gt; 에서 찾을 수 있었습니다. &lt;b&gt;Cluster AutoScaler&lt;/b&gt;를 배포하는 방법과, &lt;b&gt;Karpenter&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;Cluster AutoScaler&lt;/b&gt;를 배포하는 방식을 채택하여 쿠버네티스 클러스터의 HA (High Availability)를 한층 강화할 수 있게 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 이걸로 충분한가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 충분치는 않습니다. 이 사건의 근본 원인은 OOM 발생은 맞지만, &lt;b&gt;OOM이 발생하기 이전에 이를 예측하거나, 혹은 사전에 막을 수 없었을까? 에 초점을 둬야하기 때문입니다&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;워커노드의 OOM으로 인한 연쇄 장애 현상&lt;/b&gt;을 겪으면서도 그라파나 대시보드를 통해서 쿠버네티스 클러스터 전체의 CPU와 메모리 사용량을 모니터링 하고 있었습니다. 하지만, 쿠버네티스 전체 메모리 사용량은 4.7Gi 가 맥스였고, t3.medium 두 개의 메모리 총량은 8Gi 였기 때문에 &lt;b&gt;설마 OOM이 터지겠어?&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;이번 사건으로 인해 깨달은 점은 크게 2가지입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클러스터 단위 말고도 노드 단위로도 모니터링을 해줘야한다.&lt;/li&gt;
&lt;li&gt;인스턴스 타입을 산정할 때는 &lt;b&gt;배포할 파드의 최대 메모리 사용량도 고려를 해봐야한다&lt;/b&gt;. 예를 들어서, 1개 파드가 최대 3Gi를 사용할 잠재적인 가능성이 존재한다면, 인스턴스 타입은 적어도 해당 파드 메모리를 고려해서 결정해줘야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 글을 마치도록 하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 참고할만한 영상 (저도 아직 안 봤습니다!)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=FPlCVVrCD64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[데브옵스] 오픈소스 Karpenter를 활용한 Amazon EKS 확장 운영 전략 - 신재현, 무신사&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=FPlCVVrCD64&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bLlpY1/hySdoBtNrK/yiukvLQIdiVC4sDQ5OPOG0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/FPlCVVrCD64&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>devOps</category>
      <category>쿠버네티스</category>
      <category>트러블슈팅</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/12</guid>
      <comments>https://doccimann-memory-archive.tistory.com/12#entry12comment</comments>
      <pubDate>Tue, 11 Apr 2023 04:21:49 +0900</pubDate>
    </item>
    <item>
      <title>도커파일 최적화 (Multi-Stage)</title>
      <link>https://doccimann-memory-archive.tistory.com/11</link>
      <description>&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;제가 글에서 다루는 백엔드는 TypeScript로 작성된 Node Express 프로젝트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/doccimann-personal-projects/osondoson-backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;오손도손-백엔드&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1679837494186&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - doccimann-personal-projects/osondoson-backend: Repository for osondoson backend&quot; data-og-description=&quot;Repository for osondoson backend. Contribute to doccimann-personal-projects/osondoson-backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/doccimann-personal-projects/osondoson-backend&quot; data-og-url=&quot;https://github.com/doccimann-personal-projects/osondoson-backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zUZbO/hyR2G3mruW/WXxzwyxwKrKHayT0Sd4sE0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/doccimann-personal-projects/osondoson-backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/doccimann-personal-projects/osondoson-backend&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zUZbO/hyR2G3mruW/WXxzwyxwKrKHayT0Sd4sE0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - doccimann-personal-projects/osondoson-backend: Repository for osondoson backend&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Repository for osondoson backend. Contribute to doccimann-personal-projects/osondoson-backend development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 본격적으로 설명하기 이전에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 개발하고, 해당 프로젝트를 런타임으로 올리는 과정에 대해서 설명해볼까합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;pm2를 설치한다&lt;/li&gt;
&lt;li&gt;필요한 파일들을 모두 컨테이너에 적재한다&lt;/li&gt;
&lt;li&gt;npm 의존성을 모두 설치한다&lt;/li&gt;
&lt;li&gt;프로젝트를 tsc를 이용해서 빌드한다&lt;/li&gt;
&lt;li&gt;필요한 포트를 개방하고 pm2-runtime을 이용해서 빌드된 js 파일을 실행한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해당 과정을 이용해 Dockerfile을 작성하면 아래와 같은 파일을 만들수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1679837780127&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM node:18-alpine
WORKDIR /opt/app
COPY package*.json ./
RUN npm install &amp;amp;&amp;amp; npm install -g pm2
COPY . .
EXPOSE 3500
RUN npm run build
CMD [&quot;pm2-runtime&quot;, &quot;dist/bin/index.js&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Dockerfile을 이용해서 Jenkins에서 빌드를 실행하는 경우, Docker build 스테이지에서 &lt;b&gt;46초&lt;/b&gt;만큼 걸리며, 이미지의 크기는 &lt;b&gt;423MB&lt;/b&gt;로 이미지가 생성되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 용량 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 이용해서 Docker 이미지를 생성하면 실행이 되긴하지만, Docker image 내부에 불필요한 이미지까지 같이 적재되기 때문에 이는 비용 문제로 귀결됩니다. 따라서 Docker image의 용량을 줄일 수 있다면 최대한 줄여주는게 좋습니다.&lt;/p&gt;
&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;.env 파일과, Data 폴더, dist 폴더, 그리고 package.json, package-lock.json&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;따라서 해당 파일만을 추출해서 런타임으로 올리면 되기 때문에, Multi-Stage 방법으로 Dockerfile을 작성하겠습니다. 스테이지는 총 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;우선 2개의 스테이지를 이용해서 작성한 최적화된 도커파일을 소개하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1679839967866&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Stage 1
FROM node:18-alpine as BUILDER
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
COPY .env Data ./
COPY --from=BUILDER /app/dist ./dist
RUN npm install --production &amp;amp;&amp;amp; npm install -g pm2
EXPOSE 3500
CMD [&quot;pm2-runtime&quot;, &quot;dist/bin/index.js&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일을 이용해서 프로젝트를 빌드하는 경우, devDependencies를 제외하고 npm 의존성을 설치하기 때문에 빌드 용량이 줄어드는 효과를 보일겁니다. 예상대로 &lt;b&gt;용량은 423MB에서 382MB 까지 줄어드는&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 image history &amp;lt;image-id&amp;gt; 명령어를 이용해서 둘을 까보면 어디서 줄어들었는지 한 눈에 확인이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1679840240594&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 기존
&amp;lt;missing&amp;gt;      4 minutes ago   RUN /bin/sh -c npm install -g pm2 &amp;amp;&amp;amp; npm ins&amp;hellip;   246MB     buildkit.dockerfile.v0

# 개선된 이미지
&amp;lt;missing&amp;gt;      5 minutes ago   RUN /bin/sh -c npm install --production &amp;amp;&amp;amp; n&amp;hellip;   206MB     buildkit.dockerfile.v0&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;그러나, 도커 빌드 과정이 길어진만큼 도커 빌드타임이 길어졌습니다. 기존의 방식은 젠킨스 환경에서 평균적으로 46s가 걸린 반면, 해당 방식으로 빌드 시 &lt;b&gt;1분30초&lt;/b&gt; 정도가 걸렸습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 빌드타임 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 빌드타임을 최적화하겠습니다. 해당 이미지에서 빌드타임을 최적화 할 수 있는 부분은 단 한부분, npm 의존성을 설치하는 부분에서 개선이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm install --production 을 이용해서 설치하게 되면 package.json을 읽어내서 모든 의존성을 최신 버전으로 설치해옵니다.&lt;/p&gt;
&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;npm ci --omit=dev&lt;/b&gt; 를 이용해서 설치하면 package.json 이 아닌 package-lock.json 을 이용해서 설치하기 때문에 의존성 설치에 걸리는 시간이 줄어든다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방식을 이용한 Dockerfile을 보여드리겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1679840513162&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Stage 1
FROM node:18-alpine as BUILDER
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
COPY .env Data ./
COPY --from=BUILDER /app/dist ./dist
RUN npm ci --omit=dev &amp;amp;&amp;amp; npm install -g pm2
EXPOSE 3500
CMD [&quot;pm2-runtime&quot;, &quot;dist/bin/index.js&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Dockerfile을 이용해서 빌드하는 경우 젠킨스 환경에서&amp;nbsp;&lt;b&gt;1분 30초에서 1분 16초 정도로 대략 14초 정도의 빌드 타임이 개선되는 효과&lt;/b&gt;를 보였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/build/building/multi-stage/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Multi stage builds - Docker docs&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1679840650301&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;Multi-stage builds&quot; data-og-description=&quot; &quot; data-og-host=&quot;docs.docker.com&quot; data-og-source-url=&quot;https://docs.docker.com/build/building/multi-stage/&quot; data-og-url=&quot;https://docs.docker.com/build/building/multi-stage/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Zzdb5/hyR2T9jCkj/eGVJWYQX53b36wgtqBwrf0/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/bc9GYE/hyR37ES9N4/va5y8sX9JvfXKSZzRKchKk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://docs.docker.com/build/building/multi-stage/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.docker.com/build/building/multi-stage/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Zzdb5/hyR2T9jCkj/eGVJWYQX53b36wgtqBwrf0/img.png?width=129&amp;amp;height=128&amp;amp;face=0_0_129_128,https://scrap.kakaocdn.net/dn/bc9GYE/hyR37ES9N4/va5y8sX9JvfXKSZzRKchKk/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&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;Multi-stage builds&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.docker.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;</description>
      <category>node.js</category>
      <category>node.js</category>
      <category>도커</category>
      <category>빌드 최적화</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/11</guid>
      <comments>https://doccimann-memory-archive.tistory.com/11#entry11comment</comments>
      <pubDate>Sun, 26 Mar 2023 23:25:03 +0900</pubDate>
    </item>
    <item>
      <title>프로세스의 종료</title>
      <link>https://doccimann-memory-archive.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 프로세스의 종료, 특히나 Graceful shutdown에 대해서 짧게만 다뤄볼 예정입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 프로세스의 완전한 종료란?&lt;/h3&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;/li&gt;
&lt;li&gt;이전에 실행된적이 있는 프로세스라면 해당 프로세스에 대응하는 &lt;b&gt;PCB (Process Control Block)&lt;/b&gt;을 읽어내서 이전 상태로부터 시작하고, 이전에 실행된적이 없는 프로세스라면 프로그램 코드를 읽어내서 프로세스를 실행한다. 그와 동시에 PCB를 생성한다. 그리고 프로세스는 Ready 상태에 진입한다.&lt;/li&gt;
&lt;li&gt;프로세스 스케쥴러에 의해서 CPU에 디스패치가 되면 프로세스는 자신의 작업을 수행합니다.&lt;/li&gt;
&lt;/ol&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;/li&gt;
&lt;li&gt;프로세스를 종료하기 전에 자신이 수행하던 작업을 모두 마무리 지은 다음에, 지금의 프로세스의 상태를 PCB에 기록한다&lt;/li&gt;
&lt;li&gt;프로세스에 할당된 모든 자원을 운영체제에 반납한다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로세스 종료 시그널&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스의 종료 시그널을 5개 정도만 소개하고 다음 단락으로 넘어가겠습니다.&lt;/p&gt;
&lt;table style=&quot;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: 9.53485%;&quot;&gt;번호&lt;/td&gt;
&lt;td style=&quot;width: 17.0932%;&quot;&gt;이름&lt;/td&gt;
&lt;td style=&quot;width: 61.279%;&quot;&gt;설명&lt;/td&gt;
&lt;td style=&quot;width: 12.093%;&quot;&gt;기본 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53485%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 17.0932%;&quot;&gt;SIGHUP (HUP)&lt;/td&gt;
&lt;td style=&quot;width: 61.279%;&quot;&gt;Hang Up의 약자로, 데몬 관련 환경 설정 파일을 변경시키고 변화된 내용을 적용하기 위해 재시작할 때 해당 시그널이 적용됩니다.&lt;/td&gt;
&lt;td style=&quot;width: 12.093%;&quot;&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53485%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 17.0932%;&quot;&gt;SIGINT (INT)&lt;/td&gt;
&lt;td style=&quot;width: 61.279%;&quot;&gt;키보드로부터 오는 인터럽트 시그널로, 실행을 중지시키는 시그널입니다. [Ctrl + C]&lt;/td&gt;
&lt;td style=&quot;width: 12.093%;&quot;&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53485%;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 17.0932%;&quot;&gt;SIGKILL (KILL)&lt;/td&gt;
&lt;td style=&quot;width: 61.279%;&quot;&gt;프로세스를 강제로 종료시키는 시그널&lt;/td&gt;
&lt;td style=&quot;width: 12.093%;&quot;&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53485%;&quot;&gt;15&lt;/td&gt;
&lt;td style=&quot;width: 17.0932%;&quot;&gt;SIGTERM (TERM)&lt;/td&gt;
&lt;td style=&quot;width: 61.279%;&quot;&gt;가능한 정상 종료를 시키는 시그널로, kill 명령어의 기본 시그널입니다.&lt;/td&gt;
&lt;td style=&quot;width: 12.093%;&quot;&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53485%;&quot;&gt;17&lt;/td&gt;
&lt;td style=&quot;width: 17.0932%;&quot;&gt;SIGCHLD (CHILD)&lt;/td&gt;
&lt;td style=&quot;width: 61.279%;&quot;&gt;자식 프로세스가 stop 되거나 종료되었을 때 부모 프로세스에게 전달되는 시그널&lt;/td&gt;
&lt;td style=&quot;width: 12.093%;&quot;&gt;무시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Graceful Shutdown&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://12factor.net/disposability&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Disposability - The Twelve-Factor App&lt;/a&gt; 을 참고하면, Graceful shutdown에 대한 아래의 언급을 확인해볼 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Maximize robustness with fast startup and graceful shutdown.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;Processes with the twelve factors shut down gracefully when they receive a SIGTERM signal from the process manager.&amp;nbsp;&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 견고한 백엔드 어플리케이션이라면 프로세스의 빠른 시작과, 그리고 graceful shutdown을 지켜야한다는 뜻인데요, graceful shutdown에 대해서 좀 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Graceful shutdown이란, 프로세스가 종료될 때 최대한 side effect가 일어나지 않도록 로직들을 잘 처리하고 종료하는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국에는 process가 사용자 혹은 운영체제로부터 종료 신호를 받게되면 진행하던 로직을 마저 잘 처리한 다음에 종료되는 것이 핵심이라고 볼 수 있겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Web process에서의 Graceful Shutdown&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web process에 대한 graceful shutdown은 그저 종료 signal을 받으면 listening port를 닫음으로써 더 이상의 요청을 받지 않고 처리 중인 request는 마저 잘 처리하고 DB connection이 존재한다면 커넥션을 끊고 프로세스를 종료하면됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Worker process에서의 Graceful Shutdown&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker process에서의 Graceful Shutdown은 이전의 방식보다 복잡해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 RabbitMQ 혹은 Kafka와 같은 메세지 큐 또는 이벤트 큐를 사용하여 발행된 메세지를 구독하여 처리하는 컨슈머 프로세스를 shutdown 한다고 가정해봅시다. 그리고 해당 프로세스에는 shutdown hook이 설정되어있지 않다고 가정합니다.&lt;/p&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이며, 그리고 polling된 이벤트가 하나도 없는 상황)이라면 프로세스를 바로 죽이더라도 상관이 없겠으나, 만일 해당 토픽의 이벤트를 구독중인 컨슈머가 하나라도 있는 상태에서 해당 토픽을 구독하는 컨슈머 프로세스를 하나라도 죽이게 된다면 즉시 &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;따라서 worker process에 대한 graceful shutdown은 아래의 과정을 거쳐서 진행해주는게 좋습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 혹은 운영체제로부터 종료 signal을 수신하면 처리중인 이벤트에 대해서 이벤트 큐에 반환한다. 이 때 반환되는 내용은 &lt;b&gt;NACK에 대응되는 이벤트를 반환해야하며, 멱등성이 있는 내용&lt;/b&gt;으로 반환을 해야합니다. (보상 트랜잭션)&lt;/li&gt;
&lt;li&gt;shutdown hook을 정의하여 종료되면서 오프셋을 갱신해줍니다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 모든 백엔드 어플리케이션에 대해서 Graceful Shutdown을 적용하면 좋은게 아닌가 라고 생각할 수 있습니다. 물론 일반적인 web backend application의 경우 처리 중인 request가 있다고 하더라도 해당 request가 반드시 처리되어야만 하는 요청이 아닌 경우에는 graceful shutdown 없이 kill -9 옵션을 통해서 프로세스를 종료하여도 괜찮다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 분산 시스템에 있어서는 Graceful Shutdown을 적용하지 않으면 발생할 수 있는 side effect가 존재하기 때문에 shutdown hook을 통한 종료전 로직 처리, 그리고 안전한 프로세스 종료를 통해 프로세스를 종료해주는 것이 좋다고 생각합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. References&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://12factor.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Twelve-factor app&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678622986046&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;The Twelve-Factor App&quot; data-og-description=&quot;Background The contributors to this document have been directly involved in the development and deployment of hundreds of apps, and indirectly witnessed the development, operation, and scaling of hundreds of thousands of apps via our work on the Heroku pla&quot; data-og-host=&quot;12factor.net&quot; data-og-source-url=&quot;https://12factor.net/&quot; data-og-url=&quot;https://12factor.net/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://12factor.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://12factor.net/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;The Twelve-Factor App&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Background The contributors to this document have been directly involved in the development and deployment of hundreds of apps, and indirectly witnessed the development, operation, and scaling of hundreds of thousands of apps via our work on the Heroku pla&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;12factor.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kapentaz.github.io/spring/Spring-Boot-Actuator-Graceful-Shutdown/#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링부트 에서의 Graceful Shutdown&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678623139948&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Boot Actuator Graceful Shutdown&quot; data-og-description=&quot;Spring Boot 환경에서 application을 shutdown 하는 방법 중 대표적인 것이 actuator의 shutdown endpoint 기능을 이용하는 것입니다. 이 endpoint는 예상과 달리 처리 중인 요청이 있더라도 그냥 shutdown 처리를 합니&quot; data-og-host=&quot;kapentaz.github.io&quot; data-og-source-url=&quot;https://kapentaz.github.io/spring/Spring-Boot-Actuator-Graceful-Shutdown/#&quot; data-og-url=&quot;https://kapentaz.github.io/spring/Spring-Boot-Actuator-Graceful-Shutdown/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Cihaz/hyRTSbtQ3T/h66Kz7CoHTG7dWbFyPuv21/img.jpg?width=2000&amp;amp;height=1333&amp;amp;face=0_0_2000_1333&quot;&gt;&lt;a href=&quot;https://kapentaz.github.io/spring/Spring-Boot-Actuator-Graceful-Shutdown/#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kapentaz.github.io/spring/Spring-Boot-Actuator-Graceful-Shutdown/#&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Cihaz/hyRTSbtQ3T/h66Kz7CoHTG7dWbFyPuv21/img.jpg?width=2000&amp;amp;height=1333&amp;amp;face=0_0_2000_1333');&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;Spring Boot Actuator Graceful Shutdown&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 환경에서 application을 shutdown 하는 방법 중 대표적인 것이 actuator의 shutdown endpoint 기능을 이용하는 것입니다. 이 endpoint는 예상과 달리 처리 중인 요청이 있더라도 그냥 shutdown 처리를 합니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kapentaz.github.io&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;</description>
      <category>CS</category>
      <category>graceful shutdown</category>
      <category>프로세스</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/10</guid>
      <comments>https://doccimann-memory-archive.tistory.com/10#entry10comment</comments>
      <pubDate>Sun, 12 Mar 2023 21:12:30 +0900</pubDate>
    </item>
    <item>
      <title>Jenkins on K8S를 설정하며 겪은 일들</title>
      <link>https://doccimann-memory-archive.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 저는 부트캠프 내에서 프로젝트 스터디를 진행하면서 스터디의 DevOps, 그리고 백엔드 파트를 담당하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 현재는 백엔드 프로젝트를 Kubernetes 환경에서 운영중에 있는데요, 그 과정에서 Jenkins도 Kubernetes에서 관리하기로 결정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 제가 왜 Jenkins를 Kubernetes로 운영하려고 하는지, 그리고 Jenkins on K8S를 설정하면서 겪은 어려움들을 여러분들께 공유해드리고자합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 배경지식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Jenkins on k8s를 설명하기 이전에, Jenkins에 대해서 조금 이야기를 해볼까합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젠킨스는 여러분들도 아시다시피 CI/CD 자동화 도구입니다. 그러므로 젠킨스 agent는 사내의 모든 빌드들을 처리해야하기 때문에 리소스를 상당히 많이 차지합니다. &lt;a href=&quot;https://www.jenkins.io/doc/book/installing/docker/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;젠킨스 공식문서&lt;/a&gt;를 참고하시면 &lt;b&gt;최소 RAM은 4gb에 50gb 이상의 디스크 유휴공간&lt;/b&gt;을 추천한다고 명시되어있습니다. 그리고 젠킨스를 master/slave 구조로 운영하는 것이 아닌 standalone 구조로 운영을 한다라고 가정하면, 젠킨스에 들어오는 빌드의 양에 따라서 많게는 단일 인스턴스의 RAM이 40GB 이상도 필요할 수 있다라고 알려져있습니다.&lt;/p&gt;
&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;/li&gt;
&lt;li&gt;젠킨스는 플러그인을 설치할 때마다 Restart가 필요하다. 그런데 하나의 인스턴스가 모든 빌드를 책임져야하기 때문에 Restart의 빈도는 증가하며, 이는 곧 장애로 이어질 확률이 높다&lt;/li&gt;
&lt;li&gt;빌드가 분산되지 않고 단일 인스턴스가 책임을 지기 때문에 RAM이 많이 있다고한들 CPU 자원이 딸려서 전체 빌드 속도가 느려질 우려도 존재한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 많은 곳에서는 젠킨스를 사용할 때 Jenkins master/slave 구조를 이용해서 slave에 빌드 환경을 구성하고, master는 빌드만 관리하는 형태로 많이 관리한다고 알려져있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;jenkins_master_slave.png&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w1AJJ/btr1YJWdwjN/hvywkzsl96wEA8S2I9uUt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w1AJJ/btr1YJWdwjN/hvywkzsl96wEA8S2I9uUt0/img.png&quot; data-alt=&quot;Jenkins master/slave 구조는 위처럼 구성한다고 생각하면 편하다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w1AJJ/btr1YJWdwjN/hvywkzsl96wEA8S2I9uUt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw1AJJ%2Fbtr1YJWdwjN%2Fhvywkzsl96wEA8S2I9uUt0%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;425&quot; data-filename=&quot;jenkins_master_slave.png&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;425&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Jenkins master/slave 구조는 위처럼 구성한다고 생각하면 편하다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 그런데 왜 master/slave 구조로 Jenkins를 구성하지 않고 쿠버네티스에 올려요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이번 프로젝트에서 Jenkins 환경을 구성하면서 master/slave 구조로 갈지, 혹은 jenkins on k8s 구성으로 갈지에 대해서 많은 고민을 하였습니다. 결과만 놓고 보자면, 저는 후자를 채택하였지만 그에 대한 합당한 근거를 제시해보고자합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Plugin을 설치하면 젠킨스 agent를 Restart를 해야하는건 master/slave 구조로 가져가더라도 달라진게 없다.&lt;/li&gt;
&lt;li&gt;master/slave 구조로 가져가면 형상관리의 난이도가 많이 상승한다. 물론 소규모의 사내 프로젝트 내지는 내가 진행중인 토이프로젝트 수준에서 필요한 인프라라면 형상관리의 난이도가 높지는 않으나, slave의 개수가 많아지고 빌드 환경이 다양해질수록 형상관리 난이도는 올라간다.&lt;/li&gt;
&lt;li&gt;무엇보다도 내가 jenkins on k8s를 채택한건 &lt;b&gt;비용의 문제가 제일 크다&lt;/b&gt;. master/slave 구조로 젠킨스를 운영한다는 것은 적어도 2 * (Number of Jenkins Instance) GB 만큼의 RAM이 필요한데, 이를 계속 쓰지도 않을거면서 클라우드에 올려두는 것의 비용 낭비가 심하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세번쨰 근거를 보충 설명하겠습니다. Jenkins on k8s는 master만 쿠버네티스에 배포해두면 &lt;b&gt;빌드가 유발되었을 때 slave를 동적으로 파드의 형태로 배포해서 거기서 빌드를 수행하기 때문에&lt;/b&gt; 이전에 설명드렸던 master/slave의 구조가 master는 배포된 jenkins pod, slave는 빌드 유발에 의해 배포되는 동적 jenkins slave pod가 역할을 담당해주므로 성립합니다.&lt;/p&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인 상태에서는 jenkins master agent만 쿠버네티스 위에서 동작하므로 빌드 리소스의 양을 획기적으로 줄일수있는 효과를 불러옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4988&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmO5Ae/btr1I9QhoTo/Zp7VW3zoGzDylENWQuwVK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmO5Ae/btr1I9QhoTo/Zp7VW3zoGzDylENWQuwVK0/img.png&quot; data-alt=&quot;며칠간 jenkins 메모리를 관찰해보았다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmO5Ae/btr1I9QhoTo/Zp7VW3zoGzDylENWQuwVK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmO5Ae%2Fbtr1I9QhoTo%2FZp7VW3zoGzDylENWQuwVK0%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;4988&quot; height=&quot;618&quot; data-origin-width=&quot;4988&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;며칠간 jenkins 메모리를 관찰해보았다&lt;/figcaption&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;또한 며칠간 Jenkins를 모니터링 해본 결과, Jenkins master는 평시 500~1000MB 사이의 메모리 점유를 유지하며, 빌드 유발시 할당되는 파드는 대략 1~2gb 사이의 크기로 할당되는것을 확인하였습니다. 물론, 빌드 환경에 따라서 동적 할당되는 파드의 크기는 달라질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Jenkins on k8s는 어떻게 배포하죠?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분은 깃허브 링크로 대체하겠습니다. 더욱 자세한 내용은 &lt;a href=&quot;https://www.jenkins.io/doc/book/installing/kubernetes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Install Jenkins on Kubernetes&lt;/a&gt;를 참고해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/kubernetes-utils/tree/main/jenkins&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/doccilabs/kubernetes-utils/tree/main/jenkins&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1677858121776&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - doccilabs/kubernetes-utils: Repository for kubernetes utils (k8s v1.24)&quot; data-og-description=&quot;Repository for kubernetes utils (k8s v1.24). Contribute to doccilabs/kubernetes-utils development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/doccilabs/kubernetes-utils/tree/main/jenkins&quot; data-og-url=&quot;https://github.com/doccilabs/kubernetes-utils&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bW453h/hyROSPw6zz/HRWI7ahkcfgEUzC7KsVc0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/kubernetes-utils/tree/main/jenkins&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/doccilabs/kubernetes-utils/tree/main/jenkins&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bW453h/hyROSPw6zz/HRWI7ahkcfgEUzC7KsVc0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - doccilabs/kubernetes-utils: Repository for kubernetes utils (k8s v1.24)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Repository for kubernetes utils (k8s v1.24). Contribute to doccilabs/kubernetes-utils development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Jenkins를 배포하면서 겪은 어려움들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 여러개가 있겠지만, 대표적인 몇개만 언급하고 지나가겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Helm Chart로 젠킨스를 배포하면 오류가 발생하는 현상&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 &lt;a href=&quot;https://www.jenkins.io/doc/book/installing/kubernetes/&quot;&gt;젠킨스 공식문서 &lt;/a&gt; 의 설명에서 리소스를 정의해서 배포하는 방법에 이어서 Helm Chart를 이용해서 배포하는 방법또한 소개를 하고있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Helm Chart를 이용해서 Jenkins를 Kubernetes 상에 배포를 해보면 Jenkins Pod가 CrashLoopBackOff에 빠지는 현상을 확인할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Cache miss for: update-center-2.332.3. Unable to retrieve JSON from https://updates.jenkins.io/update-center.json?version=2.332.3&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 오류는 &lt;b&gt;젠킨스가 plugin 업데이트 요청을 수행하는 사이트와 커넥션을 맺지 못하기 때문에 벌어지는 현상&lt;/b&gt;인데, 보안그룹을 아무리 열어봐도 해결이 되지 않았던 문제였기 때문에 저는 Helm Chart로 설치하는 방법 대신에 Kubernetes 리소스를 직접 정의하는 방식으로 사용하였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 배포한 Jenkins master와 Kubernetes 클러스터를 연결해야하더라&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;Jenkins에 Kubernetes 플러그인을 설치합니다&lt;/li&gt;
&lt;li&gt;Jenkins 설정 &amp;gt; 노드 관리 &amp;gt; Configure Clouds &amp;gt; Add a new cloud를 클릭합니다&lt;/li&gt;
&lt;li&gt;Kubernetes URL에는 본인의 쿠버네티스 클라우드의 URL을 입력합니다. (저의 경우는 EKS를 사용중이므로 EKS 클러스터 주소를 입력했습니다)&lt;/li&gt;
&lt;li&gt;Kubernetes Namespace에는 Jenkins를 배포한 namespace를 입력합니다.&lt;/li&gt;
&lt;li&gt;Jenkins URL은 본인의 Jenkins master 접속 주소, 그리고 Jenkins tunnel에는 jenkins master의 50000번 포트에 대응되는 ClusterIP를 대응시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히나 jenkins tunnel을 설정하는게 중요한데요, 이를 설정하지 않으면 jenkins master 호스트의 50000번 포트에 대해서 빌드 요청을 날리기 때문에 connection timeout이 발생하는 현상을 구경할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 파드간 통신을 위해서는 Terraform 으로 EKS를 설정한 경우 추가해야하는 보안그룹 규칙이 존재합니다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 파드간 통신, 그리고 서비스간 통신을 수행하기 위해서는 EKS에 추가해야할 보안그룹 규칙이 2개가 존재합니다. 다름 아닌 &lt;b&gt;kube-dns에서 dns를 가져오기 위해 열어야하는 컨트롤 플레인에 대한 보안그룹 규칙, 그리고 Kubernetes 서비스에서 사용하는 ip 대역에 대한 보안그룹 규칙&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;컨트롤 플레인과 워커노드와의 통신을 위해서라면 VPC에 할당한 cidr_block에 대해서 inbound/outbound 규칙을 열어야합니다&lt;/li&gt;
&lt;li&gt;그리고 kube-dns에서 DNS를 가져와서 ip로 치환하게 되면 service의 clusterIP로 치환이 되기 때문에 Kubernetes가 사용중인 ip 대역에 대해서도 inbound, outbound를 열어야합니다. (일반적으로 10.0.0.0/8 대역을 쿠버네티스 서비스가 사용합니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 무엇보다도, service.yaml 에서 50000번 포트도 열려있어야합니다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용은 &lt;a href=&quot;https://www.jenkins.io/doc/book/installing/kubernetes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&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;이는 위에서 제공한 깃허브 링크에는 미리 반영이 되어있으나, JNLP는 50000번 포트에 대해서 열려있기 때문에 Service에서 50000번 포트를 열어두지 않으면 빌드를 시작할 때 jenkins master가 jenkins slave agent에 요청을 전달하지 못합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 그럼 저게 장점만 있느냐?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 해당 방법은 단점또한 명확하게 존재합니다. 제가 Jenkins on k8s를 운영하면서 느낀 단점은 대표적으로 2가지입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우선 Jenkins Pipeline 정의 방법이 기존의 방식과 사뭇 다릅니다. Jenkins on k8s는 slave를 상시 운영하는 대신에 slave를 동적 파드로 생성하는 방식이다보니 파드 할당시 빌드 환경을 정의해야하는데요, 이를 podTemplate의 형태로 구현하기 때문에 처음 접하는 분들은 많이 생소합니다.&lt;/li&gt;
&lt;li&gt;무엇보다 파이프라인 작성에 있어서 참고할만한 소스가 많이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkinsfile 예시도 보여드리고 싶었으나, 아직까지 jenkins on k8s에서 사용하는 Jenkinsfile도 많이 미흡한 상태이기 때문에 더욱 성숙된 파이프라인을 구성할 수 있을 때 Jenkinsfile도 추가 업로드하도록 하겠습니다.&lt;/p&gt;
&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;</description>
      <category>devOps</category>
      <category>DevOps</category>
      <category>jenkins</category>
      <category>Kubernetes</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/9</guid>
      <comments>https://doccimann-memory-archive.tistory.com/9#entry9comment</comments>
      <pubDate>Sat, 4 Mar 2023 01:27:09 +0900</pubDate>
    </item>
    <item>
      <title>내가 쿠버네티스 설정하며 겪은 삽질들 (alb-controller, jenkins, monitoring)</title>
      <link>https://doccimann-memory-archive.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 쿠버네티스를 설정하면서 겪은 삽질들에 대해서 다뤄보고자합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글에는 코드를 많이 소개하기 보다는, 원인 및 원인 분석, 해결 방법 위주로 작성해볼 예정입니다. 코드의 경우 필요할 경우 최대한 깃허브 링크로 소개해드릴 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. EKS 세팅 (feat. Terraform)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 저는 Kubernetes 환경을 AWS에서 제공하는 EKS 매니지드 서비스를 사용해보기로 결정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 EKS를 IaC로 관리하기 위해서 terraform 이라는 툴을 사용하였는데요, 해당 코드에 대한 뼈대 코드는 아래의 링크를 참고하시면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/terraform-codes/tree/main/eks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;EKS 세팅을 위한 테라폼 코드&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1677421317405&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - doccilabs/terraform-codes: Repository for terraform codes that im using now&quot; data-og-description=&quot;Repository for terraform codes that im using now. Contribute to doccilabs/terraform-codes development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/doccilabs/terraform-codes/tree/main/eks&quot; data-og-url=&quot;https://github.com/doccilabs/terraform-codes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kyTVS/hyRKZO6rh0/77BQkqYbqqoVh8Q4HBsja0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/terraform-codes/tree/main/eks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/doccilabs/terraform-codes/tree/main/eks&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kyTVS/hyRKZO6rh0/77BQkqYbqqoVh8Q4HBsja0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - doccilabs/terraform-codes: Repository for terraform codes that im using now&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Repository for terraform codes that im using now. Contribute to doccilabs/terraform-codes development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;h3 data-ke-size=&quot;size23&quot;&gt;1. AWS LoadBalancer Controller 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS에서는 ingress 리소스를 사용하기 위해서는 loadbalancer controller를 설치해야만합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 AWS LoadBalancer Controller를 설치하는 방법은 &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/aws-load-balancer-controller.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS 도큐먼트&lt;/a&gt;에서도 잘 소개가 되어있으나, 저는 공식문서의 방법을 조금 변형해서 terraform, helm 으로 설치하는 방법을 채택하였습니다.&lt;/p&gt;
&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;a href=&quot;https://github.com/doccilabs/terraform-codes/tree/main/alb-controller-policy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(1) AWS LoadBalancer의 iamserviceaccount에 적용할 policy를 생성하는 테라폼 코드&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/kubernetes-utils/blob/main/alb-controller/setting.sh&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(2) AWS LoadBalancer를 생성하는 스크립트&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;AWS LoadBalancer는 Kubernetes 버전에 따른 호환성이 존재하기 때문에 버전 호환성 (Capability)를 잘 체크하셔야합니다. 저는 Kubernetes 1.24 버전을 사용중이므로 AWS LoadBalancer Controller 이미지 버전을 2.4.4 로 설치하였습니다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 겪었던 문제점 3가지만 소개시켜드리도록 하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Terraform으로 EKS를 생성하면 보안그룹을 매우 보수적으로 생성하더라&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼을 이용해서 EKS를 생성하면 기본적으로 노드그룹에 적용되는 보안그룹은 Control Plane에 대해서 일부 포트(443, 10250 등등)만 열려있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 벌어지는 현상 중 하나는, AWS LoadBalancer는 컨트롤 플레인에 대해서 웹훅을 통해 로드밸런서 생성을 요청하는데요, 여기에서 웹훅 포트가 열려있지 않기 때문에 &lt;b&gt;AWS LoadBalancer Controller 설치 과정에서 CrashLoopBackOff 에러가 발생하게됩니다&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;해결 방법은, 아래의 코드 스니펫을 노드 그룹의 보안 규칙에 추가하는 것이었습니다. 아래의 코드를 테라폼에 추가하면 9443 포트로 워커노드는 컨트롤 플레인과 통신할 수 있게됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677422871107&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;alb_controller_webhook_rule = {
      description                   = &quot;Cluster API to AWS LB Controller webhook&quot;
      protocol                      = &quot;all&quot;
      from_port                     = 9443
      to_port                       = 9443
      type                          = &quot;ingress&quot;
      source_cluster_security_group = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) STS 권한 문제&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;쿠버네티스 위에서 젠킨스를 설치하고 ingress를 이용해서 젠킨스 서비스를 노출시키려고 했을 때 문제점이 발견되었는데요, ingress 리소스에 로드밸런서 주소가 잡히지 않고 있었던 것이었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677423079208&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NAME      CLASS    HOSTS                     ADDRESS    PORTS    AGE
jenkins   &amp;lt;none&amp;gt;   jenkins.example.com                   80      157m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AWS LoadBalancer Controller의 로그를 확인해보았는데, sts 권한 인증을 하는 과정에서 &lt;b&gt;dial tcp time out&lt;/b&gt;이 발생하고 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677423189690&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;level&quot;:&quot;error&quot;,&quot;ts&quot;:1643192131.4378264,&quot;logger&quot;:&quot;controller-runtime.manager.controller.ingress&quot;,&quot;msg&quot;:&quot;Reconciler error&quot;,&quot;name&quot;:&quot;ingress-2048&quot;,&quot;namespace&quot;:&quot;game-2048&quot;,&quot;error&quot;:&quot;WebIdentityErr: failed to retrieve credentials\ncaused by: RequestError: send request failed\ncaused by: Post \&quot;https://sts.ap-southeast-1.amazonaws.com/\&quot;: dial tcp 103.246.148.245:443: i/o timeout&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 sts 권한이 iamserviceaccount에 존재하지 않기 때문에 벌어지는 현상이었는데, sts 권한을 alb-controller-policy에 추가함으로써 해결하였습니다. 해당 코드는 &lt;a href=&quot;https://github.com/doccilabs/terraform-codes/blob/main/alb-controller-policy/main.tf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기서&lt;/a&gt; 확인하시면 되겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Jenkins 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Jenkins를 Kubernetes 위에 설치하기로 하였습니다. 제가 Jenkins를 외부가 아닌 Kubernetes 내부에 설치하려고 한 이유는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jenkins는 하나의 프로세스당 적어도 메모리만 2Gi 씩 할당이 요구됩니다. 그리고 제대로된 세팅을 위해서라면 master/slave 구조로 jenkins를 구성해야하기 때문에 세팅이 여간 까다로운게 아닙니다.&lt;/li&gt;
&lt;li&gt;그리고 master/slave 구조로 Jenkins를 구성한다고 하더라도, 현실적으로 해당 slave의 자원을 계속 사용하는게 아닙니다. 그 말인 즉슨, 제가 운영하는 환경 상에서는 빌드의 양이 slave를 가득 채울만큼은 아니기 때문에 유휴 리소스는 계속 발생할 수 밖에 없어집니다. 곧 이는 비용의 문제로 귀결됩니다.&lt;/li&gt;
&lt;li&gt;Jenkins는 플러그인을 설치하면 Restart가 반드시 필요합니다. 이 과정에서 빌드중인 젠킨스 파이프라인이 존재한다면 CI/CD 과정에서 장애가 발생합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Jenkins를 Kubernetes에 설치하기로 하였는데요, 방법은 크게 2가지가 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Helm Chart로 Jenkins를 마음 편하게 설치한다. (물론 values.yaml을 커스텀하는게 불편하긴합니다)&lt;/li&gt;
&lt;li&gt;Jenkins 공식문서에 따라서 쿠버네티스 리소스를 할당한다 (이것도 사실 helm chart로 묶어서 관리할수는 있지만...귀찮아서 그렇게 하진 않았습니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말씀드리자면, helm chart로 설치하는게 실패해서 후자의 방법을 선택하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm chart로 Jenkins를 설치하게되면, Pod가 Initialize 단계에서 깨져버리는 현상이 발생하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677423776260&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cache miss for: update-center-2.375.2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 현상이 무엇인지 몰라 계속 구글링을 해보고, ChatGPT에게도 물어보았지만 하루 허탕만 치고 해결을 못했습니다. 그래서 그냥 공식문서 따라서 쿠버네티스 리소스를 정의해서 배포하는 것으로 마무리 하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Prometheus + Grafana를 통한 모니터링 환경 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Prometheus + Grafana를 통해서 모니터링 환경을 배포하였습니다. 이 과정도 생각보다 쉽지는 않았습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Prometheus가 AlertManager에 요청을 보냈으나, 계속 TCP Timeout이 뜨던 현상&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Prometheus + Grafana를 &lt;b&gt;prometheus-community/kube-prometheus-stack&lt;/b&gt; 이라는 helm chart를 통해서 배포를 진행하였는데요, 배포 직후에 그라파나 보드를 들어가서 대시보드를 조회하였으나, 계속 모니터링 지표들이&amp;nbsp;&lt;b&gt;No Data&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;그래서 프로메테우스의 로그를 조회해보았는데, AlertManager 주소로 요청을 보내는데 있어서 tcp timeout 현상이 뜨는것을 발견하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677424212285&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AlertManager: ClusterIP 10.0.1.172 dial tcp timeout&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 쿠버네티스의 모든 서비스가 같은 네임스페이스에 있기 때문에 통신이 가능할거라고 생각하였으나, 서비스에 대한 ClusterIP가 아닌 private dns를 통해서 요청을 보내고있는 것을 확인하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 timeout 현상을 해결하기 위해선 방화벽 규칙을 오픈해줘야하는데요, 이 현상에 대한 제일 최소한의 방화벽 규칙은 &lt;b&gt;Inbound/Outbound에 대해서 TCP 10.0.0.0/16 을 열어주는 것&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-27 오전 12.13.01.png&quot; data-origin-width=&quot;5118&quot; data-origin-height=&quot;2606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c30wGE/btr0NM1Amhh/bSFVVMfKX5ugkph1sOkV5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c30wGE/btr0NM1Amhh/bSFVVMfKX5ugkph1sOkV5k/img.png&quot; data-alt=&quot;지금은 매우 잘 된다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c30wGE/btr0NM1Amhh/bSFVVMfKX5ugkph1sOkV5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc30wGE%2Fbtr0NM1Amhh%2FbSFVVMfKX5ugkph1sOkV5k%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;5118&quot; height=&quot;2606&quot; data-filename=&quot;스크린샷 2023-02-27 오전 12.13.01.png&quot; data-origin-width=&quot;5118&quot; data-origin-height=&quot;2606&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;지금은 매우 잘 된다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. GitHub Links&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 마무리 지으면서 제가 사용하고있는 테라폼 코드들, 쿠버네티스 매니페스트들을 모아둔 레포지토리 주소를 공유하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/terraform-codes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/doccilabs/terraform-codes&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1677424508766&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - doccilabs/terraform-codes: Repository for terraform codes that im using now&quot; data-og-description=&quot;Repository for terraform codes that im using now. Contribute to doccilabs/terraform-codes development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/doccilabs/terraform-codes&quot; data-og-url=&quot;https://github.com/doccilabs/terraform-codes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bKrD5Y/hyRKUmJGsk/py1xNtKpzTJhV8OuL9vEt0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/terraform-codes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/doccilabs/terraform-codes&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bKrD5Y/hyRKUmJGsk/py1xNtKpzTJhV8OuL9vEt0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - doccilabs/terraform-codes: Repository for terraform codes that im using now&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Repository for terraform codes that im using now. Contribute to doccilabs/terraform-codes development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/kubernetes-utils&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/doccilabs/kubernetes-utils&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1677424522903&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - doccilabs/kubernetes-utils: Repository for kubernetes utils (k8s v1.24)&quot; data-og-description=&quot;Repository for kubernetes utils (k8s v1.24). Contribute to doccilabs/kubernetes-utils development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/doccilabs/kubernetes-utils&quot; data-og-url=&quot;https://github.com/doccilabs/kubernetes-utils&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BGS4Z/hyRKWLESBd/wuJnh3tlOq8PLRbdXilLTk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/doccilabs/kubernetes-utils&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/doccilabs/kubernetes-utils&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BGS4Z/hyRKWLESBd/wuJnh3tlOq8PLRbdXilLTk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - doccilabs/kubernetes-utils: Repository for kubernetes utils (k8s v1.24)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Repository for kubernetes utils (k8s v1.24). Contribute to doccilabs/kubernetes-utils development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;</description>
      <category>devOps</category>
      <category>EKS</category>
      <category>모니터링</category>
      <category>젠킨스</category>
      <category>쿠버네티스</category>
      <category>테라폼</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/8</guid>
      <comments>https://doccimann-memory-archive.tistory.com/8#entry8comment</comments>
      <pubDate>Mon, 27 Feb 2023 00:15:52 +0900</pubDate>
    </item>
    <item>
      <title>Elice AI트랙 1차 프로젝트 (밈팔이닷컴) 회고</title>
      <link>https://doccimann-memory-archive.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 1월 30일부터 2월 10일까지 약 2주 조금 안되는 시간동안 진행하였던 Elice AI 트랙의 1차 프로젝트에 대해서 시간 순서대로 회고를 해보고자합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 팀 결정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 결정 자체는 1월 20일에 결정되었습니다. 6인 팀으로 결정이 되었는데, 1명은 저와 같이 백엔드 스터디를 하던 분이었고, 나머지 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;우선 팀으로 결정된 사람들과 먼저 친해지는 것이 중요하다고 생각하였기 때문에 팀이 결정되자마자 서로 인사를 해보는 시간을 가졌던 것으로 기억합니다! 그렇게 설날을 쉬고 조금의 회복 기간을 가지다가 1월 30일에 본격적으로 프로젝트를 시작하게됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로젝트 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1월 30일에 본격적으로 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;개발 경험이 1도 없는 비전공자 출신들이었으며, 저희 팀 중에서 유일하게 저만 전공자였으며 백엔드 개발 경험이 거의 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;따라서 어떻게 포지션을 나눌지 결정한 결과, 팀 구성을 팀장 1명, 백엔드 리더 1명, 프론트 리더 1명을 두기로 결정합니다. 그리고 팀 포지션을 분배한 결과 저희 &lt;b&gt;5지는팀&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;팀장: 김도엽(나) -&amp;gt; 팀의 DevOps까지 담당&lt;/li&gt;
&lt;li&gt;백엔드 리더: 김도엽(나)&lt;/li&gt;
&lt;li&gt;프론트 리더: 정민님&lt;/li&gt;
&lt;li&gt;백엔드 총 3명, 프론트 총 3명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 저는 5지는팀 이라는 배를 모는 선장이 되었습니다. 위의 구성을 하고보니 백엔드의 경우 제가 개발 경험이 1년 정도 있었다보니 1주일 정도면 1차 프로젝트로 요구한 요구사항을 다 구현할 자신이 있었으나, 프론트는 그것을 보장할 수가 없어서 &lt;b&gt;일주일만에 백엔드를 끝내고 어떻게든 남은 기간에 프론트엔드를 도와주자&lt;/b&gt; 라는 마인드로 참여를 하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기술스택의 선정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 스택의 선정은 백엔드 위주로 설명을 해드리겠습니다. 프론트엔드의 경우 엘리스에서 가르쳐준 리액트, CSS, Styled Component가 전부이기 때문입니다.&lt;/p&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;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주문 수정 - 사용자는 주문 완료 후 배송이 시작되기 전까지 주문 정보를 수정할 수 있다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드는 저 혼자 하는것이 아닌, 3명이서 일감을 분배해서 진행을 해야하기 때문에 위의 2가지 요구사항을 보고 고민에 빠지게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;카테고리, 주문을 진행하면서 유저의 토큰을 검증하고, 토큰에 대해서 RBAC을 수행해야하는데, 내가 방법을 알려준다한들 이걸 개발경험이 1도 없는 나머지 두 분이 무사히 진행해줄 수 있을까?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 회원 서버와 그 외의 기능들을 수행하는 서버를 분리 구현하여서, 회원 인증/인가에 대해서 나머지 2분에 대해서 블랙박스로 처리하기로 결심을 하게됩니다. 즉, 나머지 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;ExpressJS, MongoDB&lt;/b&gt;로 구성을 하여 나머지 두 분에게 코딩에 대해서 전반적으로 맡기도록 하는 방향으로 가닥을 잡았고, 저의 경우는 프로젝트 진행 이전에 TypeScript, NestJS를 공부했기 때문에 NestJS로 토이프로젝트를 해보는 겸사로 한번 해보자는 마음으로 &lt;b&gt;TypeScript, NestJS&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;Docker, Docker-Compose&lt;/b&gt;로 하기로 결정하게됩니다. 물론 둘 다 NodeJS 위에서 돌아가는 프레임워크이기 때문에 Docker로 하지않고 PM2를 다이렉트로 써서 배포를 진행해도 상관은 없겠지만, 그래도 둘의 배포 방법은 엄연히 다르기도 한데다가, NestJS는 배포 과정에서 tsc 빌드 과정을 거치기 때문에 그거를 자동화 하기 위해서라도 Docker로 배포하기로 결심하게됩니다. (Dockerfile 적는데 시간이 얼마 안 걸리기도 하고...)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 본격적인 백엔드 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 컨셉, api 리스트업을 완료한 뒤에 1월 31일부터 본격적으로 시작하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 개발을 시작하는 첫 단추는 ExpressJS 뼈대 코드 작성, 즉 아키텍처링 이었습니다. ExpressJS의 아키텍처링은 저희 백엔드 팀의 나머지 두 분에게 전적으로 맡겨드리고 후에 아키텍처링에 대해서 제가 코드리뷰를 해주는 방식으로 진행하였습니다.&lt;/p&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;이거 3계층 아키텍처로 설계해주세요&lt;/b&gt; 했을 때 매우 막막해하였던 모습이 기억이 납니다. 그래서 레퍼런스를 하나 주고, 이거 비슷하게 클론코딩만 하면 아키텍처링 할수있어요! 라고 말을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/elice-bsg/express-layerd-architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;백설기-레이어드 아키텍처 예시&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676358179254&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - elice-bsg/express-layerd-architecture&quot; data-og-description=&quot;Contribute to elice-bsg/express-layerd-architecture development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/elice-bsg/express-layerd-architecture&quot; data-og-url=&quot;https://github.com/elice-bsg/express-layerd-architecture&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/YUQUr/hyRB0uk46g/C6JZrLL7eMxqtYhWaGfGH1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/elice-bsg/express-layerd-architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/elice-bsg/express-layerd-architecture&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/YUQUr/hyRB0uk46g/C6JZrLL7eMxqtYhWaGfGH1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - elice-bsg/express-layerd-architecture&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to elice-bsg/express-layerd-architecture development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;나머지 카테고리, 품목, 주문 백엔드 로직을 짜면서도 나머지 두 분에게 코드 작성을 맡기고, PR시에 코드 리뷰를 하고, 코드 리뷰 이후에 코드를 고쳐서 Dev 브랜치에 Merge를 하는 식으로 백엔드 개발을 진행하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 1주차 코드리뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 엘리스 AI트랙은 1주차에 지금까지 작성해온 코드에 대해서 코치님께서 리뷰를 진행해주십니다. 저희는 회원 서버, 그 외의 서버가 분리된 형태였기 때문에 코치님께서 2개의 리포지토리에 대해서 리뷰를 해주시는데, 우선 회원 서버에 대해서 리뷰가 먼저 올라왔습니다.&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;1412&quot; data-origin-height=&quot;1684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVawyR/btrZjJY0FGr/rZkSFHRWD9KmaPdD7lGmK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVawyR/btrZjJY0FGr/rZkSFHRWD9KmaPdD7lGmK1/img.png&quot; data-alt=&quot;코치님의 코드리뷰. 이 정도면 극찬이지 아니한가?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVawyR/btrZjJY0FGr/rZkSFHRWD9KmaPdD7lGmK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVawyR%2FbtrZjJY0FGr%2FrZkSFHRWD9KmaPdD7lGmK1%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;1412&quot; height=&quot;1684&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;1684&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;코치님의 코드리뷰. 이 정도면 극찬이지 아니한가?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물품 서버에 대해서는 2주차 첫번째 오피스아워에서 리뷰를 해주셨는데요, 크게 코드 내용으로 지적(?)을 받은건 없으나, ExpressJS또한 결국 나머지 두 분이 코드를 작성하시면 그것을 제가 리뷰하여 PR을 날리다보니 아래의 지적 사항이 나왔습니다.&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;결국 프로젝트를 평가하는 사람들은 팀원 전체의 Commit 수도 보기 때문에, MR마다 코드리뷰를 해주는건 좋지만 MR은 나머지 두 팀원이 하도록 해주셨으면 좋겠다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 프론트에서의 SOS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 2주차 월요일까지 백엔드 코딩은 모두 끝이 났습니다. 하지만 프론트엔드의 경우 진척도가 그렇게 많이 나오지 않던 상황인데요, 화요일 즈음에 스크럼을 진행하면서, 그리고 코치님의 오피스아워 시간에 이러한 결론에 도달하였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;백엔드가 거의 마무리 되어간다면, 백엔드 분들은 모두 프론트를 도와드리는게 어떨까요?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 화요일 저녁부터 시작해서 프론트 분들을 도와주는 최선의 방법을 물색하기 시작하였는데요, 제가 선택한 방법은 &lt;b&gt;프론트엔드의 Repository를 백엔드 측에서 모두 작성해줘서 프론트가 백엔드 api를 호출하는걸 간단화 시키는 것&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;그래서 Axios를 이용한 백엔드 API 호출을 Repository 계층을 이용해서 추상화 시킨 다음, 프론트 분들에게 알려주고, 프론트분들이 시간이 부족해서 못하는 뷰와 컴포넌트 부분들을 백엔드 분들이 맡아주는 방법으로 하여 1차 프로젝트를 무사히 마무리 하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 프로젝트 발표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 대망의 프로젝트 발표입니다. 발표자료는 팀원 분들이 다 같이 만들고나서, 발표는 제가 맡게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2518&quot; data-origin-height=&quot;1446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvAAhL/btrZiWdEuWk/f9VzkkmNQ2HLxRifVRfhiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvAAhL/btrZiWdEuWk/f9VzkkmNQ2HLxRifVRfhiK/img.png&quot; data-alt=&quot;이거를 최대한 생동감있게 발표를 해달라길래....매우 난감했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvAAhL/btrZiWdEuWk/f9VzkkmNQ2HLxRifVRfhiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvAAhL%2FbtrZiWdEuWk%2Ff9VzkkmNQ2HLxRifVRfhiK%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;2518&quot; height=&quot;1446&quot; data-origin-width=&quot;2518&quot; data-origin-height=&quot;1446&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이거를 최대한 생동감있게 발표를 해달라길래....매우 난감했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 이후에 QnA 과정에서 간단하게 두 개의 질문만을 다른 코치님께 받았는데요, 제가 받았던 질문은 아래와 같았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;혹시 팀에 현업자가 있었나요? 개발 환경이 저랑 많이 비슷하던데...&lt;/li&gt;
&lt;li&gt;굳이 서버를 NestJS만으로 구성할 수도 있었는데 굳이 왜 ExpressJS랑 병행한 이유가 있으실까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 질문에 대한 답은 제가 위에서 말했던 사항과 어느 정도 통하는데요, 이유는 &lt;b&gt;저희 팀의 백엔드는 저를 제외하고는 Express도 겨우 하던 사람들이었기 때문에 제가 NestJS로 전부 수행을 하게된다면 나머지 두 분들에게 배움의 기회, 코딩의 기회를 앗아가는 나쁜 행동이기 때문이었습니다.&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;모든 개발은 협업으로 이뤄져야만이 최선의 결과를 가져올 수가 있기 때문에, 다른 인원들의 개발 수준을 차차하고서라도, 그리고 제가 모든 코드를 가르쳐주는 한이 있더라도 나머지 서버는 Express로 하는 방향으로 잡았던 이유도 있긴합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 프로젝트를 하며 느낀점이 있다면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이번 팀 프로젝트를 하기 이전에, 다른 분에게 이런 말을 들은적이 있었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;팀에 불화가 안 일어나게 팀장이 이끌어나가는거는, 단순하게 팀장인 너의 실력만 뛰어나서는 절대로 안돼. 팀원을 배려하고, 그리고 팀원의 장점을 최대한 보려하고 칭찬하는 문화를 너가 직접 이끌어나가야만이 팀에 불화가 안 일어나고 최선의 결과를 얻을 수 있지 않을까?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 1차 팀 프로젝트를 하기 이전에 이 말을 새겨듣고, 또 새겨들으면서 &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;/p&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주간 활동할 수 있었던데에는 저뿐만이 아닌, 팀원들의 몫이 대부분이었을 것입니다. 제가 팀의 문화를 잘 이끌어간다고 한들, 제가 형성하고자하는 팀의 문화를 받아들일 수 없는 팀원이 존재했다면 말짱 도루묵이었기 때문입니다.&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;마지막으로, 저와 2주 동안 함께 해주었던 팀원들에게 감사하다는 말을 전하며 글을 마치겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. Repositories&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/BrianDYKim/memepari-3A&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/BrianDYKim/memepari-3A&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676360343235&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - BrianDYKim/memepari-3A: 밈팔이닷컴 3A (Authentication/Authorization/Access-Control) 관련 리포지토리입니&quot; data-og-description=&quot;밈팔이닷컴 3A (Authentication/Authorization/Access-Control) 관련 리포지토리입니다. - GitHub - BrianDYKim/memepari-3A: 밈팔이닷컴 3A (Authentication/Authorization/Access-Control) 관련 리포지토리입니다.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/BrianDYKim/memepari-3A&quot; data-og-url=&quot;https://github.com/BrianDYKim/memepari-3A&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mhzIb/hyRBM3Wzuh/kXlwO4GF9HgHQP3hat7l8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/BrianDYKim/memepari-3A&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/BrianDYKim/memepari-3A&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mhzIb/hyRBM3Wzuh/kXlwO4GF9HgHQP3hat7l8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - BrianDYKim/memepari-3A: 밈팔이닷컴 3A (Authentication/Authorization/Access-Control) 관련 리포지토리입니&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;밈팔이닷컴 3A (Authentication/Authorization/Access-Control) 관련 리포지토리입니다. - GitHub - BrianDYKim/memepari-3A: 밈팔이닷컴 3A (Authentication/Authorization/Access-Control) 관련 리포지토리입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/BrianDYKim/memepari-item&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/BrianDYKim/memepari-item&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676360357301&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - BrianDYKim/memepari-item: 밈팔이닷컴 카테고리/품목/주문 서버입니다&quot; data-og-description=&quot;밈팔이닷컴 카테고리/품목/주문 서버입니다. Contribute to BrianDYKim/memepari-item development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/BrianDYKim/memepari-item&quot; data-og-url=&quot;https://github.com/BrianDYKim/memepari-item&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AeUxP/hyRBTILYbf/HTxxHjEimwXHIdFO9uxYck/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/BrianDYKim/memepari-item&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/BrianDYKim/memepari-item&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AeUxP/hyRBTILYbf/HTxxHjEimwXHIdFO9uxYck/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - BrianDYKim/memepari-item: 밈팔이닷컴 카테고리/품목/주문 서버입니다&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;밈팔이닷컴 카테고리/품목/주문 서버입니다. Contribute to BrianDYKim/memepari-item development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;</description>
      <category>주저리주저리</category>
      <category>엘리스 AI트랙</category>
      <category>팀프로젝트</category>
      <author>Brian, Doccimann</author>
      <guid isPermaLink="true">https://doccimann-memory-archive.tistory.com/7</guid>
      <comments>https://doccimann-memory-archive.tistory.com/7#entry7comment</comments>
      <pubDate>Tue, 14 Feb 2023 16:39:49 +0900</pubDate>
    </item>
  </channel>
</rss>