<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>윤자이 기술블로그</title>
    <link>https://ooeunz.tistory.com/</link>
    <description>개발자로 살길 정말 잘했다.</description>
    <language>ko</language>
    <pubDate>Sun, 17 May 2026 21:52:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ooeunz</managingEditor>
    <image>
      <title>윤자이 기술블로그</title>
      <url>https://t1.daumcdn.net/cfile/tistory/991968405D9D510C11</url>
      <link>https://ooeunz.tistory.com</link>
    </image>
    <item>
      <title>2020, 2021 2년 간의 짧은 회고</title>
      <link>https://ooeunz.tistory.com/151</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2022년을 맞아서 지난 2021년을 회고하고자 오랜만에 블로그에 글을 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 2021년이라고 이야기 했지만 2020년 회고글을 따로 적지 않았기 때문에 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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_5240.JPG&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;2560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VuciJ/btrrqbWASyN/ibFFXJOin12RHJppwBZoYK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VuciJ/btrrqbWASyN/ibFFXJOin12RHJppwBZoYK/img.jpg&quot; data-alt=&quot;점심먹고 오피스 돌아가는 길에...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VuciJ/btrrqbWASyN/ibFFXJOin12RHJppwBZoYK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVuciJ%2FbtrrqbWASyN%2FibFFXJOin12RHJppwBZoYK%2Fimg.jpg&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;400&quot; height=&quot;533&quot; data-filename=&quot;IMG_5240.JPG&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;2560&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이직&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2020년 여름 나는 우리나라에서 가장 큰 IT기업 중 한 곳에 신입으로 입사했다. 내 블로그에서 가장 잘 팔리는 글 중 하나인 &lt;a href=&quot;https://ooeunz.tistory.com/108&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;그것도 꽤 오래전에 이직했다. 약 8개월? 쯤 전의 시기였던 것 같다. 전 회사에 있었던 시기로 치자면 약 11개월 정도로 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;사실 IT 직종이 이직이 잦긴하지만 이렇게 빨리 이직을 하진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 신입으로 들어가서 3년차 쯤 이직하는 게 국룰이다. 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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 어디로 이직했냐고?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TOSS&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CzVYl/btrrq01ZeKR/P29LAc6LQIT25ZZ4SPq0B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CzVYl/btrrq01ZeKR/P29LAc6LQIT25ZZ4SPq0B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CzVYl/btrrq01ZeKR/P29LAc6LQIT25ZZ4SPq0B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCzVYl%2Fbtrrq01ZeKR%2FP29LAc6LQIT25ZZ4SPq0B1%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;300&quot; height=&quot;77&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스는 사실 내게 인연이 있는 회사다. 취준하던 쪼무래기 시기에 처음으로 가본(?) 회사이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2019년 말이었던가 그 당시에 속해있던 IT 동아리에 이전에 동아리를 수료하셨던 선배님이 멘토링을 와주신 적이 있다.&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;당시 선배님은 마지막에 그냥 하신 말씀이실지 모르겠지만... 나는 옳거니 하고 연락을 드렸고 몇몇 동아리 친구들을 모아서 2019년 말에 토스 견학을 갔었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 당시 내 기억에 토스는... 정말 너무 멋진 곳이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업계 최고의 인재들을 모으고 그 인재들에게 최고의 보상을 해주고 실패하는 것을 두려워하지 않는 문화.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2022-01-24-14-39-53.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6SVB1/btrrq0OusgF/Njj6tfmFwHRzR1paUuygPK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6SVB1/btrrq0OusgF/Njj6tfmFwHRzR1paUuygPK/img.jpg&quot; data-alt=&quot;2019년 12월의 토스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6SVB1/btrrq0OusgF/Njj6tfmFwHRzR1paUuygPK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6SVB1%2Fbtrrq0OusgF%2FNjj6tfmFwHRzR1paUuygPK%2Fimg.jpg&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;400&quot; height=&quot;400&quot; data-filename=&quot;KakaoTalk_Photo_2022-01-24-14-39-53.jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2019년 12월의 토스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이건 여담인데... 그 날 얻었던 인사이트를 잊고 싶지 않아서 집에 돌아가자마자 에버노트에 기록해뒀었다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;그리고 보물처럼 나만보고 싶어서 블로그에도 일부러 올리지 않았던 걸로 기억한다. ㅋㅋㅋㅋㅋ &lt;/i&gt;&lt;i&gt;그리고 오늘 이 글을 쓰면서 그때 쓴 글을 찾아봤는데... 없어졌다 ㅎ&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이래서 지식과 인사이트는 나눌수록 배가 되나보다 ^^...&amp;nbsp;&lt;/i&gt;&lt;/p&gt;
&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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주저리주저리 내 옛날이야기가 길어졌는데... 그래서 나는 왜 이직을 했을까?&lt;/p&gt;
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 취직했던 때가 아직도 눈에 선명하다. 암막커튼 쳐진 어두컴컴한 자취방에서 침대에서 뒹굴거리면서 한참만에 핸드폰을 확인했을 때&lt;/p&gt;
&lt;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;이 회사에서 성장할 내 모습을 생각하니 너무 짜릿하고 즐거웠다.&lt;/p&gt;
&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;주니어 때 쌓은 경험이 향후 10년을 바꾼다고 생각하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 전 회사 생활은 멋진 동료와 워라벨이 가득했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즐겁진 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 개발하는데에서 오는 즐거움이 큰 사람이다. 계속 뭔가 탐구하고 싶고 시도해보고 싶었고 그런 내게 워라벨이 가득한 문화는 오히려 독이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해커톤처럼 일하고 싶었다. 모든 팀원이 하나의 목표에 align 되고, 팀간의 이해관계를 따지지 않고 공동의 목표에 기여하며 풀리지 않는 문제로 밤새 고민하고 성취감을 맛보고 싶었다. 그리고 그 끝에 내가 좀 더 나은 엔지니어가 되어 있길 바랬다.&lt;/p&gt;
&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;그러던 중 토스에 다니고 있는 친구에게 연락이 왔다. 토스 이야기를 종종 해주는데 이 회사의 문화가 나의 고민에 대해 항상 yes라고만 답하는 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 토스에 합류하게 된 계기는 다시 한번 나를 돌아보는 계기가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 토스에서 최종 합격 오퍼를 받았을 때 나는 오퍼레터를 거절했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 이유가 있었지만 아무리 높은 보상이어도 지금 누리고 있는 워라벨과 비교하면 바꿀 수 없는 가치가 아닐까?라는 생각을 했었던 것 같다. 참 이상하지 않은가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 분명 개발하는 데에 즐거움을 느끼는 사람이었는데... 왜 내가 그런 생각을 했던 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 오퍼레터를 거절하고 일주일이 되기 하루 전 그 이유를 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 편한 이 생활에 익숙해지고 있었다. 100% 풀 재택. 여유로운 업무량. 나를 서포트해줄 수 있는 다양한 팀원.&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;취준을 하던 시절 내가 자체 서비스를 하는 회사만을 지원했던 건 평생을 실력으로 나를 증명하는 삶을 살고 싶었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오퍼레터를 거절한 지 일주일이 지났지만... 당시 나를 담당했던 토스 인사 담당자분께 다시 연락을 드렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 지금이라도 다시 입사할 수 있을까요...?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;나의 두 번째 직장&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 앞에서 내가 중고 신입으로 다시 들어왔다고 말한 이유를 이제 이야기해볼까 한다. 전 직장에서 나는 서버 개발자로 취직했지만 회사를 다니던 중 서버 개발 코드를 짰던 기억이 손에 꼽혔다.&lt;/p&gt;
&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;처음 토스를 들어왔을 때 내 개발 실력을 객관적으로 측정하자면 취준을 했던 그 시기의 수준이었다.&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;나의 토스 적응기는 생존의 연속이었다. 문제를 해결해 나가기보다 따라가기 급급했고 그때 &quot;내가 개발자를 업으로 삼은 게 맞는 길이었던 것인가?&quot;라는 회의마저 들었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 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;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;스크린샷 2022-01-27 오전 2.23.02.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4vAv2/btrrPOziMAc/vdJCIcBAXNdJ75K0oQWkG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4vAv2/btrrPOziMAc/vdJCIcBAXNdJ75K0oQWkG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4vAv2/btrrPOziMAc/vdJCIcBAXNdJ75K0oQWkG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4vAv2%2FbtrrPOziMAc%2FvdJCIcBAXNdJ75K0oQWkG0%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;998&quot; height=&quot;666&quot; data-filename=&quot;스크린샷 2022-01-27 오전 2.23.02.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 2, 3위 사업자의 수치를 합친 것보다 성공했던 국민 지원금 서비스는 토스 MAU의 새로운 상방을 뚫어냈다.&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;토스에서 가장 트래픽이 많았던 기존의 계좌 / 카드 조회 서비스를 fade out 시킴과 동시에 마이데이터로 전환해야하는 난이도 높은 프로젝트였다. 토스 전사적으로도 굉장히 중요도가 높은 프로젝트였지만, 우리나라 전체 금융사가 참여하는 국가 단위 프로젝트였기 때문에 무게와 책임감이 남달랐다.&lt;/p&gt;
&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;눈앞의 테스크를 어떻게 하면 더 나이스 한 방법으로 해결할 수 있을지 매 순간 고민했다.&lt;/p&gt;
&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;술잔을 부딪치면서 승건님이 했던 말씀이 머리에 계속 맴돌았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;우리가 이렇게 뜨거웠던 적이 있었을까? 아마 앞으로도 없을 것 같아.&quot; (이제 토스가 너무 커졌기 때문에)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가치&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2022-01-27-01-05-46.jpeg&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;1152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daeX5f/btrrPFI1D7V/LVAidNgQiqQzHqyzaLqjak/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daeX5f/btrrPFI1D7V/LVAidNgQiqQzHqyzaLqjak/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daeX5f/btrrPFI1D7V/LVAidNgQiqQzHqyzaLqjak/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaeX5f%2FbtrrPFI1D7V%2FLVAidNgQiqQzHqyzaLqjak%2Fimg.jpg&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;400&quot; height=&quot;400&quot; data-filename=&quot;KakaoTalk_Photo_2022-01-27-01-05-46.jpeg&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;1152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년을 맞으면서 내 삶의 새로운 가치에 대해서 끊임없이 고민하고 있다. 커리어적으로도 고민이 크고 아직 가야 할 길이 많지만 커리어가 내 삶의 전부이길 바라진 않기 때문이다. 언젠가 일을 그만두고 은퇴했을 때 나의 삶을 지켜줄 가치. 그리고 단기간 또는 장기간의 목표를 성취하였을 때 허무함이 아니라 계속해서 삶을 전진할 수 있는 삶의 철학을 계속해서 찾고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2018년 여름, 전공을 바꾸기로 결심하며 일기에 다짐하듯 적어뒀던 말이 하나 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;무슨 일을 하든 예술가로 살자&quot;&lt;/b&gt;&lt;/p&gt;
&lt;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;그렇기에 이번 2022년은 내 가치를 찾는 한 해가 되었으면 한다. 그리고 나는 끝내 찾아낼 2022년이 몹시 기대가 된다.&lt;/p&gt;</description>
      <category>Forum/Retrospect</category>
      <category>회고</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/151</guid>
      <comments>https://ooeunz.tistory.com/151#entry151comment</comments>
      <pubDate>Thu, 27 Jan 2022 01:15:15 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Reactive programing: WebFlux, WebClient</title>
      <link>https://ooeunz.tistory.com/150</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 비동기 처리를 하게 될 경우 non blocking 하게 servlet thread를 사용하는 방법에 대해서 살펴보았습니다. 하지만 여전히 문제가 남아있는 부분이 존재합니다. 바로 비동기 처리를 하는 worker thread입니다. worker thread가 만약 또 다른 서비스의 API를 호출하게 된다면 servlet thread는 반환되었지만 worker thread는 api 응답이 올 때까지 blocking 되어 대기상태가 되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 4.0에선 이러한 문제를 AsyncRestTemplate을 사용해서 해결할 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632629199486&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1632628929993&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val asyncRestTemplate = AsyncRestTemplate(Netty4ClientHttpRequestFactory(NioEventLoopGroup(1)))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;AsyncRestTemplate을 사용할땐 반드시 Netty를 이용해주어야 합니다. 그렇지 않으면 non blocking io를 사용하지만 io가 발생할 때마다 새로운 thread를 생성하는 방식으로 문제를 해결하게 됩니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AsyncRestTemplate은 기존의 RestTemplate과 동일한 사용방법으로 사용할 수 있지만 현재 Depreated 상태입니다. Spring5에선 AsyncRestTemplate의 대안으로 WebClient라는 http client를 사용할 것을 권하고 있습니다. WebClient는 내부적으로 non bloking io 라이브러리인 Netty를 사용하고 있으며 reactive 방식을 채택하고 있습니다. WebClient의 사용방법을 알아보기 전에 Reactive programing이란 무엇인지부터 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactive란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive하다는 것은 외부에서 어떠한 event가 발생했을 때 그에 대응하는 방식으로 코드를 작성하는 프로그래밍 패러다임입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632632741060&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val iterable = listOf(1, 2, 3, 4, 5).iterator()
while (iterable.hasNext()) {
	val result = iterable.next()
    println(&quot;result=$result&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;206&quot; data-filename=&quot;스크린샷 2021-09-26 오후 3.09.03.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8ziq/btrfWJQctCx/m7r5eDIaEK4y4wzf3SgrE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8ziq/btrfWJQctCx/m7r5eDIaEK4y4wzf3SgrE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8ziq/btrfWJQctCx/m7r5eDIaEK4y4wzf3SgrE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8ziq%2FbtrfWJQctCx%2Fm7r5eDIaEK4y4wzf3SgrE0%2Fimg.png&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;206&quot; data-filename=&quot;스크린샷 2021-09-26 오후 3.09.03.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 Iterator의 next() 메서드는 data를 pull 해오는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 reactive programing의 기초가 되었던 90년대에 나온 디자인 패턴인 observable pattern을 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 현재 Observableimplements는 현재 deprecated 되었기 때문에 java코드를 사용하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632633051904&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class IntegerObservable extends Observableimplements, implements Runable {
	@Override
	public void run() {
		for (int i = 0; i&amp;lt; 1= 10; i++) {
				setChanged();
				notifyObservers(i);  // iterator.next()와 달리 push하는 방식
		}
	}
}

class ObservablePattern() {
	public static void main(String[] args) {
		Observer ob = new Observer() {
			@Override
			public void update(Observable o, Object arg) {
				System.out.println(arg);
			}
		}

		IntegerObservable io = new IntegerObservable();
		io.addObserver(ob);

		io.run();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 IntegerObservable은 하나의 source이며 특정 시점에 notifyObservers() 메서드를 이용해서 event를 던지게 됩니다. (Data를 Push) 그리고 이러한 observable의 event를 관찰하고 있는 것이 바로 Observer(관찰자)이며, Observable은 addObserver() 메서드를 이용해서 Observer를 등록하게 됩니다.&lt;/p&gt;
&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;Observable에서 만들어내는 event는 하나의 observer만 등록할 수 있는 게 아니라 관심이 있는 다른 observer들이 여러 개 등록하여 멀티캐스트로 event를 전파할 수 있다는 장점이 있습니다.&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;하지만 observable 패턴은 한계를 가지고 있습니다. event가 종료되었다는 complete를 알려줄 수 있는 방법이 없고, Exception을 어떤 식으로 전파할 것이고, 받은 예외를 어떻게 처리할 것인지 등에 간한 아이디어가 패턴에 녹아져 있지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive를 처음 만든 Microsoft 엔지니어들은 수많은 server와 client가 통신하는 현재의 상황에는 이러한 90년대 초에 나온 observable 패턴을 그대로 사용하기에는 한계가 있다고 판단하여 기존의 observable 패턴에 부족한 2가지를 새롭게 추가해서 확장된 observable 패턴을 개발하게 됩니다.&lt;/p&gt;
&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;reactive&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactive Streams&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive 진영에는 크게 두가지 큰 그룹이 존재합니다. 하나는 microsoft에서 시작해서 netflix에서 완성된 reactive x, 그리고 다른 하나는 spring에서 만든 reactor입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1632633674235&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;ReactiveX&quot; data-og-description=&quot;CROSS-PLATFORM Available for idiomatic Java, Scala, C#, C++, Clojure, JavaScript, Python, Groovy, JRuby, and others&quot; data-og-host=&quot;reactivex.io&quot; data-og-source-url=&quot;http://reactivex.io/&quot; data-og-url=&quot;http://reactivex.io/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;http://reactivex.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://reactivex.io/&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;ReactiveX&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CROSS-PLATFORM Available for idiomatic Java, Scala, C#, C++, Clojure, JavaScript, Python, Groovy, JRuby, and others&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;reactivex.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두가지 그룹은 모두 reactive-streams라는 하나의 표준을 지켜서 개발되었습니다. 그렇기 때문에 reactiveX와 reactor는 어느 정도 호환이 가능합니다. 그렇다면 reactive-streams는 어떤 표준을 정의하였을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reactive-streams-jvm을 보면 표준 프로토콜을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1632633807054&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 - reactive-streams/reactive-streams-jvm: Reactive Streams Specification for the JVM&quot; data-og-description=&quot;Reactive Streams Specification for the JVM. Contribute to reactive-streams/reactive-streams-jvm development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reactive-streams/reactive-streams-jvm&quot; data-og-url=&quot;https://github.com/reactive-streams/reactive-streams-jvm&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dpRlMT/hyLKjLRgUl/I7WGs2fuzMTB11NbsscKak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/reactive-streams/reactive-streams-jvm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/reactive-streams/reactive-streams-jvm&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dpRlMT/hyLKjLRgUl/I7WGs2fuzMTB11NbsscKak/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 - reactive-streams/reactive-streams-jvm: Reactive Streams Specification for the JVM&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Reactive Streams Specification for the JVM. Contribute to reactive-streams/reactive-streams-jvm 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;아래는 reactive-streams-jvm의 문서를 발췌한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 reactive에선 observable의 객체를 다음과 같은 이름으로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher &amp;larr; Observable&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscriber &amp;larr; Observer&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;562&quot; data-filename=&quot;스크린샷 2021-09-26 오후 2.24.22.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6pGVd/btrfTLPfJVJ/ANGzRHw5WD8WiD7gybl7B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6pGVd/btrfTLPfJVJ/ANGzRHw5WD8WiD7gybl7B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6pGVd/btrfTLPfJVJ/ANGzRHw5WD8WiD7gybl7B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6pGVd%2FbtrfTLPfJVJ%2FANGzRHw5WD8WiD7gybl7B1%2Fimg.png&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;562&quot; data-filename=&quot;스크린샷 2021-09-26 오후 2.24.22.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략하게 요약하면 subscribe가 될때 onSubscribe() 메서드가 필수적으로 호출되어야 하며, 이후에 onNext() 메서드가 1번 또는 N 번까지 호출될 수 있으며, 이후에 onError() 혹은 onComplete()가 배타적으로 호출될 수 있다는 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observable pattern의 Observable.addObserver(Observer)처럼 reactive에서도 &lt;b&gt;Publisher.subscribe(Subscriber)&lt;/b&gt;와 같은 형태로 subscriber는 publisher를 구독할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632634720603&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    val publisher = object : Publisher&amp;lt;Int&amp;gt; {
        override fun subscribe(s: Subscriber&amp;lt;in Int&amp;gt;) {
            s.onSubscribe(subscription)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 onSubscribe() 호출시에 반드시 parameter로 Subscription이라는 객체를 넘겨주어야 하는데요. 여기서 subscription이란 publisher와 subscriber사이에서 중계를 해주는 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;subscriber는 subscription을 통해서 데이터를 요청하고, publisher는 data를 push 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;말하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-token-index=&quot;1&quot; data-reactroot=&quot;&quot;&gt;요청&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이란 위에서 나온 iterable의 next() 메서드 같이 pull 방식으로 데이터를 요청하는 메서드가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-token-index=&quot;3&quot; data-reactroot=&quot;&quot;&gt;backpressure&lt;/span&gt;&lt;span&gt;&amp;nbsp;&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;Backpressure&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backpressure란 publisher와 subscriber 사이에 속도차가 발생하는 경우 이를 조정할 수 있도록 하는 것입니다. (실제로 data를 보내주는 건 publisher가 onNext() 메서드를 이용하여 보내주게 됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bakpresssure가 필요한 이유는 publisher가 data를 생산하는 속도에 비해 subscriber가 속도가 느린 경우 이를 조절하기 위해 필요하게 됩니다. 예를 들어 publisher가 데이터가 100만 개가 있고, subscirber가 1초에 한 개의 data만 처리할 수 있다면 subscriber가 모든 데이터를 처리하기까지 100만 초가 필요하게 됩니다. 그 사이에 데이터는 어떻게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 data의 leak이 발생하거나 아주 큰 양의 buffer가 필요하게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 data의 생성 속도 자체를 늦추는 것이 도움이 되기 때문에 subscriber가 수용할 수 있는 정도의 데이터를 backpressure로 요청하여 데이터의 양을 조절하게 됩니다. 예를 들면 아래와 같이 구현할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632635421732&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    val publisher = object : Publisher&amp;lt;Int&amp;gt; {
        val iter = listOf(1, 2, 3, 4, 5).iterator()

        override fun subscribe(s: Subscriber&amp;lt;in Int&amp;gt;) {
            s.onSubscribe(
                object : Subscription {
                    override fun request(n: Long) {
                        try {
                            var idx = 0
                            while (idx++ &amp;lt; n) {
                                if (iter.hasNext()) {
                                    s.onNext(iter.next())
                                    idx++
                                } else {
                                    s.onComplete()
                                }
                            }
                        } catch (e: Exception) {
                            s.onError(e)
                        }
                    }
                    override fun cancel() {}
                }
            )
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 깊이가 깊어져서 한눈에 알아보기 힘들지만 조금 뜯어보면 subscription에 backpressure로 들어온 수만큼 count를 하며 data를 push 해주고, 만약 모든 data를 push 하였다면 subscriber의 onComplete() 메서드를, 중간에 exception이 발생했다면 onError() 메서드를 호출해주도록 하는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 subscriber는 어떤 식으로 구현되어야 할까요? 이것 역시 간단하게 구현해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632635854953&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    val subscriber = object : Subscriber&amp;lt;Int&amp;gt; {
        lateinit var subscription: Subscription

        override fun onSubscribe(s: Subscription) {
            subscription = s
            subscription.request(1)
        }

        override fun onNext(t: Int?) {
            subscription.request(1)
        }

        override fun onError(t: Throwable) {
            println(t.message)
        }

        override fun onComplete() {}
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onSubscribe가 실행될 때 parameter로 받는 subscription 객체를 저장해 두고 이후에 onNext가 호출될 때마다 subscription 객체의 request를 이용해서 data를 요청하게 됩니다. (현재는 간략하게 test code로써 backpressure에 1개의 data만 요청하도록 해두었지만 실제론 buffer를 두어서 buffuer의 크기를 반을 유지하면서 데이터를 요청한다고 함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 그렇다면 최종적으로 다시 한번 실행 순서를 확인해보고 puslisher에 subscriber를 구독까지 추가하여 실행하는 코드를 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1866&quot; data-filename=&quot;reactivestreams1-10 (1).png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFDfVs/btrfXyaedNB/QYQJSkyH7xOTajA8gbckS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFDfVs/btrfXyaedNB/QYQJSkyH7xOTajA8gbckS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFDfVs/btrfXyaedNB/QYQJSkyH7xOTajA8gbckS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFDfVs%2FbtrfXyaedNB%2FQYQJSkyH7xOTajA8gbckS1%2Fimg.png&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1866&quot; data-filename=&quot;reactivestreams1-10 (1).png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;p.subscribe(s)에서 subcriber는 publisher를 구독하게 됩니다.&lt;/li&gt;
&lt;li&gt;subscribe() 메서드에선 필수적으로 parameter로 들어온 subscriber의 onSubscribe() 메서드를 호출하게 됩니다.&lt;/li&gt;
&lt;li&gt;이때 onSubscribe() 메서드에 parameter로 Subscription 객체를 넣어주게 됩니다.&lt;/li&gt;
&lt;li&gt;subscription 객체는 publisher와 subscriber의 중계 역할을 하는 object로 backpressure 기능을 가지고 있습니다.&lt;/li&gt;
&lt;li&gt;subscription은 request를 이용해 backpressure를 &lt;b&gt;요청&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;subscription의 request는 내부적으로 backpressure로 들어온 long의 수만큼 data를 push 해주게 됩니다.&lt;/li&gt;
&lt;li&gt;이때 만약 더 이상 data가 없다면 onComplete()를 호출하고, exception이 발생한다면 onError()를 호출하게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1632636599595&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main(args: Array&amp;lt;String&amp;gt;) {
    val publisher = object : Publisher&amp;lt;Int&amp;gt; {
        val iter = listOf(1, 2, 3, 4, 5).iterator()

        override fun subscribe(s: Subscriber&amp;lt;in Int&amp;gt;) {
            s.onSubscribe(
                object : Subscription {
                    override fun request(n: Long) {
                        try {
                            var idx = 0
                            while (idx++ &amp;lt; n) {
                                if (iter.hasNext()) {
                                    s.onNext(iter.next())
                                    idx++
                                } else {
                                    s.onComplete()
                                }
                            }
                        } catch (e: Exception) {
                            s.onError(e)
                        }
                    }
                    override fun cancel() {}
                }
            )
        }
    }

    val subscriber = object : Subscriber&amp;lt;Int&amp;gt; {
        lateinit var subscription: Subscription

        override fun onSubscribe(s: Subscription) {
            println(&quot;onSubscribe()&quot;)
            subscription = s
            subscription.request(1)
        }

        override fun onNext(t: Int) {
            println(&quot;onNext(): $t&quot;)
            subscription.request(1)
        }

        override fun onError(t: Throwable) {
            println(&quot;onError()&quot;)
            println(t.message)
        }

        override fun onComplete() {
            println(&quot;onComplete()&quot;)
        }
    }

    publisher.subscribe(subscriber)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;298&quot; data-filename=&quot;스크린샷 2021-09-26 오후 3.10.15.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r8RLS/btrf1iEHO94/ikap20mAMgKcpj0iQvRRq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r8RLS/btrf1iEHO94/ikap20mAMgKcpj0iQvRRq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r8RLS/btrf1iEHO94/ikap20mAMgKcpj0iQvRRq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr8RLS%2Fbtrf1iEHO94%2Fikap20mAMgKcpj0iQvRRq0%2Fimg.png&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;298&quot; data-filename=&quot;스크린샷 2021-09-26 오후 3.10.15.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 pull 방식으로 했던 것과 같은 결괏값이 출력되는 것을 확인할 수 있습니다. 이와 같이 pull 방식과 push 방식처럼 표현 방법은 다르지만 결과는 같은 것을 duality(상대성: 궁극적으로 기능은 같지만 반대 방향으로 표현하는 것)라고 부르게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebFlux&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 reactive programing이 어떤 식으로 동작하는지 살펴보았으니까 WebFlux에 대해서 간략하게 언급하고 가도록 하겠습니다. 우리는 앞의 게시물에서 CompletableFuture를 이용해서 callback이 실행될 때 spring에서 servlet thread를 다시 불러와서 return 해주는 것을 확인했었습니다. 그렇다면 reactive는 spring에서 어떻게 활용할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632637135947&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/reactive&quot;)
fun reactive(): Mono&amp;lt;String&amp;gt; {
	return Mono.just(&quot;This is reactive&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 Mono 혹은 Flux type을 return 하면 됩니다. 여기서 Mono와 Flux란 reactor에서 사용하는 reactive 객체로써 구현 객체를 타고 들어가면 우리가 위에서 확인했던 Publisher를 구현하고 있습니다. Spring에선 이러한 Publisher의 구현 객체를 controller 단에서 return 해주면 알아서 subscribe를 해주게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;798&quot; data-filename=&quot;스크린샷 2021-09-26 오후 3.19.44.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJbto8/btrfWNeopSs/81MxhdaFDlApyBG3553AMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJbto8/btrfWNeopSs/81MxhdaFDlApyBG3553AMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJbto8/btrfWNeopSs/81MxhdaFDlApyBG3553AMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJbto8%2FbtrfWNeopSs%2F81MxhdaFDlApyBG3553AMk%2Fimg.png&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;798&quot; data-filename=&quot;스크린샷 2021-09-26 오후 3.19.44.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebFlux는 기존의 spring mvc의 많은 핵심 component들을 공유하고 있기 때문에 기존의 mvc 스타일을 코드와 거의 유사하게 코드를 작성할 수 있습니다. 때문에 코드만으론 spring mvc와 webflux를 구분하기 어려울 정도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mvc와 webflux 간의 가장 중요한 차이는 빌드에 추가하는 의존성의 차이가 있는데 기존의 mvc에선 spring-boot-starter-web을 사용하였지만, WebFlux는 spring-boot-starter-webflux 의존성을 사용한다는 차이가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632637592504&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// spring mvc
implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)

// spring webflux
implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebFlux는 기본적으로 내장 서버가 tomcat이 아닌 event loop 기반의 netty가 사용됩니다. netty는 webflux와 같은 reactive web framework와 궁합이 잘 맞습니다. 하지만 spring mvc라고 해도 reactive type을 사용하지 못하는 것은 아닙니다. spring mvc도 위에처럼 Mono나 Flux를 return 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 차이점은 WebFlux는 요청이 이벤트 루프로 처리되는 진짜 리액티브 웹 프레임워크인 반면 spring mvc는 multi thread에 의존하여 다수의 요청을 처리하는 servlet 기반의 web framework라는 사실입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Operation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mono와 Flux는 Publisher를 이미 구현한 type입니다. 그리고 이미 utility성 메서드들이 많이 구현되어 있습니다. 잠시 operator에 대해서 살펴보고 가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SteopVerifier&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StopVerifier는 해당 리액티브 타입을 구독한 다음 stream을 통해 전달되는 데이터에 assetion을 적용하여 결과를 확인할 수 있습니다. 아래의 예제들의 결과는 StepVerifier를 이용해서 확인해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Join&lt;/h4&gt;
&lt;pre id=&quot;code_1632643551020&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun join() {
        val peopleFlux = Flux.just(&quot;noah&quot;, &quot;kevin&quot;, &quot;elly&quot;)
        StepVerifier.create(peopleFlux)
            .expectNext(&quot;noah&quot;)
            .expectNext(&quot;kevin&quot;)
            .expectNext(&quot;elly&quot;)
            .verifyComplete()   // people이 완전히 같은지 확인
    }&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;fromIterable&lt;/p&gt;
&lt;pre id=&quot;code_1632644105988&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun fromIterable() {
        val people = listOf(&quot;noah&quot;, &quot;kevin&quot;, &quot;elly&quot;)
        val peopleFlux = Flux.fromIterable(people)
        StepVerifier.create(peopleFlux)
            .expectNext(&quot;noah&quot;)
            .expectNext(&quot;kevin&quot;)
            .expectNext(&quot;elly&quot;)
            .verifyComplete()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;range&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 범위 값을 생성하는 카운터 Flux&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;417&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqj7wt/btrf3wJjQ2b/fdwOthXCr94zJLDZkEJBn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqj7wt/btrf3wJjQ2b/fdwOthXCr94zJLDZkEJBn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqj7wt/btrf3wJjQ2b/fdwOthXCr94zJLDZkEJBn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqj7wt%2Fbtrf3wJjQ2b%2FfdwOthXCr94zJLDZkEJBn1%2Fimg.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;417&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1632644118781&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun range() {
        val people = Flux.range(1, 3);
        StepVerifier.create(people)
            .expectNext(1)
            .expectNext(2)
            .expectNext(3)
            .verifyComplete();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;inverval&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방출되는 시간이나 간격 주기를 설정하여 증가 값을 방출하는 Flux&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;517&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rraT7/btrfXIjfGAY/15bQjc8MCOQyWpvlxuuG6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rraT7/btrfXIjfGAY/15bQjc8MCOQyWpvlxuuG6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rraT7/btrfXIjfGAY/15bQjc8MCOQyWpvlxuuG6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrraT7%2FbtrfXIjfGAY%2F15bQjc8MCOQyWpvlxuuG6k%2Fimg.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;517&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1632644129707&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun interval() {
        val people = Flux.interval(Duration.ofSeconds(1))
            .take(3); // 값을 제한하지 않으면 무한정 실행됨

        StepVerifier.create(people)
            .expectNext(0L) // 0부터 시작함
            .expectNext(1L)
            .expectNext(2L)
            .verifyComplete();
    }&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;다음은 reactive type을 조합하는 operator 들입니다.&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-origin-width=&quot;1660&quot; data-origin-height=&quot;964&quot; data-filename=&quot;스크린샷 2021-09-27 오후 2.40.43.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNKAt2/btrfZpLZclt/dYDFlO2e4pNbPKOiE9WhZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNKAt2/btrfZpLZclt/dYDFlO2e4pNbPKOiE9WhZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNKAt2/btrfZpLZclt/dYDFlO2e4pNbPKOiE9WhZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNKAt2%2FbtrfZpLZclt%2FdYDFlO2e4pNbPKOiE9WhZ1%2Fimg.png&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;964&quot; data-filename=&quot;스크린샷 2021-09-27 오후 2.40.43.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;map&lt;/h4&gt;
&lt;pre id=&quot;code_1632644272710&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final &amp;lt;V&amp;gt; Flux&amp;lt;V&amp;gt; map(Function&amp;lt;? super T, ? extends V&amp;gt; mapper)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map은 단일 값을 V타입으로 반환하는 operator입니다. 그래서 map은 각 항목이 Flux로부터 발행되었을 때 &lt;b&gt;동기적으로&lt;/b&gt; 매핑이 수행됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632644323308&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun map() {
        val profileFlux: Flux&amp;lt;Profile&amp;gt; = Flux
            .just(&quot;noah 25&quot;, &quot;kevin 27&quot;, &quot;elly 20&quot;)
            .map {
                val split = it.split(&quot;\\s&quot;).toTypedArray()
                Profile(split[0], split[1])
            }
        StepVerifier.create(profileFlux)
            .expectNext(Profile(&quot;noah&quot;, &quot;25&quot;))
            .expectNext(Profile(&quot;kevin&quot;, &quot;27&quot;))
            .expectNext(Profile(&quot;elly&quot;, &quot;20&quot;))
            .verifyComplete()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;flatMap&lt;/h4&gt;
&lt;pre id=&quot;code_1632644358454&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final &amp;lt;R&amp;gt; Flux&amp;lt;R&amp;gt; flatMap(Function&amp;lt;? super T, ? extends Publisher&amp;lt;? extends R&amp;gt;&amp;gt; mapper)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 flatMap은 T유형의 단일 값을 R유형의 Publisher로 변환합니다. 그리고 이러한 내부의 Publisher를 단일 Flux로 병합합니다. flatMap은 map operator와 달리 비동기적으로 수행된다는 특징이 있습니다. 따라서 subscribeOn() 메서드를 이용하여 reactive type의 변환을 &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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;822&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQ4J1t/btrfXGMuvYn/S5Jb3SXqZme3DMgWeLwho1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQ4J1t/btrfXGMuvYn/S5Jb3SXqZme3DMgWeLwho1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQ4J1t/btrfXGMuvYn/S5Jb3SXqZme3DMgWeLwho1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQ4J1t%2FbtrfXGMuvYn%2FS5Jb3SXqZme3DMgWeLwho1%2Fimg.png&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;822&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1632644493241&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun flatmap() {
        val profileFlux = Flux
            .just(&quot;noah 25&quot;, &quot;kevin 27&quot;, &quot;elly 20&quot;)
            .flatMap {
                Mono.just(it)
                    .map {
                        val split = it.split(&quot;\\s&quot;)
                        Profile(split[0], split[1])
                    }
                .subscribeOn(Schedulers.parallel())
            }

        val profiles = listOf(
            Profile(&quot;noah&quot;, &quot;25&quot;),
            Profile(&quot;kevin&quot;, &quot;27&quot;),
            Profile(&quot;elly&quot;, &quot;20&quot;)
        )
        StepVerifier.create(profileFlux)
            .expectNextMatches { profiles.contains(it) }
            .expectNextMatches { profiles.contains(it) }
            .expectNextMatches { profiles.contains(it) }
            .verifyComplete()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 잠시 살펴보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. flatMap 안에서 String 객체를 Mono type으로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Mono에 map() operator를 이용하여 String 객체를 Profile 객체로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. subscribeOn() 메서드를 호출하면서 parameter로 넘겨주는 Scehdulers에 따라 병렬 스레드로 처리할 수 있습니다.&lt;/p&gt;
&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;Schedulers class는 아래와 같은 동시성 모델을 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 110px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;메서드&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;.immediate()&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;현재 스레드에서 구독을 실행함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;.single()&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;하나의 재사용 가능한 스레드에서 구독을 실행함. 모든 호출자에 대해 동일한 스레드를 재사용함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;.newSingle()&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;매 호출마다 전용 스레드에서 구독을 실행함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;.elastic()&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;새로운 작업 스레드가 생성되며, 유휴 스레드는 제거됨.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;.parallel()&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;고정된 크기의 풀에서 가져온 스레드에서 구독을 실행함. cpu코어의 개수가 크기가 됨.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 정말 많은 operator들이 존재합니다. 더 자세한 내용은 reactor 문서에서 확인할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1632644925383&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;Flux (reactor-core 3.4.10)&quot; data-og-description=&quot;&quot; data-og-host=&quot;projectreactor.io&quot; data-og-source-url=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html&quot; data-og-url=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html&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;Flux (reactor-core 3.4.10)&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;projectreactor.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebClient&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네... 드디어 도착했습니다. 이제 WebClient에 대해서 살펴보도록 하겠습니다. 사실 WebClient api를 익히는 것은 그리 어려운 것이 아닙니다. 다만 앞에서 언급했던 reactive programing이 어떤 문제를 해결하려고 했고, 어떤 operator들을 사용하는지와 같은 것들만 알고 있다면 말이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;410&quot; data-filename=&quot;스크린샷 2021-09-26 오후 5.30.16.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yyfud/btrf3teYoUF/2ZR1SKR9mglOFXObZXFOcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yyfud/btrf3teYoUF/2ZR1SKR9mglOFXObZXFOcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yyfud/btrf3teYoUF/2ZR1SKR9mglOFXObZXFOcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYyfud%2Fbtrf3teYoUF%2F2ZR1SKR9mglOFXObZXFOcK%2Fimg.png&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;410&quot; data-filename=&quot;스크린샷 2021-09-26 오후 5.30.16.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅의 제일 처음에 언급했던 것처럼 WebClient는 non-blocking io를 지원하는 http client입니다. 내부적으로는 netty를 사용하고 있기 때문에 event loop방식으로 동작하고 reactive type인 mono와 flux를 return 하는 http client입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient를 사용하는 패턴을 간략하게 표현하면 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebClient 인스턴스를 생성 또는 주입합니다.&lt;/li&gt;
&lt;li&gt;HTTP method 지정합니다.&lt;/li&gt;
&lt;li&gt;요청을 보낼 URI, Header, Body를 지정합니다.&lt;/li&gt;
&lt;li&gt;요청을 제출합니다.&lt;/li&gt;
&lt;li&gt;응답을 소비(사용)합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 간단하게 Get method와 Post method의 사용 방법에 대해서 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GET&lt;/h4&gt;
&lt;pre id=&quot;code_1632645610886&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun get() {
        val id = &quot;apple&quot;
        val fruit = WebClient.create()
            .get()
            .uri(&quot;localhost:8080/fruits/{id}&quot;, id) // 요청을 정의함
            .retrieve()  // 요청을 실행함
            .bodyToMono(Fruit::class.java)   // 응답 몸체의 payload를 Mono 형태로 추출함

        fruit
            .timeout(Duration.ofSeconds(1))  // 1초 이상 걸리게 되면 subscribe에 두번재 인자로 지정된 에러 핸들러가 실행됨
            .subscribe { println(it) }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청은 retrieve() 대신 exchange()을 사용할 수 있습니다. exchage()는 RestTemplate을 ResponseEntity와 비슷한 객체로 header의 정보까지 포함하고 있는 response 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;POST&lt;/h4&gt;
&lt;pre id=&quot;code_1632645961020&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun post() {
        val fruitMono = Mono.just(Fruit(&quot;파인애플&quot;))
        val fruit = WebClient.create()
            .post()
            .uri(&quot;localhost:8080/fruits&quot;)
            .body(fruitMono, Fruit::class.java)
            .retrieve()
            .bodyToMono(Fruit::class.java)
        
        fruit.subscribe { println(it) }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Post method에선 body에 publisher type의 객체를 넣어서 http body를 넣어줄 수 있습니다. 이때 만약 Mono나 Flux type이 아니라 domain 객체라면 bodyValue() 메서드(spring 2.2.0 이하에선 syncBody)를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 현재는 응답 payload로 Fruit class를 사용하고 있지만 비어져 있는 응답 papyload 받기 위해선 Void class를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632646190086&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Test
    fun voidPost() {
        val fruitMono = Fruit(&quot;파인애플&quot;)
        val fruit = WebClient.create()
            .post()
            .uri(&quot;localhost:8080/fruits&quot;)
            .bodyValue(fruitMono)
            .retrieve()
            .bodyToMono(Void::class.java)

        fruit.subscribe { println(it) }
    }&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;이외에도 다양한 메서드가 존재하지만 이제 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;사실 kotlin을 이용해서 WebFlux 개발을 한다면 이렇게 Mono와 Flux를 사용하는 방법보다 좀 더 우아한 방법이 존재합니다. 바로 coroutine을 이용하는 방법인데요 ㅎㅎ&lt;/p&gt;
&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>Server/Spring (Boot &amp;amp; Framework)</category>
      <category>non blocking</category>
      <category>observable pattern</category>
      <category>Reactive Streams</category>
      <category>WebClient</category>
      <category>Webflux</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/150</guid>
      <comments>https://ooeunz.tistory.com/150#entry150comment</comments>
      <pubDate>Sun, 26 Sep 2021 15:30:01 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 비동기 처리시 blocking 되는 servlet thread 관리</title>
      <link>https://ooeunz.tistory.com/149</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat에서의 IO는 HttpServletRequest와 HttpServletResponse를 사용하고 있고, 이 둘은 InputStream과 OutputStream을 구현하고 있습니다. 즉 IO가 이루어질 때마다 blocking이 발생한다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tomcat에서 NIO connector가 구현된 이후부터 connection을 nonblocking하게 맺고 있지만, 결국엔 servlet을 실행하는 순간 servlet thread가 필요로 하기 때문에 근본적인 문제의 해결책이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 일어나는 작업들, 흔히 req - logic - res가 이루어질 때 빠르게 thread가 작업을 처리하고 pool로 반납하면 이러한 방식도 문제가 되진 않습니다. 다만 최근 많이 가져가고 있는 MSA 아키텍처에서의 처리 방식, 이를테면 req - Blocking IO (db, api) 혹은 cpu bouond 작업 - res 와 같이 중간에 blocking io가 발생하게 되면 blocking io가 발생하는 긴 시간 동안 servlet thread는 놀고 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 오래걸리는 작업을 병렬적으로 해결하기 위해 background에 worker thread를 생성하여 병렬적으로 io작업을 수행한다면 직렬적으로 io를 수행하는 것보다 수행 시간을 줄일 수 있겠지만, 그 사이 servlet thread가 blocking을 당한다는 문제점이 여전히 존재하게 됩니다. Blocking은 cpu resource 운영에 상당한 악영향을 주게 되는데&amp;nbsp;blocking io가 발생했을 때 한번, 다시 cpu를 할당받을 때 또 한 번 총 두 번의 context switching이 발생하기 때문에 되도록 thread가 blocking 당하는 일을 피하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기적으로 시간이 긴 작업을 처리하더라도 servlet thread가 blocking 당하면 성능의 저하가 올 수 있음을 확인할 수 있는 간단한 테스트를 진행해 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해 tomcat의 max thread를 1개로 제한하고 io가 오래 걸리는 작업을 의미하는 slowThread 메서드(요청당 2초의 delay가 걸림)를 실행하는 서버 코드를 작성했습니다. 그리고 서버에선 slowThread 메서드를 비동기적으로 실행하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632561794786&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server.tomcat.threads.max=1&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1632561705371&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
class NoahDemoApplication

fun main(args: Array&amp;lt;String&amp;gt;) {
    runApplication&amp;lt;NoahDemoApplication&amp;gt;(*args)
}

@RestController
class DemoControler {
    val es = Executors.newFixedThreadPool(100)

    @GetMapping(&quot;/block&quot;)
    fun block(idx: Int): String {
        val future = es.submit&amp;lt;String&amp;gt;{ slowThread(idx) }
        return future.get()
    }
    
	private fun slowThread(idx: Int): String {
        Thread.sleep(2000L);
        return &quot;SLOW-$idx&quot;;
    }
}&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;부하테스트를 위해 실행 시간을 재기 위해 StopWatch를 사용했고, 100개의 요청을 비동기로 동시에 요청하도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632561752533&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private val log = KotlinLogging.logger {}
private val es = Executors.newFixedThreadPool(100)

class LoadTest(
) {
    private val BASE_URL: String = &quot;http://localhost:8080&quot;

    private val restTemplate = RestTemplate()

    fun fetchApi() {
        for (i in 1..100) {
            es.execute {
                val stopWatch = StopWatch()

                stopWatch.start()
                val response = restTemplate.getForObject(&quot;$BASE_URL/block?=idx=$i&quot;, String::class.java)
                stopWatch.stop()

                log.info { &quot;response=$response, stopWatch=${stopWatch.totalTimeSeconds}&quot; }
            }
        }
    }
}

fun main(args: Array&amp;lt;String&amp;gt;) {
    val loadTest = LoadTest()
    val stopWatch = StopWatch()

    stopWatch.start()
    loadTest.fetchApi()
    stopWatch.stop()

    es.shutdown()
    es.awaitTermination(1000, TimeUnit.SECONDS)
    log.info { &quot;Total stop watch ${stopWatch.totalTimeSeconds}&quot; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2714&quot; data-origin-height=&quot;1418&quot; data-filename=&quot;스크린샷 2021-09-25 오후 6.22.54.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QJ4K1/btrfZqCyo5P/DJoL7DV79Y7pEm3aWx6x30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QJ4K1/btrfZqCyo5P/DJoL7DV79Y7pEm3aWx6x30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QJ4K1/btrfZqCyo5P/DJoL7DV79Y7pEm3aWx6x30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQJ4K1%2FbtrfZqCyo5P%2FDJoL7DV79Y7pEm3aWx6x30%2Fimg.png&quot; data-origin-width=&quot;2714&quot; data-origin-height=&quot;1418&quot; data-filename=&quot;스크린샷 2021-09-25 오후 6.22.54.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하 테스트시 thread 상태를 확인해보면 의도대로 servlet thread (http-nio-exec-1)는 1개만 생성된 것을 알 수 있고, background thread로 task를 처리한 것을 알 수 있습니다. 그렇다면 수행 시간은 어떻게 됐을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;50&quot; data-filename=&quot;스크린샷 2021-09-25 오후 10.12.00.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8KzgI/btrfURarlQC/iehccwoXvxbWXkPjE48Ut0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8KzgI/btrfURarlQC/iehccwoXvxbWXkPjE48Ut0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8KzgI/btrfURarlQC/iehccwoXvxbWXkPjE48Ut0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8KzgI%2FbtrfURarlQC%2FiehccwoXvxbWXkPjE48Ut0%2Fimg.png&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;50&quot; data-filename=&quot;스크린샷 2021-09-25 오후 10.12.00.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;background에 100개의 thread가 존재했지만, servlet thread의 blocking으로 인해서 slowThread 메서드가 수행되는 2초 * 100회로 총 200초가 걸린 것을 알 수 있습니다. 다시 말해, 비동기로 시간이 긴 작업을 처리하더라도 여전히 servlet thread는 blocking 상태라는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 default로 200개의 thread를 가지게 됩니다. 그리고 이 thread가 모두 소모되게 된다면 queue에 request가 쌓이게 됩니다.&amp;nbsp; 이때부터 서버는 요청에 대한 응답에 latency가 발생을 하게 되고, default queue size인 100개 마저 모두 차게 된다면&amp;nbsp; 서버는 SERVICE_UNAVAILABLE(503)라는 서비스 이용불가 error code를 내려주게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 무한정 thread를 늘리면 되지 않을까?라고 생각할 수 있지만, thread 하나하나 모두 자기 고유의 stacktrace와 data 공간을 필요로 하기 때문에 그만큼 많은 memory가 필요합니다. 또한 thread의 수만큼 많은 context switching이 필요하기 때문에 cpu의 수가 극히 많은 서버를 갖고 있는 게 아니라면 좋은 방법이 아닐 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 오래 걸리는 작업은 background에 worker thread에 할당하고, servlet thread는 즉시 pool로 반납함으로써 이러한 문제를 해결할 수 있습니다. 이러한 문제를 해결하기 위해 servlet 3.2 spec에는 DeferredResult라는 기술이 추가되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeferredResult를 사용하면 결과를 바로 return 하여 servlet thread를 즉시 pool로 반환하고 이후에 비동기 작업이 완료되었을 때 DeferredResult에게 알려줌으로써 servlet thread를 빠르게 할당받아 client에게 결과를 return 해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max thread가 1개로 제한된 환경에서 이번엔 DeferredResult로 test 해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632589397219&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @GetMapping(&quot;/nonblock&quot;)
    fun nonblock(idx: Int): DeferredResult&amp;lt;String&amp;gt; {
        val def = DeferredResult&amp;lt;String&amp;gt;()
        es.submit {
            val slowResult = slowThread(idx)
            def.setResult(slowResult)
        }
        return def
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;44&quot; data-filename=&quot;스크린샷 2021-09-26 오전 2.04.47.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csvZY3/btrfXyA2DsW/Gentob8Uv0KVHqATuX3SfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csvZY3/btrfXyA2DsW/Gentob8Uv0KVHqATuX3SfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csvZY3/btrfXyA2DsW/Gentob8Uv0KVHqATuX3SfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsvZY3%2FbtrfXyA2DsW%2FGentob8Uv0KVHqATuX3SfK%2Fimg.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;44&quot; data-filename=&quot;스크린샷 2021-09-26 오전 2.04.47.png&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 100번의 요청을 동시에 실행하였지만 전체 실행시간이 고착 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;이러한 코드를 좀 더 modern 하게 변경한다면 CompletableFuture를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632589648733&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @GetMapping(&quot;/modern&quot;)
    fun modernNonBlock(idx: Int): CompletableFuture&amp;lt;String&amp;gt; {
        return CompletableFuture.supplyAsync { slowThread(idx) }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture는 Javascript에서 Promise와 비슷한 역할을 하는 기술로, 비동기 콜백 구조의 코드를 좀 더 깔끔하게 짤 수 있다는 장점이 있습니다. 또한 동시에 spring 기반의 @Async와 같은 애노테이션을 사용하지 않고도 자체만으로 비동기 코드를 짤 수 있고, controller에서 return 시에 spring에서 알아서 callback이 이루어지는 시점에 thread를 할당하여 결과를 반환해주게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture는 그 자체만으로 내용이 방대하므로 이 포스팅에선 여기까지만 다루도록 하겠습니다 ㅎㅎ&lt;/p&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 thread는 blocking 방식으로 동작하고 있다는 한계가 존재하고 있습니다. Non-blocking IO와 reacctive programing에 대해서 다뤄보도록 하겠습니다.&lt;/p&gt;</description>
      <category>Server/Spring (Boot &amp;amp; Framework)</category>
      <category>CompletableFuture</category>
      <category>DeferredResult</category>
      <category>servlet thread</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/149</guid>
      <comments>https://ooeunz.tistory.com/149#entry149comment</comments>
      <pubDate>Sat, 25 Sep 2021 21:54:11 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes] CRD(Custom Resource Definition)와 Custom Controller 사용하기</title>
      <link>https://ooeunz.tistory.com/148</link>
      <description>&lt;p&gt;이번 포스팅엔 Kubernetes에 Custom Resource를 사용하는 방법에 대해서 살펴보도록 하겠습니다. Custom resource를 정의하고 사용하기 위해선 기반 지식이 조금 필요한데요. 이전의 포스팅에서 조금씩 언급한 부분이지만 시간이 지나고 나니 저의 설명이 너무 부실....^^한 포스팅이 많은 것 같아서 관련된 부분들을 조금씩 다시 언급하며 포스팅을 진행하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Custom Resource를 사용하기 위해서 알아야 하는 가장 중요한 지식이 있는데 바로 Object와 Controller입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Object&lt;/h3&gt;
&lt;p&gt;Kubernetes에서 관리하는 리소스를 말합니다. 여기서 리소스란 Container, Network Config, Storage Config 등이 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들어 noah-deploy라는 deployment가 kubernetes 환경에 배포되어 있을 경우 배포된 yaml 파일을 조회해보면 아래와 같은 결과를 확인하실 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617169575666&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl get deploy noah-deploy -o yaml

apiVersion: v1
kind: Deployment
metadata:
  name: noah-deploy
  namespace: default
  ...
spec:
  replicas: 3
  template:
  ...
    containers:
    - image: ooeunz/noah:0.0.1-SNAPSHOT
      imagePullPolicy: Always
      name: noah
status:
  ...
  readyReplicas: 3
  replicas: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주의 깊게 볼 부분은 metadata, spec, status입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;metadata: 말 그대로 metadata입니다. Object의 name이나 namespace 같은 정보가 들어있습니다.&lt;/li&gt;
&lt;li&gt;spec: Object가 원하는 상태입니다. (Desired State)&lt;/li&gt;
&lt;li&gt;status: Object의 현재 상태입니다. (Current State)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller&lt;/h3&gt;
&lt;p&gt;위에서 확인한 Object의 Status(현재 상태)를 spec(정의한 스펙)과 일치하도록 제어하는 일을 합니다. 즉 예를 들어 기대한 spec의 replicas가 3인데 status의 값이 2라면 controller는 status를 spec에 일치시키기 위해 controller는 하나의 pod를 더 실행시키게 될 것입니다.&lt;/p&gt;
&lt;p&gt;controller의 이러한 제어 과정을&amp;nbsp;&lt;b&gt;Reconcile&lt;/b&gt;이라고 명칭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-03-31 오후 2.29.58.png&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;622&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CQVhR/btq1sBl4a1Y/GEHeRYV0ocLpkQQiMu1bVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CQVhR/btq1sBl4a1Y/GEHeRYV0ocLpkQQiMu1bVK/img.png&quot; data-alt=&quot;if kakao 2020&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CQVhR/btq1sBl4a1Y/GEHeRYV0ocLpkQQiMu1bVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCQVhR%2Fbtq1sBl4a1Y%2FGEHeRYV0ocLpkQQiMu1bVK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-03-31 오후 2.29.58.png&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;622&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;if kakao 2020&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그렇다면 Controller는 내부적으로 어떤 식으로 동작하는지 좀 더 자세히 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;K8S API Server&lt;/p&gt;
&lt;p&gt;가장 위에 있는 K8S API Server는 아시다시피 사용자가 클러스터에 접근하기 위한 api를 제공하는 서버입니다. 그와 동시에 kubernetes의 모든 구성요소와 상호 작용하는 서버입니다. 만약 사용자로부터 Object 생성 요청을 받게 되면 api server는 object를 etcd 분산형 저장소에 저장하게 되고 Object Event를 컨트롤러에 전달하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Work Queue&lt;/p&gt;
&lt;p&gt;API Server로부터 spec 또는 status가 변경된 object의 식별 정보를 저장하는 queue입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Reconcile&lt;/p&gt;
&lt;p&gt;그림에 존재하는 reconciile은 실제론 reconcile을 수행하는 함수입니다. Controller는 reconcile을 실행하기 위해 spec 또는 status가 변경됐다는 object의 식별자를 필요로 합니다. 이 식별자는 controller 내부에 존재하는 work queue에서 가져오게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이때 reconcile을 성공하게 되면 work queue로부터 가져온 식별자를 제거하게 되지만, 만약 실패한다면 다시 수행하기 위해 식별자를 queue에 다시 적재합니다. 이러한 과정을 &lt;b&gt;BackOff&lt;/b&gt;라고 부릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;K8S (Read) Cache&lt;/h2&gt;
&lt;p&gt;Reconcile 함수는 reconcile을 수행하기 위해 k8s client를 사용하게 됩니다. 이때 k8s client는 API Server의 과부하를 방지하기 위해 controller 내부에 존재하는 cache로부터 데이터를 읽어오게 되는데요. 캐시를 사용하기 때문에 일시적으로 API Server와 cache 사이에 데이터 불일치가 발생할 수 있습니다. 하지만 이러한 문제는 계속해서 reconcile이 적용됨에 따라 api server와 cache 사이에 sync가 적용되며 자연스레 해소되게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Custom Controller&lt;/h2&gt;
&lt;p&gt;그렇다면 custom controller란 무엇일까요?&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;custom controller란 &lt;/span&gt;말 뜻에서도 유추할 수 있듯이 kubernetes 사용자가 개발한 컨트롤러입니다. custom controller를 사용하면 kubernetes 사용자가 원하는 대로 리소스를 선언형으로 관리할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 선언형으로 관리한다는 것은 일단 한번 리소스를 선언하고 나면 이후부턴 controller가 리소스를 스스로 관리한다는 뜻입니다. 즉 장애가 발생하더라도 위에서 살펴본 reconcile로 인해 controller가 스스로 장애를 복구하며, 리소스 전체를 이해하지 않더라도 간단하게 spec을 변경함으로 컨트롤러가 알아서 변경 관리하도록 해서 유지보수의 이점까지 챙길 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;무엇을 만들지?&lt;/h2&gt;
&lt;p&gt;그럼 이번엔 직접 Kubernetes Controller를 만들어보도록 하겠습니다. Kubernetes에선 Controller를 만들기 위해 다양한 client library를 지원하고 있습니다. 대표적인 언어로는 Go, Python, Java, Javascript가 있으며 자세한 내용은 &lt;a href=&quot;https://kubernetes.io/ko/docs/reference/using-api/client-libraries/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이곳&lt;/a&gt;에서 확인하실 수 있습니다. 특별히 이번 포스팅에서는 Java 클라이언트 라이브러리를 사용해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1617859994945&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;fabric8io/kubernetes-client&quot; data-og-description=&quot;Java client for Kubernetes &amp;amp; OpenShift . Contribute to fabric8io/kubernetes-client development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/fabric8io/kubernetes-client&quot; data-og-url=&quot;https://github.com/fabric8io/kubernetes-client&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bzR4rj/hyJPuuIqV7/WQmqUmW1tDvWtAJL9l35kk/img.png?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256&quot;&gt;&lt;a href=&quot;https://github.com/fabric8io/kubernetes-client&quot; data-source-url=&quot;https://github.com/fabric8io/kubernetes-client&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bzR4rj/hyJPuuIqV7/WQmqUmW1tDvWtAJL9l35kk/img.png?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;fabric8io/kubernetes-client&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Java client for Kubernetes &amp;amp; OpenShift . Contribute to fabric8io/kubernetes-client development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서 만들 Custom Controller는 아래와 같은 CustomResource Foo에 대한 컨트롤러입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617859805297&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: samplecontroller.k8s.io/v1alpha1
kind: Foo
metadata:
  name: example-bar
spec:
  deploymentName: example-bar-deploy
  replicas: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 템플릿으로 정의된 Foo resource가 생성되면 우리가 지정한 이름과 원하는 replica의 수에 대한 정보를 담은 Deployment를 함께 생성하게 됩니다. 그리고 Foo resource의 spec을 변경하면 다른 Kubernetes resource처럼 child Deployment에 업데이트를 진행하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Project Structure&lt;/h3&gt;
&lt;p&gt;Java project의 구조는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;model/v1alpha1 package 아래에 있는 Foo.java, FooList.java, FooSpec.java, FooStatus.java 클래스들은 Foo CustomResource에서 사용할 모델 클래스입니다.&lt;/p&gt;
&lt;p&gt;controller package 하위에 있는 MyController.java 클래스는 컨트롤러의 로직을 포함하는 클래스입니다.&lt;/p&gt;
&lt;p&gt;마지막으로 MyControllerMain.java 클래스는 KubernetesClient를 초기화하고 MyController에게 제어권을 위임하는 이 프로젝트의 driver class입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617860008680&quot; class=&quot;html xml&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;. 
├── pom.xml 
├── README.md 
└── src 
    ├── main 
    │   ├── java 
    │   │   └── io 
    │   │       └── fabric8 
    │   │           └── mycontroller 
    │   │               ├── api 
    │   │               │   └── model 
    │   │               │       └── v1alpha1 
    │   │               │           ├── Foo.java
    │   │               │           ├── FooList.java 
    │   │               │           ├── FooSpec.java 
    │   │               │           └── FooStatus.java 
    │   │               ├── controller 
    │   │               │   └── MyController.java 
    │   │               └── MyControllerMain.java 
    │   └── resources 
    │       ├── crd.yaml 
    │       ├── cr.yaml 
    │       └── example-foo.yml 
    └── test 
        └── ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 Maven Project를 생성하고 아래와 같이 dependency를 추가하도록 합니다. 사용할 dependency로는 로깅에 사용할 slf4j-simple과 Fabric8 kubernetes-client를 사용하게 됩니다. 그리고 모든 dependency를 포함하여 실행 가능한 jar를 만들기 위해 maven-assembly-plugin도 함께 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617857847917&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.fabric8&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;kubernetes-client&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;5.2.1&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;slf4j-simple&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.7.30&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-assembly-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.1.0&amp;lt;/version&amp;gt;
                &amp;lt;executions&amp;gt;
                    &amp;lt;execution&amp;gt;
                        &amp;lt;phase&amp;gt;package&amp;lt;/phase&amp;gt;
                        &amp;lt;goals&amp;gt;
                            &amp;lt;goal&amp;gt;single&amp;lt;/goal&amp;gt;
                        &amp;lt;/goals&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;archive&amp;gt;
                                &amp;lt;manifest&amp;gt;
                                    &amp;lt;mainClass&amp;gt;
                                        io.fabric8.mycontroller.MyControllerMain
                                    &amp;lt;/mainClass&amp;gt;
                                &amp;lt;/manifest&amp;gt;
                            &amp;lt;/archive&amp;gt;
                            &amp;lt;descriptorRefs&amp;gt;
                                &amp;lt;descriptorRef&amp;gt;jar-with-dependencies&amp;lt;/descriptorRef&amp;gt;
                            &amp;lt;/descriptorRefs&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                    &amp;lt;/execution&amp;gt;
                &amp;lt;/executions&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Model&lt;/h3&gt;
&lt;p&gt;Controller를 만들기 전에 컨트롤러의 기반이 되는 model 클래스들을 작성하도록 하겠습니다. 이때 작성되는 클래스들은 Fabric8 Kubernetes Client를 사용하여 Kubernetes API 서버와의 요청 및 응답으로 사용될 POJO 클래스입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래의 코드를 보면 알 수 있듯이 간단하게 애노테이션을 이용해서 Foo CustomResourceDefinition에 &lt;span style=&quot;color: #333333;&quot;&gt;Version, apiGroup, plural와&lt;span&gt; 같은 정보를 제공할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;Foo.java&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1617861360001&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.fabric8.mycontroller.api.model.v1alpha1;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Plural;
import io.fabric8.kubernetes.model.annotation.Version;

@Version(&quot;v1alpha1&quot;)
@Group(&quot;mycontroller.k8s.io&quot;)
@Plural(&quot;foos&quot;)
public class Foo extends CustomResource&amp;lt;FooSpec, FooStatus&amp;gt; implements Namespaced {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;FooSpec.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1617861407524&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.fabric8.mycontroller.api.model.v1alpha1;

public class FooSpec {
    private String deploymentName;
    private int replicas;

    public String getDeploymentName() {
        return deploymentName;
    }

    public void setDeploymentName(String deploymentName) {
        this.deploymentName = deploymentName;
    }

    public int getReplicas() {
        return replicas;
    }

    public void setReplicas(int replicas) {
        this.replicas = replicas;
    }

    @Override
    public String toString() {
        return &quot;FooSpec{replicas=&quot; + replicas + &quot;}&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;FooStatus.java&lt;/p&gt;
&lt;pre id=&quot;code_1617861389828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.fabric8.mycontroller.api.model.v1alpha1;

public class FooStatus {
    private int availableReplicas;

    public int getAvailableReplicas() {
        return availableReplicas;
    }

    public void setAvailableReplicas(int availableReplicas) {
        this.availableReplicas = availableReplicas;
    }

    @Override
    public String toString() {
        return &quot;FooStatus{ availableReplicas=&quot; + availableReplicas + &quot;}&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;FooList.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1617861371141&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.fabric8.mycontroller.api.model.v1alpha1;

import io.fabric8.kubernetes.client.CustomResourceList;

public class FooList extends CustomResourceList&amp;lt;Foo&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Main Class for initialization&lt;/h3&gt;
&lt;p&gt;이번엔 애플리케이션을 &lt;span style=&quot;color: #333333;&quot;&gt;Initialization 할 MyControllerMain 클래스에 작성하도록 하겠습니다. 이 클래스엔 &lt;/span&gt;Kubernetes API와 상호작용 하기 위한 KubernetesClient와 Foo resource와 Deployment를 위한 SharedInformers를 생성합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  SharedInformers란?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Informers 또는&amp;nbsp;SharedInformers는&amp;nbsp;이벤트 기반 아키텍처로 kube-apiserver의 이벤트를 사용자 지정 코드에 의해 포착합니다.&lt;br /&gt;이 이벤트의 예로는 pod 또는 node가 생성되는 일 등이 있습니다.&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Main 클래스에서 모든 Initialization이 끝나면 MyController의 인스턴스를 생성하고 mycontroller.create() 코드를 통해 Foo resource와 Deployment에 대한 이벤트 핸들러를 셋업 합니다. 그리고 myController.run()을 통해서 Custom Controller를 실행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1617865127460&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.fabric8.mycontroller;

import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
import io.fabric8.kubernetes.client.informers.SharedInformerFactory;
import io.fabric8.mycontroller.api.model.v1alpha1.Foo;
import io.fabric8.mycontroller.api.model.v1alpha1.FooList;

/**
 * Main Class for application, you can run this sample using this command:
 *
 *  mvn exec:java -Dexec.mainClass=&quot;io.fabric8.mycontroller.MyControllerMain&quot;
 */
public class MyControllerMain {

    public static final String DEFAULT_NAMESPACE = &quot;default&quot;;

    public static void main(String[] args) {
        try (KubernetesClient client = new DefaultKubernetesClient()) {
            String namespace = client.getNamespace();
            if (namespace.isEmpty()) {
                namespace = DEFAULT_NAMESPACE;
            }
            SharedInformerFactory informerFactory = client.informers();

            MixedOperation&amp;lt;Foo, FooList, Resource&amp;lt;Foo&amp;gt;&amp;gt; fooClient = client.customResources(Foo.class, FooList.class);
            SharedIndexInformer&amp;lt;Deployment&amp;gt; deploymentSharedIndexInformer =
                informerFactory.sharedIndexInformerFor(Deployment.class, 10 * 60 * 1000);
            SharedIndexInformer&amp;lt;Foo&amp;gt; fooSharedIndexInformer =
                informerFactory.sharedIndexInformerForCustomResource(Foo.class,10 * 60 * 1000);

            SampleController sampleController = new SampleController(client, fooClient, deploymentSharedIndexInformer, fooSharedIndexInformer, namespace);


            sampleController.create();
            informerFactory.startAllRegisteredInformers();
            informerFactory.addSharedInformerEventListener(exception -&amp;gt; System.out.println(&quot;Exception occurred, but caught: &quot; + exception));

            sampleController.run();
        } catch (KubernetesClientException exception) {
            System.out.println(&quot;Kubernetes Client Exception: &quot; + exception);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Custom Controller class&lt;/h3&gt;
&lt;p&gt;이제 MyController 클래스를 작성해보도록 하겠습니다. 이번엔 먼저 코드를 살펴본 다음 설명을 이어가도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1617884602725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package io.fabric8.mycontroller.controller;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.OwnerReference;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
import io.fabric8.kubernetes.client.informers.cache.Cache;
import io.fabric8.kubernetes.client.informers.cache.Lister;
import io.fabric8.mycontroller.MyControllerMain;
import io.fabric8.mycontroller.api.model.v1alpha1.FooList;
import io.fabric8.mycontroller.api.model.v1alpha1.Foo;
import io.fabric8.mycontroller.api.model.v1alpha1.FooSpec;
import io.fabric8.mycontroller.api.model.v1alpha1.FooStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class MyController {
    private final BlockingQueue&amp;lt;String&amp;gt; workqueue;
    private final SharedIndexInformer&amp;lt;Foo&amp;gt; fooInformer;
    private final SharedIndexInformer&amp;lt;Deployment&amp;gt; deploymentInformer;
    private final Lister&amp;lt;Foo&amp;gt; fooLister;
    private final KubernetesClient kubernetesClient;
    private final MixedOperation&amp;lt;Foo, FooList, Resource&amp;lt;Foo&amp;gt;&amp;gt; fooClient;
    public static final Logger logger = LoggerFactory.getLogger(MyControllerMain.class.getName());

    public MyController(KubernetesClient kubernetesClient, MixedOperation&amp;lt;Foo, FooList, Resource&amp;lt;Foo&amp;gt;&amp;gt; fooClient, SharedIndexInformer&amp;lt;Deployment&amp;gt; deploymentInformer, SharedIndexInformer&amp;lt;Foo&amp;gt; fooInformer, String namespace) {
        this.kubernetesClient = kubernetesClient;
        this.fooClient = fooClient;
        this.fooLister = new Lister&amp;lt;&amp;gt;(fooInformer.getIndexer(), namespace);
        this.fooInformer = fooInformer;
        this.deploymentInformer = deploymentInformer;
        this.workqueue = new ArrayBlockingQueue&amp;lt;&amp;gt;(1024);
    }

    public void create() {
        // Set up an event handler for when Foo resources change
        fooInformer.addEventHandler(new ResourceEventHandler&amp;lt;Foo&amp;gt;() {
            @Override
            public void onAdd(Foo foo) {
                enqueueFoo(foo);
            }

            @Override
            public void onUpdate(Foo foo, Foo newFoo) {
                enqueueFoo(newFoo);
            }

            @Override
            public void onDelete(Foo foo, boolean b) {
                // Do nothing
            }
        });

        // Set up an event handler for when Deployment resources change. This
        // handler will lookup the owner of the given Deployment, and if it is
        // owned by a Foo resource will enqueue that Foo resource for
        // processing. This way, we don't need to implement custom logic for
        // handling Deployment resources. More info on this pattern:
        // https://github.com/kubernetes/community/blob/8cafef897a22026d42f5e5bb3f104febe7e29830/contributors/devel/controllers.md
        deploymentInformer.addEventHandler(new ResourceEventHandler&amp;lt;Deployment&amp;gt;() {
            @Override
            public void onAdd(Deployment deployment) {
                handleObject(deployment);
            }

            @Override
            public void onUpdate(Deployment oldDeployment, Deployment newDeployment) {
                // Periodic resync will send update events for all known Deployments.
                // Two different versions of the same Deployment will always have different RVs.
                if (oldDeployment.getMetadata().getResourceVersion().equals(newDeployment.getMetadata().getResourceVersion())) {
                    return;
                }
                handleObject(newDeployment);
            }

            @Override
            public void onDelete(Deployment deployment, boolean b) {
                handleObject(deployment);
            }
        });
    }

    public void run() {
        logger.info(&quot;Starting {} controller&quot;, Foo.class.getSimpleName());
        logger.info(&quot;Waiting for informer caches to sync&quot;);
        while (!deploymentInformer.hasSynced() || !fooInformer.hasSynced()) {
            // Wait till Informer syncs
        }

        while (!Thread.currentThread().isInterrupted()) {
            try {
                logger.info(&quot;trying to fetch item from workqueue...&quot;);
                if (workqueue.isEmpty()) {
                    logger.info(&quot;Work Queue is empty&quot;);
                }
                String key = workqueue.take();
                Objects.requireNonNull(key, &quot;key can't be null&quot;);
                logger.info(&quot;Got {}&quot;, key);
                if (key.isEmpty() || (!key.contains(&quot;/&quot;))) {
                    logger.warn(&quot;invalid resource key: {}&quot;, key);
                }

                // Get the Foo resource's name from key which is in format namespace/name
                String name = key.split(&quot;/&quot;)[1];
                Foo foo = fooLister.get(key.split(&quot;/&quot;)[1]);
                if (foo == null) {
                    logger.error(&quot;Foo {} in workqueue no longer exists&quot;, name);
                    return;
                }
                reconcile(foo);

            } catch (InterruptedException interruptedException) {
                Thread.currentThread().interrupt();
                logger.error(&quot;controller interrupted..&quot;);
            }
        }
    }

    /**
     * Compares the actual state with the desired, and attempts to
     * converge the two. It then updates the Status block of the Foo resource
     * with the current status of the resource.
     *
     * @param foo specified resource
     */
    protected void reconcile(Foo foo) {
        String deploymentName = foo.getSpec().getDeploymentName();
        if (deploymentName == null || deploymentName.isEmpty()) {
            // We choose to absorb the error here as the worker would requeue the
            // resource otherwise. Instead, the next time the resource is updated
            // the resource will be queued again.
            logger.warn(&quot;No Deployment name specified for Foo {}/{}&quot;, foo.getMetadata().getNamespace(), foo.getMetadata().getName());
            return;
        }

        // Get the deployment with the name specified in Foo.spec
        Deployment deployment = kubernetesClient.apps().deployments().inNamespace(foo.getMetadata().getNamespace()).withName(deploymentName).get();
        // If the resource doesn't exist, we'll create it
        if (deployment == null) {
            createDeployments(foo);
            return;
        }

        // If the Deployment is not controlled by this Foo resource, we should log
        // a warning to the event recorder and return error msg.
        if (!isControlledBy(deployment, foo)) {
            logger.warn(&quot;Deployment {} is not controlled by Foo {}&quot;, deployment.getMetadata().getName(), foo.getMetadata().getName());
            return;
        }

        // If this number of the replicas on the Foo resource is specified, and the
        // number does not equal the current desired replicas on the Deployment, we
        // should update the Deployment resource.
        if (foo.getSpec().getReplicas() != deployment.getSpec().getReplicas()) {
            logger.info(&quot;Foo {} replicas: {}, Deployment {} replicas: {}&quot;, foo.getMetadata().getName(), foo.getSpec().getReplicas(),
                deployment.getMetadata().getName(), deployment.getSpec().getReplicas());
            deployment.getSpec().setReplicas(foo.getSpec().getReplicas());
            kubernetesClient.apps().deployments()
                .inNamespace(foo.getMetadata().getNamespace())
                .withName(deployment.getMetadata().getNamespace())
                .replace(deployment);
        }

        // Finally, we update the status block of the Foo resource to reflect the
        // current state of the world
        updateAvailableReplicasInFooStatus(foo, foo.getSpec().getReplicas());
    }

    private void createDeployments(Foo foo) {
        Deployment deployment = createNewDeployment(foo);
        kubernetesClient.apps().deployments().inNamespace(foo.getMetadata().getNamespace()).create(deployment);
    }

    private void enqueueFoo(Foo foo) {
        logger.info(&quot;enqueueFoo({})&quot;, foo.getMetadata().getName());
        String key = Cache.metaNamespaceKeyFunc(foo);
        logger.info(&quot;Going to enqueue key {}&quot;, key);
        if (key != null &amp;amp;&amp;amp; !key.isEmpty()) {
            logger.info(&quot;Adding item to workqueue&quot;);
            workqueue.add(key);
        }
    }

    private void handleObject(HasMetadata obj) {
        logger.info(&quot;handleDeploymentObject({})&quot;, obj.getMetadata().getName());
        OwnerReference ownerReference = getControllerOf(obj);
        Objects.requireNonNull(ownerReference);
        if (!ownerReference.getKind().equalsIgnoreCase(Foo.class.getSimpleName())) {
            return;
        }
        Foo foo = fooLister.get(ownerReference.getName());
        if (foo == null) {
            logger.info(&quot;ignoring orphaned object '{}' of foo '{}'&quot;, obj.getMetadata().getSelfLink(), ownerReference.getName());
            return;
        }
        enqueueFoo(foo);
    }

    private void updateAvailableReplicasInFooStatus(Foo foo, int replicas) {
        FooStatus fooStatus = new FooStatus();
        fooStatus.setAvailableReplicas(replicas);
        // NEVER modify objects from the store. It's a read-only, local cache.
        // You can create a copy manually and modify it
        Foo fooClone = getFooClone(foo);
        fooClone.setStatus(fooStatus);
        // If the CustomResourceSubresources feature gate is not enabled,
        // we must use Update instead of UpdateStatus to update the Status block of the Foo resource.
        // UpdateStatus will not allow changes to the Spec of the resource,
        // which is ideal for ensuring nothing other than resource status has been updated.
        fooClient.inNamespace(foo.getMetadata().getNamespace()).withName(foo.getMetadata().getName()).updateStatus(foo);
    }

    /**
     * createNewDeployment creates a new Deployment for a Foo resource. It also sets
     * the appropriate OwnerReferences on the resource so handleObject can discover
     * the Foo resource that 'owns' it.
     * @param foo {@link Foo} resource which will be owner of this Deployment
     * @return Deployment object based on this Foo resource
     */
    private Deployment createNewDeployment(Foo foo) {
        return new DeploymentBuilder()
            .withNewMetadata()
            .withName(foo.getSpec().getDeploymentName())
            .withNamespace(foo.getMetadata().getNamespace())
            .withLabels(getDeploymentLabels(foo))
            .addNewOwnerReference().withController(true).withKind(foo.getKind()).withApiVersion(foo.getApiVersion()).withName(foo.getMetadata().getName()).withNewUid(foo.getMetadata().getUid()).endOwnerReference()
            .endMetadata()
            .withNewSpec()
            .withReplicas(foo.getSpec().getReplicas())
            .withNewSelector()
            .withMatchLabels(getDeploymentLabels(foo))
            .endSelector()
            .withNewTemplate()
            .withNewMetadata().withLabels(getDeploymentLabels(foo)).endMetadata()
            .withNewSpec()
            .addNewContainer()
            .withName(&quot;nginx&quot;)
            .withImage(&quot;nginx:latest&quot;)
            .endContainer()
            .endSpec()
            .endTemplate()
            .endSpec()
            .build();
    }

    private Map&amp;lt;String, String&amp;gt; getDeploymentLabels(Foo foo) {
        Map&amp;lt;String, String&amp;gt; labels = new HashMap&amp;lt;&amp;gt;();
        labels.put(&quot;app&quot;, &quot;nginx&quot;);
        labels.put(&quot;controller&quot;, foo.getMetadata().getName());
        return labels;
    }

    private OwnerReference getControllerOf(HasMetadata obj) {
        List&amp;lt;OwnerReference&amp;gt; ownerReferences = obj.getMetadata().getOwnerReferences();
        for (OwnerReference ownerReference : ownerReferences) {
            if (ownerReference.getController().equals(Boolean.TRUE)) {
                return ownerReference;
            }
        }
        return null;
    }

    private boolean isControlledBy(HasMetadata obj, Foo foo) {
        OwnerReference ownerReference = getControllerOf(obj);
        if (ownerReference != null) {
            return ownerReference.getKind().equals(foo.getKind()) &amp;amp;&amp;amp; ownerReference.getName().equals(foo.getMetadata().getName());
        }
        return false;
    }

    private Foo getFooClone(Foo foo) {
        Foo cloneFoo = new Foo();
        FooSpec cloneFooSpec = new FooSpec();
        cloneFooSpec.setDeploymentName(foo.getSpec().getDeploymentName());
        cloneFooSpec.setReplicas(foo.getSpec().getReplicas());

        cloneFoo.setSpec(cloneFooSpec);
        cloneFoo.setMetadata(foo.getMetadata());

        return cloneFoo;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;create()&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;포스팅 첫 부분에서 언급했듯 Controller에는 workQueue가 있습니다. 이 큐에는 컨트롤러가 처리해야 하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ADD, UPDATE, DELETE와 같은 다양한&amp;nbsp;&lt;/span&gt;이벤트들이 대기열을 이루고 있게 됩니다. 이때 큐 대기열을 채우는 게&amp;nbsp;&lt;span&gt;SharedIndexInformer 이벤트 핸들러입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;create() 메서드에선 이 이벤트 핸들러를 셋업합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;run()&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;run 메서드는 Thread가 멈추지 않는 한 무한루프로 실행되면서&amp;nbsp;&lt;/span&gt;workQueue 안에 item이 있는지 체크하는 메서드입니다.&lt;/p&gt;
&lt;p&gt;만약 item이 존재한다면 item에 해당하는 Foo resource를 가져오게 됩니다. 이때 item이 유효하다고 생각이 되면 reconile() 메서드를 실행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;reconcile()&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;reconcile은 이 포스팅 처음에도 언급했지만 컨트롤러의 핵심 로직 메서드입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Deployment의 이름이 비어있거나 null이 아닌지 체크합니다.&lt;/li&gt;
&lt;li&gt;Kubernetes API Server에서 이름이 일치하는 Deployment를 가져옵니다. 이때 만약 일치하는 Deployment가 존재하지 않으면 createDeployment() 메서드를 호출해서 새로운 Deployment를 생성합니다.&lt;/li&gt;
&lt;li&gt;Kubernetes API Server에서 받은 Deployment가 실제로 Foo 리소스에 있는지 확인합니다. 만약 없다면 경고를 보냅니다.&lt;/li&gt;
&lt;li&gt;Kubernetes API Server에서 받은 Deployment의 replicas의 수와 foo.getSpec().getReplicas()의 수가 동일한지 확인합니다. 만약 동일하지 않다면 Foo에 설정한 replicas의 수와 동일하도록 업데이트합니다.&lt;/li&gt;
&lt;li&gt;모든 작업이 완료되었다면 Foo resource의 .status 필드에 현재 상태를 업데이트합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;설명한 것들 이외의 메서드는 child Deployment를 만드는 것과 Foo 상태를 업데이트하는 등과 같은 helper 메서드입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Running&lt;/h3&gt;
&lt;p&gt;이제 Custom Controller와 관련된 코드를 모드 작성했습니다. 마지막으로 resource 디렉토리 하위에 CRD를 생성하는 템플릿과 예시로 사용할 템플릿을 추가하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;crd.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1617884558269&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: foos.mycontroller.k8s.io
spec:
  group: mycontroller.k8s.io
  version: v1alpha1
  names:
    kind: Foo
    plural: foos
  scope: Namespaced
  subresources:
    status: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;example-foo.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1617884572035&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: mycontroller.k8s.io/v1alpha1
kind: Foo
metadata:
  name: example-foo
spec:
  deploymentName: example-foo-deploy
  replicas: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 실제로 Custom Controller를 실행시켜 보도록 하겠습니다. 아래의 명령어로 프로젝트를 compile 하도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617883910893&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mvn clean install&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런 다음 간단한 테스트 환경을 구축하기 위해 minikube를 install 하도록 하겠습니다. 아래의 명령어로 간단하게 minikube를 설치할 수 있습니다.(MacOS 기준)&lt;/p&gt;
&lt;pre id=&quot;code_1617883802961&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install minikube&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;minikube가 설치되면 minikube를 실행시켜줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617883819243&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;minikube start&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자, 이제 test 할 환경까지 갖추어졌으니 먼저 Kubernetes Cluster에 CRD를 배포하도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617884024040&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl create -f src/main/resources/crd.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;CRD가 생성된 것을 확인했으면 아래의 명령어로 Custom Controller를 실행시켜봅니다.&lt;/p&gt;
&lt;p&gt;그럼&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1617884105200&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -jar target/mycontroller-1.0.0-SNAPSHOT-jar-with-dependencies.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Custom Controller를 실행시키면 잠시 후 Work Queue가 비어져 있다는 log를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-04-08 오후 9.15.47.png&quot; data-origin-width=&quot;2616&quot; data-origin-height=&quot;1100&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brc1FC/btq2aFHtUyW/FTHTn19HnNlbeVYIJA5D40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brc1FC/btq2aFHtUyW/FTHTn19HnNlbeVYIJA5D40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brc1FC/btq2aFHtUyW/FTHTn19HnNlbeVYIJA5D40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrc1FC%2Fbtq2aFHtUyW%2FFTHTn19HnNlbeVYIJA5D40%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-04-08 오후 9.15.47.png&quot; data-origin-width=&quot;2616&quot; data-origin-height=&quot;1100&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이번엔 터미널 창을 하나 더 열어서 아래의 명령어로 Custom Resource인 Foo를 배포해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1617884359455&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl create -f src/main/resources/example-foo.yml  &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Foo가 배포되고 나면 Custom Controller의 work queue로 item이 들어오고 controller가 이를 처리하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-04-08 오후 9.19.03.png&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;302&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KNu4x/btq169Qa6qi/nKtEyR5kgjnAcvspviNwz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KNu4x/btq169Qa6qi/nKtEyR5kgjnAcvspviNwz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KNu4x/btq169Qa6qi/nKtEyR5kgjnAcvspviNwz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKNu4x%2Fbtq169Qa6qi%2FnKtEyR5kgjnAcvspviNwz1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-04-08 오후 9.19.03.png&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;302&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기까지 Custom controller를 생성하고 실행하는 방법에 대해 알아봤습니다. 이 포스팅은 아래의 링크들의 자료를 보며 현재 kubernetes client 버전에 맞게 수정하며 공부한 내용을 정리한 포스팅입니다. 좀 더 자세한 내용은 아래의 링크를 통해 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한 해당 포스팅에서 사용된 모든 코드는 github에 올려뒀습니다. :)&lt;/p&gt;
&lt;figure id=&quot;og_1617885157181&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;ooeunz/blog-code&quot; data-og-description=&quot;Contribute to ooeunz/blog-code development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/custom-controller/mycontroller&quot; data-og-url=&quot;https://github.com/ooeunz/blog-code&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dV4nnf/hyJPkFP9a1/WJ7jjxlKuYeFTI3kqq5r81/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320&quot;&gt;&lt;a href=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/custom-controller/mycontroller&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/custom-controller/mycontroller&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dV4nnf/hyJPkFP9a1/WJ7jjxlKuYeFTI3kqq5r81/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;ooeunz/blog-code&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Contribute to ooeunz/blog-code development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;figure id=&quot;og_1617884762575&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;if(kakao)2020&quot; data-og-description=&quot;오늘도 카카오는 일상을 바꾸는 중&quot; data-og-host=&quot;if.kakao.com&quot; data-og-source-url=&quot;https://if.kakao.com/session/101&quot; data-og-url=&quot;https://if.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dIBX6f/hyJPg4uzVX/E5yvSruXN7ZcsdyLRBmGOk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://if.kakao.com/session/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://if.kakao.com/session/101&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dIBX6f/hyJPg4uzVX/E5yvSruXN7ZcsdyLRBmGOk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;if(kakao)2020&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;오늘도 카카오는 일상을 바꾸는 중&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;if.kakao.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1617884682865&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Writing Kubernetes Sample Controller in Java&quot; data-og-description=&quot;I recently saw this Sample Controller repository on kubernetes Github repository. This just implements a simple controller for watching&amp;hellip;&quot; data-og-host=&quot;itnext.io&quot; data-og-source-url=&quot;https://itnext.io/writing-kubernetes-sample-controller-in-java-c8edc38f348f&quot; data-og-url=&quot;https://itnext.io/writing-kubernetes-sample-controller-in-java-c8edc38f348f&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cucBwK/hyJNXyJi94/kgUsk84wKxvQ1BJJcKhAFk/img.png?width=581&amp;amp;height=244&amp;amp;face=0_0_581_244,https://scrap.kakaocdn.net/dn/boJBiY/hyJPmjmeDg/ukXjNHvN5y91LFErOf3nk0/img.png?width=60&amp;amp;height=29&amp;amp;face=0_0_60_29,https://scrap.kakaocdn.net/dn/vKSPa/hyJNWNjWqa/FjlSrvMaJWGJKdm4wSA05k/img.png?width=60&amp;amp;height=28&amp;amp;face=0_0_60_28&quot;&gt;&lt;a href=&quot;https://itnext.io/writing-kubernetes-sample-controller-in-java-c8edc38f348f&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://itnext.io/writing-kubernetes-sample-controller-in-java-c8edc38f348f&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cucBwK/hyJNXyJi94/kgUsk84wKxvQ1BJJcKhAFk/img.png?width=581&amp;amp;height=244&amp;amp;face=0_0_581_244,https://scrap.kakaocdn.net/dn/boJBiY/hyJPmjmeDg/ukXjNHvN5y91LFErOf3nk0/img.png?width=60&amp;amp;height=29&amp;amp;face=0_0_60_29,https://scrap.kakaocdn.net/dn/vKSPa/hyJNWNjWqa/FjlSrvMaJWGJKdm4wSA05k/img.png?width=60&amp;amp;height=28&amp;amp;face=0_0_60_28');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Writing Kubernetes Sample Controller in Java&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;I recently saw this Sample Controller repository on kubernetes Github repository. This just implements a simple controller for watching&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;itnext.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Kubernetes</category>
      <category>CRD</category>
      <category>custom controller</category>
      <category>Custom Resource</category>
      <category>custom resource define</category>
      <category>fabirc8</category>
      <category>Java</category>
      <category>kubernetes client</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/148</guid>
      <comments>https://ooeunz.tistory.com/148#entry148comment</comments>
      <pubDate>Thu, 8 Apr 2021 21:34:23 +0900</pubDate>
    </item>
    <item>
      <title>[Error Log] Elasticsearch: ERROR: [1] bootstrap checks failed[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]</title>
      <link>https://ooeunz.tistory.com/147</link>
      <description>&lt;p&gt;Elasticsearch 5.0 이후부터 config 내 network.hosk가 loopback이 아닌 경우 bootstrape 체크 시 아래와 같은 에러가 발생하며 elasticsearch가 정상적으로 뜨지 않는 문제가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613605958092&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ERROR: [1] bootstrap checks failed
[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p&gt;Elasticsearch는&amp;nbsp;&lt;b&gt;mmapfs&lt;/b&gt;라는 디렉토리를 사용하여 색인하는데, 이때 &lt;u&gt;os가 제공하는 &lt;span style=&quot;color: #333333;&quot;&gt;mmap 수 &lt;/span&gt;제한이 너무 낮으면 메로리 부족 예외가 발생&lt;/u&gt;하게 됩니다. 위와 같은 문제가 발생했다면 아래의 명령어로 현재 서버의 vm.max_map_count가 몇인지 확인해봅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613605922953&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# vm.max_map_count 수 확인
sysctl vm.max_map_count

# 또는
cat /proc/sys/vm/max_map_count&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p&gt;OS에서 기본 제공되는 vm.max_map_count 값을 &lt;span&gt;262144 이상으로 변경해주면 됩니다. 아래의 명령어를 통해서 값을 변경할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1613606119651&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# vm.max_map_count 값을 변경 (root 권한이 필요할 수 있음)
sysctl -w vm.max_map_count=262144

# 또는
/usr/sbin/sysctl -w vm.max_map_count=262144&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;하지만 이 방법은 영구적인 방법은 아니며 재부팅 이후에 초기화됩니다. 따라서 영구적으로 이 값을 설정하려면 vm.max_map_count 값을 아래의 경로에 입력해주고 리붓해주도록 합니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1613606243657&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/etc/sysctl.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;좀 더 자세한 내용은 아래의 elasticsearch 공식 도큐먼트에서 찾아볼 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1613606279488&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Virtual memory | Elasticsearch Reference [7.11] | Elastic&quot; data-og-description=&quot;Elasticsearch uses a mmapfs directory by default to store its indices. The default operating system limits on mmap counts is likely to be too low, which may result in out of memory exceptions. On Linux, you can increase the limits by running the following &quot; data-og-host=&quot;www.elastic.co&quot; data-og-source-url=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html&quot; data-og-url=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cmaGxD/hyJjLcVPON/vAfpp8k31hpPY9W2JYMsi0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200&quot;&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cmaGxD/hyJjLcVPON/vAfpp8k31hpPY9W2JYMsi0/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Virtual memory | Elasticsearch Reference [7.11] | Elastic&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Elasticsearch uses a mmapfs directory by default to store its indices. The default operating system limits on mmap counts is likely to be too low, which may result in out of memory exceptions. On Linux, you can increase the limits by running the following&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;www.elastic.co&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Error Log</category>
      <category>bootstrap checks failed</category>
      <category>elasticsearch</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/147</guid>
      <comments>https://ooeunz.tistory.com/147#entry147comment</comments>
      <pubDate>Thu, 18 Feb 2021 08:59:17 +0900</pubDate>
    </item>
    <item>
      <title>[Prometheus] Exporter 배포하기 (node-exporter, kube-state-metrics, actuator)</title>
      <link>https://ooeunz.tistory.com/145</link>
      <description>&lt;p&gt;전 포스팅에서 봤듯이 prometheus는 exporter를 배포하지 않더라도 이미 많은 metric을 수집하고 있다는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;특히 kubernetes 안에 배포된 container와 기본적인 node에 관한 metric도 수집되는 것을 알 수 있는데, 이는 kubernetes가 자체적으로 cluster 내의 모든 노드에 metric을 수집하는 &lt;b&gt;cAdvisor&lt;/b&gt;라는 &lt;span&gt;모니터링 에이전트를 배포하기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;cAdvisor만으로도 많은 data를 얻을 수 있지만, 이번 포스팅에서는 더 다양한 exporter를 배포하여 cAdvisor에서 수집하지 않는 metric을 추가적으로 scrape 해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;해당 포스팅에선 아래의 version을 사용했습니다.&lt;br /&gt;&lt;br /&gt;kubernetes:v1.17.12&lt;br /&gt;prom/prometheus:v2.20.1&lt;br /&gt;prom/node-exporter:v1.0.1&lt;br /&gt;quay.io/coreos/kube-state-metrics:v1.8.0&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  Node-exporter&lt;/h2&gt;
&lt;p&gt;&lt;u&gt;Node-exporter는 Cluster에 존재하는 노드마다 하나씩 배포되어 해당 노드에서 발생하는 metric을 수집하는 exporter입니다.&lt;/u&gt; Node-exporter는 노드의 cpu 사용률, memory 사용률, disk 사용률, network bandwidth와 같은 &lt;span style=&quot;color: #333333;&quot;&gt;hardware 단에서 발생하는 metric을 수집하게 됩니다. 노드마다 하나씩 고유하게 배포&lt;/span&gt;되어야 하기 때문에 &lt;b&gt;daemonset&lt;/b&gt;으로 배포하도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613114676897&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# node-exporter-service.yaml

apiVersion: v1
kind: Service
metadata:
  annotations:
    prometheus.io/scrape: &quot;true&quot;
  name: node-exporter-http
  namespace: monitoring
  labels:
    app: node-exporter
spec:
  type: ClusterIP
  selector:
    app: node-exporter
  ports:
    - name: scrape
      port: 9100
      protocol: TCP&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1613114693689&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# node-exporter-daemonset.yaml

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
  labels:
    app: node-exporter
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      hostNetwork: true
      hostIPC: true
      hostPID: true
      containers:
        - name: node-exporter
          image: prom/node-exporter:v1.0.1
          imagePullPolicy: IfNotPresent
          args:
            - --path.procfs=/host/proc
            - --path.sysfs=/host/sys
          resources:
            requests:
              cpu: 10m
              memory: 100Mi
            limits:
              cpu: 100m
              memory: 100Mi
          ports:
            - name: scrape
              containerPort: 9100
              hostPort: 9100
          volumeMounts:
            - mountPath: /host/proc
              name: proc
              readOnly: true
            - mountPath: /host/sys
              name: sys
              readOnly: true
      volumes:
        - name: proc
          hostPath:
            path: /proc
            type: &quot;&quot;
        - name: sys
          hostPath:
            path: /sys
            type: &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  kube-state-metrics&lt;/h2&gt;
&lt;p&gt;&lt;u&gt;kube-state-metrics는 kubernetes&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/u&gt;&lt;span&gt;&lt;u&gt;클러스터 내부의 Pod가 사용 중인 리소스 metric과 네트워크 I/O, Deployments 수, Pod 수 등의 다양한 정보를 수집합니다.&lt;/u&gt;&lt;span&gt; kube-state-metrics는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;모든 node에 접근하여 해당 node의 metric을 직접 수집하는 node-exporter와는 달리,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;kube-state-metrics는 kubernetes가 이미 갖고 있는 데이터를 kubernetes api 서버로부터 요청하여 가져오기 때문에 cluster 내에 단 하나의 component만 배포합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1613107424975&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# kube-state-metrics-rbac.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kube-state-metrics
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kube-state-metrics
subjects:
  - kind: ServiceAccount
    name: kube-state-metrics
    namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kube-state-metrics
rules:
  - apiGroups:
      - &quot;&quot;
    resources: [&quot;configmaps&quot;, &quot;secrets&quot;, &quot;nodes&quot;, &quot;pods&quot;, &quot;services&quot;, &quot;resourcequotas&quot;, &quot;replicationcontrollers&quot;, &quot;limitranges&quot;, &quot;persistentvolumeclaims&quot;, &quot;persistentvolumes&quot;, &quot;namespaces&quot;, &quot;endpoints&quot;]
    verbs: [&quot;list&quot;,&quot;watch&quot;]
  - apiGroups:
      - extensions
    resources: [&quot;daemonsets&quot;, &quot;deployments&quot;, &quot;replicasets&quot;, &quot;ingresses&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - apps
    resources: [&quot;statefulsets&quot;, &quot;daemonsets&quot;, &quot;deployments&quot;, &quot;replicasets&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - batch
    resources: [&quot;cronjobs&quot;, &quot;jobs&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - autoscaling
    resources: [&quot;horizontalpodautoscalers&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - authentication.k8s.io
    resources: [&quot;tokenreviews&quot;]
    verbs: [&quot;create&quot;]
  - apiGroups:
      - authorization.k8s.io
    resources: [&quot;subjectaccessreviews&quot;]
    verbs: [&quot;create&quot;]
  - apiGroups:
      - policy
    resources: [&quot;poddisruptionbudgets&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - certificates.k8s.io
    resources: [&quot;certificatesigningrequests&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - storage.k8s.io
    resources: [&quot;storageclasses&quot;, &quot;volumeattachments&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - admissionregistration.k8s.io
    resources: [&quot;mutatingwebhookconfigurations&quot;, &quot;validatingwebhookconfigurations&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
  - apiGroups:
      - networking.k8s.io
    resources: [&quot;networkpolicies&quot;]
    verbs: [&quot;list&quot;, &quot;watch&quot;]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kube-state-metrics-account
  namespace: monitoring&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1613107332067&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# kube-state-metrics.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: kube-state-metrics
  name: kube-state-metrics
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kube-state-metrics
  template:
    metadata:
      labels:
        app: kube-state-metrics
    spec:
      containers:
        - name: kube-state-metrics
          image: quay.io/coreos/kube-state-metrics:v1.8.0
          resources:
            requests:
              cpu: 500m
              memory: 2Gi
            limits:
              cpu: 500m
              memory: 2Gi
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 5
          ports:
            - containerPort: 8080
              name: scrape
            - containerPort: 8081
              name: telemetry
          readinessProbe:
            httpGet:
              path: /
              port: 8081
            initialDelaySeconds: 5
            timeoutSeconds: 5
      nodeSelector:
        kubernetes.io/os: linux
      serviceAccountName: kube-state-metrics-account
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  Actuator (jmx-exporter)&lt;/h2&gt;
&lt;p&gt;이번엔 JVM에서 발생하는 metric을 수집하기 위해 Jmx-exporter를 사용해 보겠습니다. Jvm에서 발생되는 metric을 수집하는 방법에는 외장 jmx-exporter를 사용하는 방법과 java agent를 사용하는 방법 두 가지가 있습니다. 해당 포스팅에선 Actuator라는 java agent를 이용해서 metric을 수집해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Actuator는 Spring Boot 애플리케이션에서 &lt;/span&gt;maven 또는 gradle을 이용해서 손쉽게 배포할 수 있습니다. 해당 예시에선 maven을 이용해서 진행해 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Actuator 설정하기&lt;/h4&gt;
&lt;p&gt;먼저 아래와 같이 actuator와 micrometer를 dependencies로 추가해주도록 합니다.&lt;/p&gt;
&lt;p&gt;그런 다음 아래와 같이 &lt;b&gt;management.endpoints.web.exposure.include&lt;/b&gt; 옵션에 prometheus를 추가해줌으로써 metric을 prometheus 형식에 맞게 export 시켜 줄 수 있습니다. 이외에도 health도 함께 넘겨주었는데, 해당 옵션은 kubernetes에서 pod에 대한 health check를 할 때 사용하는 옵션입니다. (아래에서 다시 설명하겠습니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1613107822589&quot; class=&quot;.properties&quot; data-ke-language=&quot;.properties&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// porm.xml

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.micrometer&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;micrometer-registry-prometheus&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1613107887690&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.properties

spring.application.name=noah
management.metrics.tags.application=${spring.application.name}
management.endpoints.web.exposure.include=prometheus,health&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Boot 배포하기&lt;/h4&gt;
&lt;p&gt;이제 본격적으로 Spring Boot 애플리케이션을 kubernetes에 배포해보도록 하겠습니다. source code는 아래와 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;해당 예시에서 사용하는 docker image는 변경되거나 삭제할 수 있으므로, source code의 형식만 눈에 익히시고, test는 다른 이미지로 하시는걸 추천드립니다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1613108607289&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tomcat-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: noah-deploy
  namespace: default
  labels:
    app: noah
spec:
  replicas: 1
  selector:
    matchLabels:
      app: noah
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: noah
    spec:
      containers:
        - name: noah
          image: ooeunz/noah:0.0.1-SNAPSHOT
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              protocol: TCP
            - containerPort: 8999
              protocol: TCP
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
            limits:
              cpu: 100m
              memory: 100Mi
          livenessProbe:
            httpGet:
              path: /actuator/health
              port: 8999
            initialDelaySeconds: 20
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: 8999
            initialDelaySeconds: 5
            periodSeconds: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1613108622596&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tomcat-service.yaml

apiVersion: v1
kind: Service
metadata:
  annotations:
    prometheus.io/scrape: &quot;true&quot;
    prometheus.io/port: &quot;8999&quot;
    prometheus.io/path: &quot;/actuator/prometheus&quot;
  name: noah-http
  namespace: noah-test
spec:
  selector:
    app: &quot;noah&quot;
  ports:
    - port: 8080
      name: application
      protocol: TCP
      targetPort: 8080
    - port: 8999
      name: scrape
      protocol: TCP
      targetPort: 8999&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Scarpe 된 metric 확인하기&lt;/h4&gt;
&lt;p&gt;&lt;u&gt;jmx-exporter는 기본적으로 tomcat의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;8999&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;port를 이용해서 metric을 export 시킵니다.&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 service에서 8080과 8999 port를 모두 열어주도록 하겠습니다. 뿐만 아니라&lt;span&gt; &lt;u&gt;jmx-exporter&lt;/u&gt;&lt;/span&gt;의 경우 &quot;/actuator/prometheus&quot; 라는 경로에 metric이 노출되므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;metricprometheus.io/path&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;역시 설정해주도록 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;우리는 위에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;management.endpoints.web.exposure.include&amp;nbsp;&lt;/b&gt;값에 health를 함께 넣어 주었는데, 해당 옵션을 주면 &quot;/actuator/health&lt;span style=&quot;color: #333333;&quot;&gt;&quot;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;경로로 GET 요청을 보낼 경우 아래와 같은 경량 응답 값을 받을 수 있습니다. 해당 옵션을 이용하면 응답 값의 데이터 크기도 적으며, health check를 위해 따로 api를 만들지 않아도 된다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-12 오후 2.59.12.png&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;1652&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TOt43/btqWVQ16KQ4/kknpSXFNgAhfvm6zk7Bp6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TOt43/btqWVQ16KQ4/kknpSXFNgAhfvm6zk7Bp6K/img.png&quot; data-alt=&quot;GET&amp;amp;amp;nbsp;http://localhost:8999/actuator/health&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TOt43/btqWVQ16KQ4/kknpSXFNgAhfvm6zk7Bp6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTOt43%2FbtqWVQ16KQ4%2FkknpSXFNgAhfvm6zk7Bp6K%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-12 오후 2.59.12.png&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;1652&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;GET&amp;nbsp;http://localhost:8999/actuator/health&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음은 JVM에서 발생하는 metric을 확인해보도록 하겠습니다. &quot;&lt;b&gt;/actuator/prometheus&lt;/b&gt;&quot; 로 GET 요청을 해보도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;GET 요청을 보내면 JVM에서 생성되는 많은 metric이 scrape 되고 있다는 것을 알 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;jmx-exporter가 수집하는 metric을 살펴보면&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;cAdvisor와 달리 &lt;u&gt;jvm안에서 발생하는 metric을 수집한다는 것을 알 수 있습니다.&lt;/u&gt; 대표적인 metric으로는 heap메모리의 사용률, gc 수행 시간 및 실행 수, thread 수와 같은 metric이 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-12 오후 3.03.46.png&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;2032&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIS47r/btqWUL7X0bF/QBLnzgtQdfSYvyC3zPkDkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIS47r/btqWUL7X0bF/QBLnzgtQdfSYvyC3zPkDkk/img.png&quot; data-alt=&quot;GET http://localhost:8999/actuator/prometheus&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIS47r/btqWUL7X0bF/QBLnzgtQdfSYvyC3zPkDkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIS47r%2FbtqWUL7X0bF%2FQBLnzgtQdfSYvyC3zPkDkk%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-12 오후 3.03.46.png&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;2032&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;GET http://localhost:8999/actuator/prometheus&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Jmx-exporter의 추가 기능&lt;/h4&gt;
&lt;p&gt;사실 jmx-exporter&lt;span style=&quot;color: #333333;&quot;&gt;가 수집하는 metric은 이게 다가 아닙니다. Jmx-exporter는 Spring Boot에서 사용하는 third party library에 관하여서도 metric을 수집하는데 대표적인 라이브러리는 아래와 같습니다. (더 자세한 내용은 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics-meter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring 공식 도큐먼트&lt;/a&gt;를 참고합니다.)&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kafka.*&lt;/li&gt;
&lt;li&gt;hystrix.*&lt;/li&gt;
&lt;li&gt;hicaricp.*&lt;/li&gt;
&lt;li&gt;rabbitmq.*&lt;/li&gt;
&lt;li&gt;cache.*&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Kafka Libray를 예로 들어보겠습니다. (kafka가 pub/sub 모델로 이루어졌다는 것 정도만 알고 읽으시면 됩니다)&lt;/p&gt;
&lt;p&gt;대표적인 Exporter의 종류 중엔 &lt;b&gt;kafka-exporter&lt;/b&gt;라는 exporter가 존재합니다. 해당 exporter는 kafka에 붙어서 발생하는 metric을 수집하는 exporter입니다. 하지만 kafka-exporter는 kafka broker 붙어서 발생하는 metric을 scrape 하기 때문에 broker를 subscribe 하고 있는 애플리케이션과 publish 하고 있는 애플리케이션에 관하여서는 metric을 수집할 수 없습니다. 때문에 당연히 수집하는 metric의 종류가 제한적일 수밖에 없기 때문에 추가적인 exporter가 필요할 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;jmx-exporter는 이러한 불편함을 자동화 해주는데, &lt;u&gt;jmx-exporter에 굳이 따로 설정을 해주지 않더라도 애플리케이션이&amp;nbsp;&lt;b&gt;kafka.*&lt;/b&gt; 라이브러리를 사용하고 있다면 kafka와 관련된 metric도 수집해주게 됩니다.&lt;/u&gt; (정말 기가 막히지 않나요...?)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  어떻게 따로 설정하지 않았음에도 metric을 수집할 수 있는 걸까?&lt;/h4&gt;
&lt;p&gt;&quot;jmx-exporter가 좋은 건 알겠어. 하지만 어떻게 kafka와 관련된 export 설정을 해주지 않았는데도 metric을 수집할 수 있는 거지?&quot;라는 의문이 드실 것 같습니다. (궁금하지 않으셨다면 죄송합니다...ㅎ)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이와 관련된 의문은 micrometer의 소스 코드를 뜯어보면 알 수 있는데, 대표적으로 kafka consumer 라이브러리의 metric을 수집하는 &lt;span&gt;&lt;a href=&quot;https://github.com/micrometer-metrics/micrometer/blob/master/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaConsumerMetrics.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;KafkaConsumerMetrics&lt;/a&gt;라는 Class를 살펴보겠습니다. &lt;u&gt;JMX는 기본적으로 Spring에 존재하는 모든 bean을 jmx MBeanServer라는 곳에 등록&lt;/u&gt;하게 됩니다. &lt;u&gt;여기서 노출되는 metric을 KafkaConsumerMetrics (&lt;span&gt;micrometer v1.1.0부터 지원하고 custom metric을 등록하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;MeterBinder&lt;/u&gt;&lt;span&gt;&lt;u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;interface를 구현한 클래스)의 객체가 수집하여 custom metric으로 등록&lt;/u&gt;하기 때문에 따로 metric과 관련된 export 설정을 해주지 않아도 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;이때 지정된 메트릭 이름은 kafka 0.11.0 버전에 지정된 이름을 기반으로 한다고 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  그 밖의 다양한 exporter&lt;/h2&gt;
&lt;p&gt;사실 해당 포스팅에서 다룬 exporter는 정말 극히 일부입니다. 이외에도 다양한 exporter들이 open source로 존재합니다. 오픈소스로 공개된 exporter가 없다면 직접 custom exporter를 구현할 수도 있지만, 웬만하면 누군가 만들어둔 exporter가 존재하니까 서치 해보시길 추천드립니다. 아래는 대표적으로 사용되는 exporter의 종류입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prometheus/node_exporter&quot;&gt;node-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/google/cadvisor&quot;&gt;cAdvisor (docker-container)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prometheus/mysqld_exporter&quot;&gt;mysql-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prometheus-community/windows_exporter&quot;&gt;wmi-exporter (window server)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prometheus/mysqld_exporter&quot;&gt;mysql-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wrouesnel/postgres_exporter/&quot;&gt;postgresql-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oliver006/redis_exporter&quot;&gt;redis-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dcu/mongodb_exporter&quot;&gt;mongodb-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/justwatchcom/elasticsearch_exporter&quot;&gt;elasticsearch-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/danielqsj/kafka_exporter&quot;&gt;kafka-exporter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;여기까지 exporter를 이용하여 더 다양한 metric을 수집하는 방법에 대해서 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;오늘도 이 포스팅 쓰는 데 온종일이 걸렸네요.  &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음 포스팅에선 클러스터에 문제가 발생했을 경우 alertmanager를 이용해 알림을 전송하는 방법을 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 해당 포스팅에서 사용된 source code는 아래의 github에서 확인 하실 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1613114323066&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;ooeunz/blog-code&quot; data-og-description=&quot;Contribute to ooeunz/blog-code development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/monitoring&quot; data-og-url=&quot;https://github.com/ooeunz/blog-code&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/t1YD0/hyJetRRFnN/vfVXXiKCfAlDkeenKt7jGk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320&quot;&gt;&lt;a href=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/monitoring&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/monitoring&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/t1YD0/hyJetRRFnN/vfVXXiKCfAlDkeenKt7jGk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;ooeunz/blog-code&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Contribute to ooeunz/blog-code development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Prometheus</category>
      <category>actuator</category>
      <category>cadvisor</category>
      <category>exporter 종류</category>
      <category>jmx exporter</category>
      <category>kafka exporter</category>
      <category>kube-state-metrics</category>
      <category>Metric</category>
      <category>Micrometer</category>
      <category>node exporter</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/145</guid>
      <comments>https://ooeunz.tistory.com/145#entry145comment</comments>
      <pubDate>Fri, 12 Feb 2021 16:19:13 +0900</pubDate>
    </item>
    <item>
      <title>[Prometheus] kubernetes 환경에 prometheus 구축하기</title>
      <link>https://ooeunz.tistory.com/139</link>
      <description>&lt;p&gt;명절에 짬을 내서 최근 공부하고 직접 구축해보았던 prometheus 포스팅을 작성해보려고 합니다. 이번 포스팅에선 Prometheus에 대한 간략한 소개와 Kubernetes 환경에서 모니터링 시스템을 구축하는 방법으로 진행해보려고 합니다. 사실 prometheus는 많은 회사에서 사용하고 있고 kubernetes의 사실상 표준 모니터링 시스템으로 사용되고 있습니다. 그럼에도 불구하고, 한국어로 된 자료가 많이 없고, 번역된 책 조차도 yes24 기준 2권밖에 존재하지 않는 기술입니다.  &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그래서 개념만 제대로 이해하고 있다면 구축하고 사용하는 것이 그리 어렵지 않음에도 불구하고, 관련 레퍼런스를 찾기 힘들어서 helm을 사용하지 않고는 직접 구축하고 커스텀 하는 데에 어려움이 있었습니다. (저만 그런진 모르겠습니다만...)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그래서 kubernetes 환경에 prometheus를 이용해 모니터링 시스템을 구축하고 관련된 개념을 살펴보도록 포스팅을 진행해보도록 하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;해당 포스팅에선 아래와 같은 version을 사용하였습니다.&lt;br /&gt;&lt;br /&gt;kubernetes version : v1.17.12&lt;br /&gt;quay.io/prometheus/alertmanager:v0.21.0&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  Prometheus란?&lt;/h2&gt;
&lt;p&gt;프로메테우스는 SoundCloud사에서 만든 &lt;b&gt;오픈소스 모니터링 툴&lt;/b&gt;입니다. go언어로 만들어졌으며, 지금은 독립된 오픈소스 프로젝트로 개발되고 있으며, kubernetes 환경에서 모니터링 하기 원하는 리소스로부터 metirc을 수집하고 해당 메트릭을 이용해서 모니터링하는 기능을 제공합니다. 또한 이상 증세가 발생했을 때 slack이나 여타 다른 webhook을 이용해서 알림을 주는 등 다양한 기능을 제공하고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;prometheus에 대한 자세한 내용은 아래의 공식 레퍼런스에서 확인할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1601621011132&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Prometheus - Monitoring system &amp;amp; time series database&quot; data-og-description=&quot;Some of our users include:&quot; data-og-host=&quot;prometheus.io&quot; data-og-source-url=&quot;https://prometheus.io/&quot; data-og-url=&quot;https://prometheus.io/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bllWqn/hyHHQtQ33O/EwljhziZB3IjkduBZyeUx0/img.png?width=1200&amp;amp;height=466&amp;amp;face=0_0_1200_466&quot;&gt;&lt;a href=&quot;https://prometheus.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prometheus.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bllWqn/hyHHQtQ33O/EwljhziZB3IjkduBZyeUx0/img.png?width=1200&amp;amp;height=466&amp;amp;face=0_0_1200_466');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Prometheus - Monitoring system &amp;amp; time series database&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Some of our users include:&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;prometheus.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  Prometheus의 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 시계열 데이터 모델&lt;/p&gt;
&lt;p&gt;prometheus는 key-value 쌍으로 이루어진 메트릭을 수집하여 유저에게 &lt;b&gt;시계열 데이터&lt;/b&gt;로 제공합니다. 여기서 시계열이란 말 그대로 &lt;b&gt;시간&lt;/b&gt;을 뜻하는 말로 '오전 6시부터 8시까지 5분 단위로'와 같은 형식으로 데이터를 제공받을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Prometheus에선 이러한 시계열 데이터를 고유하게 식별하기 위해 metric name과 label로 불리는 key-value(optional)을 사용하게 되는데 그예는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1618550695982&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;api_http_requests_total{method=&quot;POST&quot;, handler=&quot;/messages&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 데이터에서 api_http_requests_total은 metric name을 뜻하고 method=&quot;POST&quot;나 handler=&quot;/messages&quot;는 label입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. PromQL&lt;/p&gt;
&lt;p&gt;이와 같이 수집된 메트릭을 유연하게 가공하기 위해 prometheus는 &lt;b&gt;PromQL&lt;/b&gt;이라는 자체 쿼리언어를 제공하는데 문법 자체가 쉽고 간결하여 쉽게 익힐 수 있습니다. 자세한 내용은 &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/querying/basics/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 레퍼런스&lt;/a&gt;의 쿼리 사용법을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 단일 노드&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 prometheus는 분산 스토리지를 사용하지 않고 &lt;b&gt;단일 노드&lt;/b&gt;를 사용하여 수집한 메트릭을 저장합니다. default로 15일을 저장하게 되는데 수정으로 변경할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. Pull 방식&lt;/p&gt;
&lt;p&gt;prometheus는 특이하게 다른 모니터링 툴과는 다르게 &lt;b&gt;pull 방식&lt;/b&gt;을 이용합니다. 여기서 &lt;u&gt;pull 방식이란 메트릭을 직접적으로 수집하는 exporter(client)에 prometheus가 직접 접속하여 수집한 메트릭을 가지고 오는 방식&lt;/u&gt;을 뜻합니다. 기존의 모니터링 툴들은 push 방식으로 메트릭을 수집하는 client가 서버에 메트릭을 전송하는 방식과 차이가 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; Prometheus의 장단점?&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;- 장점&lt;br /&gt;pull 방식을 사용하게 되면 모든 메트릭의 정보를 중앙 서버로 보내지 않아도 되기 때문에 부하가 높은 상황에선 fail point를 비교적 예방할 수 있습니다.&lt;br /&gt;&lt;br /&gt;- 단점&lt;br /&gt;plain prometheus를&amp;nbsp;사용하는 경우&amp;nbsp;단일 노드 구조를 가지고 있기 때문에&amp;nbsp;수집해야 할 metric의 정보와 rule이 많아질수록 configuration이 복잡지고&amp;nbsp;scale out이 어렵다는 단점이 있습니다. 이러한 경우 레퍼런스에선 prometheus를 hierarchy 구조를 만들어 사용하라고 가이드하고 있지만, 쉬운 구성방법 역시 아닙니다.&lt;br /&gt;그래서 kubernetes 환경에서 여러 대의 prometheus를 사용해야 하는 상황이라면 prometheus operator와 tanos와 같은 기술들을 고려할 수 있습니다.&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj; &amp;nbsp;Architecture&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;600&quot; data-filename=&quot;architecture.png&quot; data-origin-width=&quot;1351&quot; data-origin-height=&quot;811&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JyLXr/btqJV7bnWSI/YZn7nbISBSS9UMBlwXzObK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JyLXr/btqJV7bnWSI/YZn7nbISBSS9UMBlwXzObK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JyLXr/btqJV7bnWSI/YZn7nbISBSS9UMBlwXzObK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJyLXr%2FbtqJV7bnWSI%2FYZn7nbISBSS9UMBlwXzObK%2Fimg.png&quot; width=&quot;600&quot; data-filename=&quot;architecture.png&quot; data-origin-width=&quot;1351&quot; data-origin-height=&quot;811&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 이미지는 promtheus 공식 홈페이지에서 가져온 promtehus architecture를 간결하게 표현한 이미지입니다. 아키텍처가 복잡해 보이지만 사실 그 구조는 무척 단순합니다. 위의 아키텍처에서 신경 써서 봐야 할 부분은 크게 5가지입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Exporter&lt;/p&gt;
&lt;p&gt;가장 먼저 데이터를 수집하는&amp;nbsp;&lt;b&gt;exporter&lt;/b&gt;를 살펴보겠습니다. exporter는 실질적으로 모니터링 대상으로부터 메트릭을 수집하는 컴포넌트로 데이터를 수집하여 prometheus가 exporter로 metric을 요청 했을 때 메트릭을 전송하는 역할을 하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Node-exporter를 예로 들어보자면, kubernetes 환경에 운영중인 여러 개의 node에 node-exporter가 하나씩 뜨게 됩니다. 그리고 해당 node에서 발생하는 metric (예를 들면 cpu 사용률 같은) 수집하게 됩니다. 그럼 Prometheus server는 각 node마다 떠 있는 node-exporter에게 metric을 요청하게 되고 node-exporter는 수집한 metric을 응답해주게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Exporter 역시 종류가 굉장히 많은데, 방금 예로 들었던 Node의 cpu, memory, bandwidth와 같은 metric을 수집하는 node-exporter부터 kafka의 상태를 체크하는 kafka-exporter, jvm에서 발생하는 metric을 수집하는 jmx-exporter 등등 정말 다양한 오픈소스 exporter가 존재합니다. 그러므로 상황에 따라 필요한 exporter를 찾아서 사용하실 수도 있고, 또는 직접 custom metric을 만들어서 수집하는 exporter를 개발하여 사용하실 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. Prometheus Server&lt;/p&gt;
&lt;p&gt;조금 전 exporter를 이야기 하며 잠깐 등장했던 prometheus server입니다. prometheus server는 exporter가 열어둔 http endpoint에 접속하여 exporter가 수집한 metric을 수집하고 prometheus server에 저장하게 됩니다. 이때 저장되는 metric은 분산처리가 되지 않기 때문에 단일 노드에 저장되며, prometheus는 기본적으로 15일이 지난 이전의 metric은 삭제하기 때문에, &lt;span style=&quot;color: #333333;&quot;&gt;이를 고려하여 적절한 storage 용량을 할당하는 것이 중요합니다. (retention time은 다시 다루겠지만, &lt;span&gt;storage.tsdb.retention.time 옵션을 이용해서 조정할 수 있습니다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;3. Jobs&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Prometheus Server가 HTTP endpoint에 접근하여 모니터링 대상의 metric을 수집해오도록 scrape config에 metric scrape job을 등록할 수 있습니다. 이때 등록된 job은 target url에 연결된 instance들에게서 주기적으로 metric을 수집해옵니다.&lt;/p&gt;
&lt;p&gt;때론 짧게 실행되고 종료되는 job인 경우엔 prometheus server에서 pull 하기 전에 job이 종료될 수 있습니다. 이러한 경우엔 push gateway를 사용하여 metric을 push할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. Alertmanager&lt;/p&gt;
&lt;p&gt;Prometheus에선 알림이 발생하는 rule을 정의해서 해당 조건에 부합할 경우 알림을 발생시킬 수 있습니다. 예를 들어 A노드의 CPU 사용률이 90% 이상으로 그래프가 솟고 있다면 이는 조치가 필요한 상황이 되겠죠? 그러한 상황에 개발자 혹은 devops 팀 등 알림과 관련된 팀에게 선택적으로 알림을 발송할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. Grafana&lt;/p&gt;
&lt;p&gt;Prometheus UI만으로도 많은 시각화를 지원하지만, grafana와 같은 오픈소스 시각화 툴을 이용하면 더 다양한 시각화 기능을 사용할 수 있습니다. grafana에선 PromQL을 이용해서 Prometheus에서 시각화할 metric을 선택적으로 request 하고 이를 시각화 합니다. 또한 꼭 grafana를 사용할 필요 없이 다른 시각화 툴을 사용하거나, 직접 모니터링 시각화 툴을 개발해서 사용할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj; &amp;nbsp;Prometheus 구축하기&lt;/h2&gt;
&lt;p&gt;이제 직접 prometheus를 하나씩 구성해보며 살펴보도록 하겠습니다. prometheus를 설치하는 방법은 여러 가지가 있지만, 해당 포스팅에서는 kubernetes 환경에서 docker container로 prometheus를 구성해보도록 하겠습니다. 꼭 kubernetes 환경에서 prometheus를 구성할 필요는 없지만, kubernetes와 prometheus는 대단히 궁합이 잘 맞기도 하고, 아마 이 포스팅을 찾는 대부분의 독자들이 클러스터 환경에서 prometheus를 구성할 것 같다는 예상이 되기 때문에 kubernetes 환경에서 실습을 진행하도록 하겠습니다. ㅎㅎ&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고로 kubernetes 환경이 구축되어있다는 가정하에 실습을 진행하며, 혹시 kubernetes를 아직 다뤄본 적이 없으시다면 아래의 링크에 있는 kubernetes 시리즈를 먼저 공부하고 오시는 것을 추천드리겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1601623733835&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kubernetes] 쿠버네티스의 등장 배경&quot; data-og-description=&quot;※ 본 포스팅은 Network &amp;gt; Cloud &amp;gt; Docker &amp;gt; Kubernetes 순으로 먼저 클라우드와 인프라에 관한 전반적인 지식이 수행된 다음 읽어볼 것을 추천합니다. [Docker] Docker의 개요 Docker란 무엇일까? 개발자라면 도.&quot; data-og-host=&quot;ooeunz.tistory.com&quot; data-og-source-url=&quot;https://ooeunz.tistory.com/84?category=837108&quot; data-og-url=&quot;https://ooeunz.tistory.com/84&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/os78r/hyHHK1uXbc/rF7kY18XKbMiHpKMkcLKLk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/rgaZF/hyHHLszGVa/KHb1OkVbWtzXkyROiG5VWK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bfKJVC/hyHHN4Z8Fi/9ztOPO1r0UpwiFE5AJVEX1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://ooeunz.tistory.com/84?category=837108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ooeunz.tistory.com/84?category=837108&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/os78r/hyHHK1uXbc/rF7kY18XKbMiHpKMkcLKLk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/rgaZF/hyHHLszGVa/KHb1OkVbWtzXkyROiG5VWK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bfKJVC/hyHHN4Z8Fi/9ztOPO1r0UpwiFE5AJVEX1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;[Kubernetes] 쿠버네티스의 등장 배경&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;※ 본 포스팅은 Network &amp;gt; Cloud &amp;gt; Docker &amp;gt; Kubernetes 순으로 먼저 클라우드와 인프라에 관한 전반적인 지식이 수행된 다음 읽어볼 것을 추천합니다. [Docker] Docker의 개요 Docker란 무엇일까? 개발자라면 도.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;ooeunz.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;제일 먼저 kubernetes 환경에서 prometheus를 구축 및 사용하기 위해서 rbac(&lt;span&gt;role-based access control)를 배포 해주도록 하겠습니다. 여기엔 Prometheus service accounts, ClusterRole, Clusterrolebinding, Namespace가 포함되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1613022559957&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# rbac.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: monitoring
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: monitoring
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: monitoring
  namespace: monitoring
rules:
- apiGroups: [&quot;&quot;]
  resources:
  - nodes
  - nodes/proxy
  - services
  - endpoints
  - pods
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
- apiGroups: [&quot;&quot;]
  resources:
  - configmaps
  verbs: [&quot;get&quot;]
- nonResourceURLs: [&quot;/metrics&quot;]
  verbs: [&quot;get&quot;]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: monitoring
subjects:
  - kind: ServiceAccount
    name: monitoring
    namespace: monitoring
roleRef:
  kind: ClusterRole
  name: monitoring
  apiGroup: rbac.authorization.k8s.io
---&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음으로 Prometheus Configmap을 배포합니다. configmap에는 prometheus.yaml이란 파일을 포함합니다. 해당 파일에는 prometheus를 배포하기 위해 필요한 다양한 설정들이 정의되어 있습니다. 이곳에 포함되는 대표적인 설정은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. global&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;scrape_interval: 몇 초에 한번씩 metric을 수집할 것인지에 관한 옵션입니다. 해당 값을 설정하지 않는다면 default로 1m 값이 설정됩니다.&lt;/li&gt;
&lt;li&gt;scrape_timeout: metric을 scrape하는데 time out을 얼마나 둘 것인지에 관한 옵션입니다. default는 10입니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;evaluation_interval: 몇 초에 한번씩 규칙을 평가할 것인지 확인하는 옵션입니다. default 값은 1m입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;2. rule_files&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;알림을 발생시키기 위한 조건을 나열한 파일입니다. 해당 파일에 |- 를 이용해서 바로 작성해도 상관없지만, 가독성을 유지하기 위해서 해당 예시에선 /etc/prometheus-rules 하위 경로로 파일을 분리하였습니다.&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;3. alerting&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&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;위의 rule files에 명시된 조건에 부합할 경우 알림을 전송할 alertmanager의 경로를 지정하는 옵션입니다.&lt;/li&gt;
&lt;li&gt;해당 예시에선 같은 monitoring이라는 namespace에 prometheus와 함께 배포할 것이기 때문에, 아래와 같이 static 경로를 잡아주도록 하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. scrape_configs&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;prometheus가 metric을 어떻게 scrape 할 것인지와 관련된 옵션입니다. 해당 예시는 prometheus 공식 레퍼런스를 참조했습니다. 자세한 내용은 &lt;a href=&quot;https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고하시기 바랍니다.&lt;/li&gt;
&lt;li&gt;다양한 옵션들이 있지만 한 가지만 보고 넘어가자면, &lt;b&gt;job_name&lt;/b&gt;이라는 key입니다. 이후에 prometheus를 배포하고 실제로 metric을 수집하게 되면 각 metric마다 &lt;b&gt;job&lt;/b&gt;이라는 label이 존재한다는 것을 알 수 있습니다. 즉 다시 말해서 prometheus에선 아래의 job의 조건에 알맞게 metric을 수집하게 됩니다.&lt;/li&gt;
&lt;li&gt;해당 예시에선 annotation에 `prometheus.io/scrape&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&quot;true&quot;`가 있는 경우 metric을 scrape 하도록 설정하였습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1613023096695&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# prometheus-server-conf.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: prometheus-server-conf
  namespace: monitoring
data:
  prometheus.yaml: |-
    global:
      scrape_interval: 15s
      scrape_timeout: 10s
      evaluation_interval: 15s
    rule_files:
      - &quot;/etc/prometheus-rules/*.rules&quot;
    alerting:
      alertmanagers:
      - scheme: http
        static_configs:
        - targets:
          - &quot;alertmanager-http.monitoring.svc:9093&quot;

    scrape_configs:
      - job_name: 'kubernetes-nodes'
        tls_config:
          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
        kubernetes_sd_configs:
          - role: node
        relabel_configs:
          - source_labels: [__address__]
            regex: '(.*):10250'
            replacement: '${1}:10255'
            target_label: __address__

      - job_name: 'kubernetes-service-endpoints'
        kubernetes_sd_configs:
          - role: endpoints
        relabel_configs:
          - source_labels: [__meta_kubernetes_pod_node_name]
            target_label: instance
          - source_labels: [__meta_kubernetes_pod_name]
            action: replace
            target_label: kubernetes_pod_name
          - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
            action: keep
            regex: true
          - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
            action: replace
            target_label: __scheme__
            regex: (https?)
          - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)
          - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
            action: replace
            target_label: __address__
            regex: (.+)(?::\d+);(\d+)
            replacement: $1:$2
          - action: labelmap
            regex: __meta_kubernetes_service_label_(.+)
          - source_labels: [__meta_kubernetes_namespace]
            action: replace
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_service_name]
            action: replace
            target_label: kubernetes_name

      - job_name: 'kubernetes-services'
        metrics_path: /probe
        params:
          module: [http_2xx]
        kubernetes_sd_configs:
          - role: service
        relabel_configs:
          - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
            action: keep
            regex: true
          - source_labels: [__address__]
            target_label: __param_target
          - target_label: __address__
            replacement: blackbox
          - source_labels: [__param_target]
            target_label: instance
          - action: labelmap
            regex: __meta_kubernetes_service_label_(.+)
          - source_labels: [__meta_kubernetes_namespace]
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_service_name]
            target_label: kubernetes_name

      - job_name: 'kubernetes-pods'
        kubernetes_sd_configs:
          - role: pod
        relabel_configs:
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
            action: keep
            regex: true
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)
          - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
            action: replace
            regex: ([^:]+)(?::\d+)?;(\d+)
            replacement: $1:$2
            target_label: __address__
          - action: labelmap
            regex: __meta_kubernetes_pod_label_(.+)
          - source_labels: [__meta_kubernetes_namespace]
            action: replace
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_pod_name]
            action: replace
            target_label: kubernetes_pod_name
          - source_labels: [__meta_kubernetes_pod_container_port_number]
            action: keep
            regex: 9\d{3}

      - job_name: 'kubernetes-cadvisor'
        scheme: https
        tls_config:
          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
        kubernetes_sd_configs:
          - role: node
        relabel_configs:
          - action: labelmap
          - action: labelmap
            regex: __meta_kubernetes_node_label_(.+)
          - target_label: __address__
            replacement: kubernetes.default.svc:443
          - source_labels: [__meta_kubernetes_node_name]
            regex: (.+)
            target_label: __metrics_path__
            replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor

      - job_name: 'kube-state-metrics'
        static_configs:
          - targets: ['kube-state-metrics-http.monitoring:8080']
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번엔 위의 prometheus-server-conf에서 외부의 파일 경로로 대체한 rules의 파일을 configmap으로 작성해주도록 하겠습니다. 아래의 alerting rule들은 제가 주로 사용하는 alerting rule을 선별해 둔 것이고, alerting rule의 다양한 예시는 &lt;a href=&quot;https://awesome-prometheus-alerts.grep.to/rules.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이곳&lt;/a&gt;을 참고하여 prometheus를 구성하는 환경에 맞게 세팅하도록 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1613045251122&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# prometheus-rules.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-rules
  labels:
    name: prometheus-rules
  namespace: monitoring
data:
  alert-rules.yaml: |-
    groups:
      - name: Node
        rules:
          - alert: Kubernetes PV Error
            expr: &amp;gt;
              kube_persistentvolume_status_phase{phase=~Failed|Pending, job=kube-state-metrics} &amp;gt; 0
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: Kubernetes PersistentVolume error (pv: {{ $labels.persistentvolume }})
              description: Persistent volume is in {{ $value }}
              team: devops
          
          - alert: Kubernetes PVC Pending
            expr: &amp;gt;
              kube_persistentvolumeclaim_status_phase{job=kube-state-metrics, phase=Pending} == 1
            for: 5m
            labels:
              severity: warning
            annotations:
              summary: Kubernetes PersistentVolumeClaim pending (instance: {{ $labels.instance }})
              description: PersistentVolumeClaim {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} is pending
              team: devops
          
          - alert: Kubernetes Node Ready
            expr: &amp;gt;
              kube_node_status_condition{job=kube-state-metrics, condition=Ready,status=true} == 0
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: Kubernetes Node ready (node: {{ $labels.node }})
              description: Node {{ $labels.node }} has been unready for a long time
              team: devops

          - alert: Node Out Of Memory
            expr: &amp;gt;
              ((node_memory_MemTotal_bytes{job=kubernetes-service-endpoints} - node_memory_MemFree_bytes{job=kubernetes-service-endpoints}) / node_memory_MemTotal_bytes{job=kubernetes-service-endpoints}) * 100 &amp;gt; 90
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: Node memory usage &amp;gt; 90% (instance: {{ $labels.instance }})
              description: {{ $value }}%
              team: devops

      - name: Pod
        rules:
          - alert: Container Cpu Usage
            expr: &amp;gt;
              sum(rate(container_cpu_usage_seconds_total{name!~.*prometheus.*, image!=, container!=POD, job=kubernetes-cadvisor}[5m])) by (container, namespace) / sum(container_spec_cpu_quota{name!~.*prometheus.*, image!=, container!=POD, job=kubernetes-cadvisor}/container_spec_cpu_period{name!~.*prometheus.*, image!=, container!=POD, job=kubernetes-cadvisor}) by (container, namespace) * 100 &amp;gt; 90
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: Container CPU usage &amp;gt; 90% (namespace: {{ $labels.namespace }}, container: {{ $labels.container }})
              description: {{ $value }}%

          - alert: Container Memory Usage
            expr: &amp;gt;
              (avg (container_memory_working_set_bytes{container!=POD, container!=, job=kubernetes-cadvisor}) by (container , namespace)) / (avg (container_spec_memory_limit_bytes{container!=POD, container!=, job=kubernetes-cadvisor} &amp;gt; 0 ) by (container, namespace)) * 100 &amp;gt; 90
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: Container Memory usage &amp;gt; 90% (namespace: {{ $labels.namespace }}, container: {{ $labels.container }})
              description: {{ $value }}%
              team: dev

          - alert: Kubernetes Statefulset Down
            expr: &amp;gt;
              (kube_statefulset_status_replicas_ready{job=kube-state-metrics} / kube_statefulset_status_replicas{job=kube-state-metrics}) != 1
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: Kubernetes StatefulSet down (namespace: {{ $labels.namespace }}, statefulset: {{ $labels.statefulset }})
              description: A StatefulSet went down
              team: dev

          - alert: Kubernetes Pod Not Healthy
            expr: &amp;gt;
              min_over_time(sum by (namespace, pod) (kube_pod_status_phase{job=kube-state-metrics, phase=~Pending|Unknown|Failed})[5m:]) &amp;gt; 0
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: Kubernetes Pod not healthy (namespace: {{ $labels.namespace }})(pod: {{ $labels.pod }})
              description: Pod has been in a non-ready state for longer than a minute.
              team: dev
          
          - alert: Kubernetes Job Failed
            expr: &amp;gt;
              kube_job_status_failed{job=kube-state-metrics} &amp;gt; 0
            for: 5m
            labels:
              severity: warning
            annotations:
              summary: Kubernetes Job failed (job: {{ $labels.job_name }})
              description: Job {{ $labels.namespace }} / {{ $labels.job_name }} failed to complete
              team: dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기까지 배포했으면 이제 진짜로 Prometheus를 배포해보도록 하겠습니다. 해당 예시에선 prometheus가 down 또는 delete 되더라도, 이전에 scrape 했던 metric을 보존하고 prometheus를 고유한 number로 관리하기 위해 statefulset으로 배포하도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613052330169&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# prometheus-pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: prometheus-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: &quot;/mnt/data&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1613052351255&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# prometheus-server-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: prometheus-server-http
  namespace: monitoring
  labels:
    app: prometheus
  annotations:
    prometheus.io/scrape: &quot;true&quot;
spec:
  selector:
    app: prometheus
  type: NodePort
  ports:
    - port: 9090
      protocol: TCP
      name: prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1613052374105&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# prometheus-server-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: prometheus-server
  namespace: monitoring
  labels:
    app: prometheus
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus
  serviceName: prometheus-server-http
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      serviceAccountName: monitoring
      securityContext:
        runAsUser: 0
      containers:
        - name: prometheus
          image: prom/prometheus:v2.20.1
          args:
            - &quot;--storage.tsdb.path=/prometheus&quot;
            - &quot;--storage.tsdb.retention.time=15d&quot;
            - &quot;--config.file=/etc/prometheus/prometheus.yaml&quot;
            - &quot;--web.enable-admin-api&quot;
          ports:
            - name: prometheus
              containerPort: 9090
          resources:
            requests:
              cpu: 1
              memory: 1Gi
            limits:
              cpu: 1
              memory: 1Gi
          volumeMounts:
            - name: prometheus-storage
              mountPath: /prometheus
            - name: prometheus-server-conf
              mountPath: /etc/prometheus
            - name: prometheus-rules
              mountPath: /etc/prometheus-rules
      volumes:
        - name: prometheus-server-conf
          configMap:
            defaultMode: 420
            name: prometheus-server-conf
        - name: prometheus-rules
          configMap:
            name: prometheus-rules
  volumeClaimTemplates:
    - metadata:
        name: prometheus-storage
        namespace: monitoring
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: manual
        resources:
          requests:
            storage: 20Gi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;대부분은 기본적인 kubernetes 배포와 관련된 옵션들이기 때문에 다룰 필요가 없을 것 같지만, 몇 가지 유의해서 볼 부분만 살펴보도록 하겠습니다. 먼저 securityContext입니다. 현재 `runAsUser: 0`값을 주었는데, 이는 root의 권한으로 접근한다는 뜻입니다. 사실 이는 보안 측면에선 좋은 방법은 아니지만, root 권한을 주지 않을 경우 아래와 같은 에러를 만날 수 있기 때문에 편의상 예제의 목적으로 root 권한을 부여하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613052699987&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;Error opening query log file&quot; file=/prometheus/queries.active err=&quot;open /prometheus/queries.active: permission denied&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 root 권한으로 컨테이너를 실행시키고 싶지 않다면 아래의 옵션으로 변경합니다. (구성중인 kubernetes 상황에 따라 위의 에러가 발생할 수 있습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1613052827238&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      securityContext:
        fsGroup: 2000
        runAsNonRoot: true
        runAsUser: 1000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음으로 살펴볼 부분은 args입니다. args를 통해서 prometheus가 실행될 때 여러 옵션을 줄 수 있습니다. 현재 예시에서 넣어준 args는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;--storage.tsdb.path: prometheus가 수집한 metric을 저장할 경로입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;--storage.tsdb.retention.time: prometheus가 scrape 한 metric을 며칠간 보관할지에 관한 옵션입니다. default 값은 15d입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;--config.file: prometheus의 config 파일의 경로입니다. 해당 예시에선 configmap으로 작성하여 &lt;/span&gt;volumeMounts로 주입시켜 주었습니다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;--web.enable-admin-api: prometheus의 admin api 활성화 옵션입니다. 혹여나 prometheus의 storage가 가득 찼을 경우에 api를 통해 storage를 비워주는 등의 기능을 제공합니다. 자세한 내용은 &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/querying/api/#tsdb-admin-apis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자, 이제 prometheus가 배포되었습니다. 이제 아래와 같이 port-forward를 입력한 후, localhost:9090/graph로 접근해 보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-11 오후 11.25.12.png&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;212&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oHmsk/btqWX5qnkSO/ZLhZRe3jKROaivKYBZTU8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oHmsk/btqWX5qnkSO/ZLhZRe3jKROaivKYBZTU8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oHmsk/btqWX5qnkSO/ZLhZRe3jKROaivKYBZTU8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoHmsk%2FbtqWX5qnkSO%2FZLhZRe3jKROaivKYBZTU8K%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-11 오후 11.25.12.png&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;212&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-11 오후 11.34.49.png&quot; data-origin-width=&quot;3808&quot; data-origin-height=&quot;2334&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mkcZO/btqWX5w9Vlu/QBJRjxirhAKgY68eg4LMpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mkcZO/btqWX5w9Vlu/QBJRjxirhAKgY68eg4LMpK/img.png&quot; data-alt=&quot;prometheus web ui&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mkcZO/btqWX5w9Vlu/QBJRjxirhAKgY68eg4LMpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmkcZO%2FbtqWX5w9Vlu%2FQBJRjxirhAKgY68eg4LMpK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-11 오후 11.34.49.png&quot; data-origin-width=&quot;3808&quot; data-origin-height=&quot;2334&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;prometheus web ui&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;localhost:9090/graph로 접속했을 때 위와 같은 화면이 보인다면 성공입니다. Execute 옆에 있는 버튼을 누르면 현재 prometheus가 수집하고 있는 metric의 종류를 볼 수 있고, 위의 입력창을 통해 직접 PromQL을 사용할 수도 있습니다. 또한 Graph 버튼을 눌러 그래프 형태로 metric을 보는 등 다양한 기능을 web을 통해서 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  Alertmanager란&lt;/h2&gt;
&lt;p&gt;위에서 우리는 prometheus에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;prometheus-rules.yaml 파일에 알림이 발생하는 조건을 명시했었습니다. prometheus는 해당 조건에 부합하는 경우 알림을 발생시키고 해당 알림을 alertmanager로 전송하게 됩니다. 이때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;alertmanager는&lt;u&gt;&amp;nbsp;어떤 client로 알림을 전송할 것인지&lt;span&gt;&amp;nbsp;또한&amp;nbsp;&lt;/span&gt;&lt;/u&gt;얼마나 자주 client로 알림을 전송할 것인지와 같은 알림 발송과 관련된 다양한 설정과 실질적인 알림 전송을 담당하고 있습니다.&lt;/u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Alertmanager가 지원하는 대표적인 메신저로는 slcak, email, wechat,&lt;span&gt;&amp;nbsp;&lt;/span&gt;pagerduty, pushover,&lt;span&gt;&amp;nbsp;&lt;/span&gt;opsgenie,&lt;span&gt;&amp;nbsp;&lt;/span&gt;victorops과 custom하게 사용할 수 있는 webhook이 있습니다. 해당 포스팅에선 slack을 기준으로 배포해보도록 하겠습니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1613283265966&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# alertmanager-config.yaml

kind: ConfigMap
apiVersion: v1
metadata:
  name: alertmanager
  namespace: monitoring
data:
  config.yml: |-
    global:
      resolve_timeout: 5m
      slack_api_url: &quot;&quot;
    route:
      group_by: ['alertname']
      receiver: slack
      group_wait: 10s
      group_interval: 1m
      repeat_interval: 4h

    receivers:
      - name: slack
        slack_configs:
        - channel: &quot;general&quot;
          username: &quot;Prometheus&quot;
          send_resolved: true
          title: &quot;&quot;
          text: &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;alertmanager의 configmap의 가장 기본적인 세팅부터 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;route는 default 설정이 들어가는 부분입니다. route 하위에 있는 값들은 모두 따로 설정을 변경하지 않는 한 default 값으로 알림에 적용됩니다. 위의 코드에선 알림의 반복 주기에 관한 옵션이 들어있는데 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. group_wait&lt;/p&gt;
&lt;p&gt;&lt;u&gt;첫번째 알림을 보내기 전에 동일한 문제의 알림을 줄이기 위해 대기하는 시간&lt;/u&gt;입니다. 위의 코드 기준으로 10초동안 발생하는 알림을 모은 후에 client로 전송하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. group_interval&lt;/p&gt;
&lt;p&gt;&lt;u&gt;동일한 그룹에서 새로운 알림이 배치에 추가됐을 때 추가 알림을 보내기 전까지 대기하는 시간&lt;/u&gt;입니다. 여기서 group이란 위에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;group_by&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;배열에 들어가는 값을 집계 키로 잡아 그룹핑 되는 값들을 이야기합니다. group_by에 들어갈 수 있는 값으론 알림이 발생했을 경우 포함되는 label의 종류입니다.&lt;br /&gt;예를 들어 동일한 alertname으로 이루어진 두개의 알림이 30초 간격으로 발생한다면, alertmanager는 두 개의 알림은 같은 그룹으로 인식하게 됩니다. 여기서 label의 값들마저 두 개의 알림이 모두 같다면(value 값이 같거나 다른 것은 판단 기준에 들어가지 않음), 두 개의 알림이 이전에 발생한 알림과 동일한 알림이라고 판단하고&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;repeat_interval의 시간까지 이와 같은 조건의 알림은 client로 전송하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. repeat_interval&lt;/p&gt;
&lt;p&gt;group_interval에서 동일한 알림이라고 판단한 알림에 대하여 얼마만큼의 시간이 흐른 후에 동일한 알림을 다시 client로 재전송 할 것인지에 대한 옵션입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이와 같이, 알림에 대하여 다양한 옵션이 존재하는 것은, 너무 많은 알림으로 인해 생기는 스트레스나 중복된 정보로 중요한 정보가 묻히는 것을 염려하기 때문입니다. 따라서 팀의 상황에 따라서 적절한 알림 빈도를 선택하는 것이 옳습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음은 alertmanager의 deployment와 service에 관한 코드입니다. 크게 특별한 부분은 없고, 위에서 배포한 configmap을 alertmanager에 mount 해서 args의 config.file로 넣어주도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613283303230&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# alertmanager-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: alertmanager
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: alertmanager
  template:
    metadata:
      name: alertmanager
      labels:
        app: alertmanager
    spec:
      containers:
        - name: alertmanager
          image: quay.io/prometheus/alertmanager:v0.21.0
          imagePullPolicy: Always
          resources:
            requests:
              cpu: 250m
              memory: 500Mi
            limits:
              cpu: 250m
              memory: 500Mi
          args:
            - &quot;--config.file=/etc/alertmanager/config.yml&quot;
            - &quot;--storage.path=/alertmanager&quot;
          ports:
            - name: alertmanager
              containerPort: 9093
          volumeMounts:
            - name: config-volume
              mountPath: /etc/alertmanager
            - name: alertmanager
              mountPath: /alertmanager
      volumes:
        - name: config-volume
          configMap:
            name: alertmanager
        - name: alertmanager
          emptyDir: {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1613283311390&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  annotations:
    prometheus.io/scrape: &quot;true&quot;
    prometheus.io/path: &quot;/metrics&quot;
  labels:
    name: alertmanager
  name: alertmanager-http
  namespace: monitoring
spec:
  selector:
    app: alertmanager
  type: ClusterIP
  ports:
    - name: alertmanager
      protocol: TCP
      port: 9093
      targetPort: 9093&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;여기까지 kubernetes 환경에 prometheus를 배포해보았습니다. 예제 코드를 하나하나 테스트하며 작성해서 생각보다 포스팅 시간이 오래 걸렸네요. &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다음 포스팅에선 좀 더 유용하고 다양한 metric을 얻기 위해 추가적인 exporter를 배포하는 방법에 대해서 살펴보도록 하겠습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위의 코드들은 아래의 github url에서 확인하실 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1613090471030&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;ooeunz/blog-code&quot; data-og-description=&quot;Contribute to ooeunz/blog-code development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/monitoring&quot; data-og-url=&quot;https://github.com/ooeunz/blog-code&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/t1YD0/hyJetRRFnN/vfVXXiKCfAlDkeenKt7jGk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320&quot;&gt;&lt;a href=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/monitoring&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/monitoring&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/t1YD0/hyJetRRFnN/vfVXXiKCfAlDkeenKt7jGk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;ooeunz/blog-code&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Contribute to ooeunz/blog-code development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Prometheus</category>
      <category>alertmanager</category>
      <category>grafana</category>
      <category>Kubernetes</category>
      <category>monitoring</category>
      <category>node exporter</category>
      <category>prometheus</category>
      <category>prometheus란?</category>
      <category>모니터링</category>
      <category>쿠버네티스</category>
      <category>프로메테우스</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/139</guid>
      <comments>https://ooeunz.tistory.com/139#entry139comment</comments>
      <pubDate>Fri, 12 Feb 2021 00:19:18 +0900</pubDate>
    </item>
    <item>
      <title>[개발 환경] iTerm2로 터미널 커스텀하기</title>
      <link>https://ooeunz.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에선 Mac에서 사용하는 터미널을 꾸며보도록 하겠습니다. 사실 오래전에 작성한 글이지만, 꾸준하게 조회수가 있는 포스팅이라 좀 더 상세한 내용과 최근에 추가한 커스텀 항목을 추가해서 다시 업로드하였습니다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️해당 포스팅은 다양한 기술 블로그의 iTerm2의 커스텀 글을 토대로 저의 입맛에 맞게 소스들을 선택하여 작성되었습니다. 포스팅 최하단에 참고한 블로그의 링크를 첨부하였으니 상세한 내용은 아래 링크를 확인해주시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;아래 이미지가 해당 포스팅에서 최종적으로 만들게 될 터미널의 모습입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.08.02.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1256&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bElOYO/btqVZSmFbSi/BPaP8Ed5XLJzoBmr3UGWA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bElOYO/btqVZSmFbSi/BPaP8Ed5XLJzoBmr3UGWA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bElOYO/btqVZSmFbSi/BPaP8Ed5XLJzoBmr3UGWA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbElOYO%2FbtqVZSmFbSi%2FBPaP8Ed5XLJzoBmr3UGWA1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.08.02.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1256&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  iTerm2 Install&lt;/h2&gt;
&lt;p&gt;터미널을 꾸민다고 했지만, 정확히 말하면 terminal이라는 이름의 앱을 커스텀 하는게 아니라 터미널과 같은 기능을 하지만, 더 다양한 기능을 제공해주는 iterm2라는 애플리케이션을 사용할 것입니다. iterm2를 설치하는 방법은 여러가지가 있으니까 필요에 따라 맞는 방법을 사용하시면 되겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. iterm2 홈페이지에서 다운로드&lt;/p&gt;
&lt;p&gt;첫번째로 iTrem2 공식 사이트에서 다운로드 받는 방법입니다. 아래 링크로 들어가셔서 &lt;b&gt;Download&lt;/b&gt; 버튼을 눌러서 설치해주시면 되겠습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1612676034946&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;iTerm2 - macOS Terminal Replacement&quot; data-og-description=&quot;iTerm2 by George Nachman. Website by Matthew Freeman, George Nachman, and James A. Rosen. Website updated and optimized by HexBrain&quot; data-og-host=&quot;iterm2.com&quot; data-og-source-url=&quot;https://www.iterm2.com/&quot; data-og-url=&quot;https://iterm2.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/behDAM/hyJbo4jRE4/L9uQJie0pAxLgVmCykusLK/img.jpg?width=1600&amp;amp;height=624&amp;amp;face=0_0_1600_624&quot;&gt;&lt;a href=&quot;https://www.iterm2.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.iterm2.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/behDAM/hyJbo4jRE4/L9uQJie0pAxLgVmCykusLK/img.jpg?width=1600&amp;amp;height=624&amp;amp;face=0_0_1600_624');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;iTerm2 - macOS Terminal Replacement&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;iTerm2 by George Nachman. Website by Matthew Freeman, George Nachman, and James A. Rosen. Website updated and optimized by HexBrain&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;iterm2.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  &lt;/span&gt;zsh install하기&lt;/h2&gt;
&lt;p&gt;zsh은 bash의 확장된 유닉스 셸입니다. (MacOS Catalina 버전부턴 default로 설치되어 있습니다.)&lt;/p&gt;
&lt;p&gt;zsh를 설치하기 위해선 homebrew가 필요합니다. homebrew란 MacOS에선 리눅스의 apt-get이나 yum와 같은 package manager가 입니다. Homebrew를 이용하면 &lt;span style=&quot;color: #333333;&quot;&gt;iTerm2 이외에도 다양한 애플리케이션을 명령어 하나로 설치하고 지우며 손쉽게 관리할 수 있습니다. &lt;/span&gt;혹시 사용해보시지 않으셨다면 이 기회에 사용해보시는 것도 좋을 것 같습니다. 더 자세한 내용은 &lt;a href=&quot;http://brew.sh/index_ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;iTerm2를 키신 다음, 아래의 명령어를 입력해서 homebrew를 설치합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612676333252&quot; class=&quot;Bash&quot; data-ke-language=&quot;Bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런 다음 아래의 명령어를 입력해서 &lt;b&gt;zsh&lt;/b&gt;와 &lt;b&gt;oh-my-zsh&lt;/b&gt;를 설치하겠습니다. 아래의 명령어를 차례대로 iTerm2에 입력해주시면 되겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612676933792&quot; class=&quot;Bash&quot; data-ke-language=&quot;Bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# zsh install
brew install zsh

# oh-my-zsh install
sh -c &quot;$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Color theme 적용하기&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;iTerm2에서 사용한 color theme를 고르기 위해 아래의 url로 이동하도록 하겠습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1612677632088&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Iterm Themes - Color Schemes and Themes for Iterm2&quot; data-og-description=&quot;iTerm Themes Intro This is a set of color themes for iTerm (aka iTerm2). Screenshots below and in the screenshots directory. Installation Instructions To install: Launch iTerm 2. Get the latest version at iterm2.com Type CMD+i Navigate to Colors tab Click &quot; data-og-host=&quot;iterm2colorschemes.com&quot; data-og-source-url=&quot;https://iterm2colorschemes.com/&quot; data-og-url=&quot;https://iterm2colorschemes.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bmuYI5/hyJcWZoKZn/49AYUghgrn9bazoeokijy0/img.png?width=641&amp;amp;height=371&amp;amp;face=0_0_641_371,https://scrap.kakaocdn.net/dn/cYNyzU/hyJblmcX8y/vcEkqq8nXkB7nCKfQvAbH1/img.png?width=643&amp;amp;height=367&amp;amp;face=0_0_643_367,https://scrap.kakaocdn.net/dn/eKp7m/hyJcQZa7Dc/QSVupvCqhprYQ9dK8QVKuK/img.png?width=638&amp;amp;height=362&amp;amp;face=0_0_638_362&quot;&gt;&lt;a href=&quot;https://iterm2colorschemes.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://iterm2colorschemes.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bmuYI5/hyJcWZoKZn/49AYUghgrn9bazoeokijy0/img.png?width=641&amp;amp;height=371&amp;amp;face=0_0_641_371,https://scrap.kakaocdn.net/dn/cYNyzU/hyJblmcX8y/vcEkqq8nXkB7nCKfQvAbH1/img.png?width=643&amp;amp;height=367&amp;amp;face=0_0_643_367,https://scrap.kakaocdn.net/dn/eKp7m/hyJcQZa7Dc/QSVupvCqhprYQ9dK8QVKuK/img.png?width=638&amp;amp;height=362&amp;amp;face=0_0_638_362');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Iterm Themes - Color Schemes and Themes for Iterm2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;iTerm Themes Intro This is a set of color themes for iTerm (aka iTerm2). Screenshots below and in the screenshots directory. Installation Instructions To install: Launch iTerm 2. Get the latest version at iterm2.com Type CMD+i Navigate to Colors tab Click&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;iterm2colorschemes.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;원하는 테마를 찾으면 아래와 같이 테마의 이름을 클릭하도록 합니다. 저는 그중 &lt;b&gt;snazzy&lt;/b&gt; 테마를 사용하도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.02.16.png&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;904&quot; width=&quot;600&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhYUXB/btqV19VwrYP/Cpn1qkSpiLwsLIyfIpNnWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhYUXB/btqV19VwrYP/Cpn1qkSpiLwsLIyfIpNnWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhYUXB/btqV19VwrYP/Cpn1qkSpiLwsLIyfIpNnWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhYUXB%2FbtqV19VwrYP%2FCpn1qkSpiLwsLIyfIpNnWK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.02.16.png&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;904&quot; width=&quot;600&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그럼 아래의 이미지처럼 코드로 가득한 화면이 나올텐데요. 여기서 URL을 복사하도록 합니다. 그리고 해당 color file을 download 받을 디렉토리로 이동한 후 아래와 같이 해당 file을 download 받도록 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1612678357621&quot; class=&quot;Bash&quot; data-ke-language=&quot;Bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# curl이 설치되어 있지 않은 경우
brew install curl

# util이라는 이름의 directory를 생성하고 이동
mkdir util &amp;amp;&amp;amp; cd util

# snazzy color theme를 download
# 만약 다른 color 테마를 다운로드 할 경우 curl -LO 이후에 해당 URL을 넣으면 됨
curl -LO https://raw.githubusercontent.com/mbadolato/iTerm2-Color-Schemes/master/schemes/Snazzy.itermcolors&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.13.01.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;384&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTfAJt/btqV2aUoilp/Hg2OVCaatZYvMuLHLYKrc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTfAJt/btqV2aUoilp/Hg2OVCaatZYvMuLHLYKrc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTfAJt/btqV2aUoilp/Hg2OVCaatZYvMuLHLYKrc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTfAJt%2FbtqV2aUoilp%2FHg2OVCaatZYvMuLHLYKrc1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.13.01.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;384&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 iterm preferences를 열어서 profiles -&amp;gt; colors를 들어갑니다. (또는 단축키 &lt;span style=&quot;color: #333333;&quot;&gt;⌘&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;+&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;,)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-10-11 오후 3.42.18.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLpPPW/btqyYPrJRKf/0Om21wTBJfEJEkFIOFa9JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLpPPW/btqyYPrJRKf/0Om21wTBJfEJEkFIOFa9JK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLpPPW/btqyYPrJRKf/0Om21wTBJfEJEkFIOFa9JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLpPPW%2FbtqyYPrJRKf%2F0Om21wTBJfEJEkFIOFa9JK%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-10-11 오후 3.42.18.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그리고 우측 하단에 color presets를 누른 후 import를 선택하여서 방금 전에 다운받은 snazzy 테마를 설정해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-10-11 오후 3.42.21.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y5gl0/btqy0FuE2qU/7KWGUAIYHBHeUt6NY4G4sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y5gl0/btqy0FuE2qU/7KWGUAIYHBHeUt6NY4G4sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y5gl0/btqy0FuE2qU/7KWGUAIYHBHeUt6NY4G4sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy5gl0%2Fbtqy0FuE2qU%2F7KWGUAIYHBHeUt6NY4G4sk%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-10-11 오후 3.42.21.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  T&lt;/span&gt;heme 변경하기&lt;/h2&gt;
&lt;p&gt;이번엔 iTerm2의 터미널 테마를 변경하도록 하겠습니다. 여기서 사용할 테마는 &lt;span&gt;agonster라는 테마로 현재 checkout중인 branch를 쉽게 알 수 있는 테마입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;1_GfXWnVl3VH5AkHHk9MPfaQ.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjsFq2/btqy0EP2thm/4pKSCWy3MrjF2UdCFPwkv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjsFq2/btqy0EP2thm/4pKSCWy3MrjF2UdCFPwkv0/img.png&quot; data-alt=&quot;agonster&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjsFq2/btqy0EP2thm/4pKSCWy3MrjF2UdCFPwkv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjsFq2%2Fbtqy0EP2thm%2F4pKSCWy3MrjF2UdCFPwkv0%2Fimg.png&quot; data-filename=&quot;1_GfXWnVl3VH5AkHHk9MPfaQ.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;agonster&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;iTerm2에 &lt;b&gt;vi ~/.zshrc&lt;/b&gt;를 입력합니다. 꼭 vi가 아니더라도 자신이 사용하는 에디터를 사용해서 해당 파일을 수정하셔도 상관없습니다.&lt;/p&gt;
&lt;p&gt;해당 파일에 접근해보면 아래의 이미지와 같이 &lt;b&gt;ZSH_THEME&lt;/b&gt;라는 항목을 찾으실 수 있는데, 이 부분으로 이동하신 다음 &lt;b&gt;i&lt;/b&gt;를 눌러서 수정모드로 변경한 다음&amp;nbsp;&lt;b&gt;agnoster&lt;/b&gt;로 변경해주도록 합니다. 이후&amp;nbsp;&lt;b&gt;esc&lt;/b&gt;를 누른 후에 &lt;b&gt;:wq!&amp;nbsp;&lt;/b&gt;라는 명령어를 입력해서 저장후 종료하도록 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.17.21.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1256&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UtKhH/btqV35E4xGL/qkP85UvKb1XPHGwMzVTCI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UtKhH/btqV35E4xGL/qkP85UvKb1XPHGwMzVTCI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UtKhH/btqV35E4xGL/qkP85UvKb1XPHGwMzVTCI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUtKhH%2FbtqV35E4xGL%2FqkP85UvKb1XPHGwMzVTCI1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.17.21.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1256&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;&amp;nbsp;Font 변경하기&lt;/h2&gt;
&lt;p&gt;이후로 폰트를 바꿔주도록 하겠습니다. 저는 Naver D2 font를 선호하기 때문에 아래의 링크에서 다운로드하도록 하겠습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1570777593868&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;naver/d2codingfont&quot; data-og-description=&quot;D2 Coding 글꼴. Contribute to naver/d2codingfont development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/naver/d2codingfont&quot; data-og-url=&quot;https://github.com/naver/d2codingfont&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xcRah/hyDctJw6eT/xIzEkN8YW79fqErAQg4ifk/img.png?width=150&amp;amp;height=150&amp;amp;face=0_0_150_150,https://scrap.kakaocdn.net/dn/DpV0y/hyDcBOkJxK/EpyDGOjUsJ3pndrD8BYwg1/img.png?width=1878&amp;amp;height=666&amp;amp;face=0_0_1878_666,https://scrap.kakaocdn.net/dn/eIZtSe/hyDcEddP3J/nUpmkhRvKAVifjZ7fIhtLk/img.png?width=888&amp;amp;height=350&amp;amp;face=0_0_888_350&quot;&gt;&lt;a href=&quot;https://github.com/naver/d2codingfont&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/naver/d2codingfont&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xcRah/hyDctJw6eT/xIzEkN8YW79fqErAQg4ifk/img.png?width=150&amp;amp;height=150&amp;amp;face=0_0_150_150,https://scrap.kakaocdn.net/dn/DpV0y/hyDcBOkJxK/EpyDGOjUsJ3pndrD8BYwg1/img.png?width=1878&amp;amp;height=666&amp;amp;face=0_0_1878_666,https://scrap.kakaocdn.net/dn/eIZtSe/hyDcEddP3J/nUpmkhRvKAVifjZ7fIhtLk/img.png?width=888&amp;amp;height=350&amp;amp;face=0_0_888_350');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;naver/d2codingfont&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;D2 Coding 글꼴. Contribute to naver/d2codingfont development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 iTerm preference에서 이번엔 Text 탭으로 들어가서 font를 방금 다운로드한 D2 font로 변경해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-10-11 오후 4.07.03.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRf1xm/btqyZsixsaM/FkcYTV9VSuOmlMuNZNmpNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRf1xm/btqyZsixsaM/FkcYTV9VSuOmlMuNZNmpNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRf1xm/btqyZsixsaM/FkcYTV9VSuOmlMuNZNmpNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRf1xm%2FbtqyZsixsaM%2FFkcYTV9VSuOmlMuNZNmpNK%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-10-11 오후 4.07.03.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  &lt;/span&gt;터미널에 사용자 이름 삭제하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-11-13 오후 4.29.12.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oYXHy/btqzK0rqLdi/tYExMG7R1D5oWOavhaIXK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oYXHy/btqzK0rqLdi/tYExMG7R1D5oWOavhaIXK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oYXHy/btqzK0rqLdi/tYExMG7R1D5oWOavhaIXK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoYXHy%2FbtqzK0rqLdi%2FtYExMG7R1D5oWOavhaIXK1%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-11-13 오후 4.29.12.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;터미널에 사용자 이름을 사용하는 나타내는 영역이 매우 길기 때문에 이 부분을 아래와 같이 사용자이름을 제외한 다른 영역을 지워주도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-11-13 오후 4.29.18.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQdGKX/btqzJFuM2wL/qfOTMYt9wm6IiATgpyZXVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQdGKX/btqzJFuM2wL/qfOTMYt9wm6IiATgpyZXVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQdGKX/btqzJFuM2wL/qfOTMYt9wm6IiATgpyZXVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQdGKX%2FbtqzJFuM2wL%2FqfOTMYt9wm6IiATgpyZXVk%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-11-13 오후 4.29.18.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;b&gt;vi ~/.zshrc&lt;/b&gt;를 한 이후에 아래 코드를 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1573630137500&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;prompt_context() {
  if [[ &quot;$USER&quot; != &quot;$DEFAULT_USER&quot; || -n &quot;$SSH_CLIENT&quot; ]]; then
    prompt_segment black default &quot;%(!.%{%F{yellow}%}.)$USER&quot;
  fi
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  &lt;/span&gt;New Line 적용하기&lt;/h2&gt;
&lt;p&gt;터미널의 명령어가 길어지다 보면 화면을 벗어나는 경우가 있다. 이러한 경우 터미널 입력 어를 new line으로 입력함으로 불편을 해소할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-10-11 오후 4.11.12.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHrl1z/btqyYPkYjtX/ALGobKgQhkucznhqeCzz4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHrl1z/btqyYPkYjtX/ALGobKgQhkucznhqeCzz4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHrl1z/btqyYPkYjtX/ALGobKgQhkucznhqeCzz4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHrl1z%2FbtqyYPkYjtX%2FALGobKgQhkucznhqeCzz4k%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-10-11 오후 4.11.12.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span&gt;agnoster 테마를 설치한 기준으로 쉘에 아래의 명령어를 입력해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span&gt;vi ~/.oh-my-zsh/themes/agnoster.zsh-theme&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그 후 아래를 내려가다가 build_prompt()를 찾습니다. 그리고 &lt;b&gt;prompt_newline&lt;/b&gt;을 prompt_hg와 promt_end사이에 넣어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613379252950&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;build_prompt() {
  RETVAL=$?
  prompt_status
  prompt_virtualenv
  prompt_context
  prompt_dir
  prompt_git
  prompt_bzr
  prompt_hg
  prompt_newline //이부분을 추가 꼭 순서 지켜서
  prompt_end
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 코드의 제일 하단에 내려가서 &lt;span style=&quot;color: #333333;&quot;&gt;prompt_newline() 에 대한 기능을 정의하는 코드를&lt;/span&gt;&amp;nbsp;입력해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1613379274773&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;prompt_newline() {
  if [[ -n $CURRENT_BG ]]; then
    echo -n &quot;%{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR
%{%k%F{blue}%}$SEGMENT_SEPARATOR&quot;
  else
    echo -n &quot;%{%k%}&quot;
  fi

  echo -n &quot;%{%f%}&quot;
  CURRENT_BG=''
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  &lt;/span&gt;Syntax Highlight 적용 (option)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-10-11 오후 4.17.12.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xv2Yy/btqy0GmSktQ/ktkxqL5IdAvWWAwQmuOb7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xv2Yy/btqy0GmSktQ/ktkxqL5IdAvWWAwQmuOb7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xv2Yy/btqy0GmSktQ/ktkxqL5IdAvWWAwQmuOb7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXv2Yy%2Fbtqy0GmSktQ%2FktkxqL5IdAvWWAwQmuOb7K%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-10-11 오후 4.17.12.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;syntax highlight는 사용 가능한 명령어들에 highlight를 넣어주는 기능입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;iTerm2에 다음과 같은 명령어를 입력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612679119528&quot; class=&quot;Bash&quot; data-ke-language=&quot;Bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# brew를 통해 설치해줍니다.
brew install zsh-syntax-highlighting

# ~/.zshrc에 들어가서 아래의 코드를 입력해줍니다.
vi ~/.zshrc

source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;colorscripter-code&quot; style=&quot;color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; position: relative !important; overflow: auto;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  &lt;/span&gt;쉘에 이모티콘 적용하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2019-10-11 오후 3.55.18.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vADoJ/btqy0pFF1dc/LhycIKRstk6iTobQ3g2ujk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vADoJ/btqy0pFF1dc/LhycIKRstk6iTobQ3g2ujk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vADoJ/btqy0pFF1dc/LhycIKRstk6iTobQ3g2ujk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvADoJ%2Fbtqy0pFF1dc%2FLhycIKRstk6iTobQ3g2ujk%2Fimg.png&quot; data-filename=&quot;스크린샷 2019-10-11 오후 3.55.18.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 이미지를 보면 쉘에 사용자 이름 옆에 이모티콘이 나타나는 것을 볼 수 있다. 해당 기능을 사용하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;역시나 &lt;b&gt;vi&amp;nbsp;~/.zshrc&lt;/b&gt; 명령어로 에디터를 열어줍니다. 그리고 가장 하단에 아래의 코드를 삽입한 후에 &lt;b&gt;{하고싶은이름}&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt; 부분을&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;원하는 텍스트로 바꾸도록 합니다. 저의 경우엔 noah로 변경하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1612679374993&quot; class=&quot;Bash&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;Bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;prompt_context() { 
  # Custom (Random emoji) 
  emojis=(&quot;⚡️&quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot; &quot;)
  RAND_EMOJI_N=$(( $RANDOM % ${#emojis[@]} + 1)) 
  prompt_segment black default &quot;{하고싶은이름} ${emojis[$RAND_EMOJI_N]} &quot; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이렇게 하면 터미널을 킬때마다 랜덤으로 위의 작성된 이모티콘이 적용됩니다. 또는 자기가 원하는 이모지만 나오도록 하려면 추가적으로&amp;nbsp; 위의 코드에서&amp;nbsp;&lt;b&gt;${emojis[$RAND_EMOJI_N]}&lt;/b&gt; 부분을 자신이 원하는 이모지로 채워주시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  상태바 추가&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.48.35.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;458&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beQj1r/btqV192haDu/rJZHxn8BCb4gTPlRT73tcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beQj1r/btqV192haDu/rJZHxn8BCb4gTPlRT73tcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beQj1r/btqV192haDu/rJZHxn8BCb4gTPlRT73tcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeQj1r%2FbtqV192haDu%2FrJZHxn8BCb4gTPlRT73tcK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.48.35.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;458&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;iTerm2 하단에 아래와 같이 다양한 정보들을 출력시켜줄 수 있는 기능입니다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&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;&lt;span style=&quot;color: #333333;&quot;&gt;상태바 활성: &lt;/span&gt;Preferences &amp;gt; Profiles &amp;gt; Session &amp;gt; Status bar enabled&lt;/li&gt;
&lt;li&gt;상태바 위치 설정: Preferences &amp;gt; Appearance &amp;gt; Status bar location&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.44.52.png&quot; data-origin-width=&quot;2060&quot; data-origin-height=&quot;1296&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEziWo/btqV35yif1u/XRpt7k4KLZQ54tMBQ9KGZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEziWo/btqV35yif1u/XRpt7k4KLZQ54tMBQ9KGZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEziWo/btqV35yif1u/XRpt7k4KLZQ54tMBQ9KGZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEziWo%2FbtqV35yif1u%2FXRpt7k4KLZQ54tMBQ9KGZ1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-07 오후 3.44.52.png&quot; data-origin-width=&quot;2060&quot; data-origin-height=&quot;1296&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  추가 디자인&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;iTerm2에서 &lt;b&gt;⌘&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;+&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;,&amp;nbsp;&lt;/b&gt;를 툴러서 Preference에 들어가도록 합니다. 그런 다음 아래의 추가적인 디자인을 적용해보겠습니다. 버전에 따라 옵션의 위치가 변경되고나 이름이 변경될 수 있습니다. &lt;u&gt;현재 포스팅은 3.4.4 버전을 기준으로 설명됩니다.&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1. Title bar style&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;Appearance &amp;gt; Theme: Minial&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;2. Title bar 밑에 1px 라인제거&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Appearance&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Windows&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Show line under title bar when the tab bar is not visible: 체크 안함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;3. 폰트 크기 및 줄간격 변경&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Profiles&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Text: 폰트사이즈 14로 변경&lt;/li&gt;
&lt;li&gt;Profiles&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Text: n/n 줄간격 110으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;4. Margin 수정&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Appearance &amp;gt; Panes&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Side margins: 12&lt;/li&gt;
&lt;li&gt;Appearance &amp;gt; Panes&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Top &amp;amp; bottom margins: 10&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;5. 탭 선 제거&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;In minimal theme, how prominent should the tab outline be?: 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;6. Unicode 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Profiles&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Text&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Unicode normalization form:&lt;span&gt;&amp;nbsp;&lt;/span&gt;NFC&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  참고 출처&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/harrythegreat/oh-my-zsh-iterm2%EB%A1%9C-%ED%84%B0%EB%AF%B8%EB%84%90%EC%9D%84-%EB%8D%94-%EA%B0%95%EB%A0%A5%ED%95%98%EA%B2%8C-a105f2c01bec&quot;&gt;https://medium.com/harrythegreat/oh-my-zsh-iterm2%EB%A1%9C-%ED%84%B0%EB%AF%B8%EB%84%90%EC%9D%84-%EB%8D%94-%EA%B0%95%EB%A0%A5%ED%95%98%EA%B2%8C-a105f2c01bec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://fernando.kr/15?fbclid=IwAR0y4sc8UYhcVLvU6HsgYhSIv1UkRWxq3MIpB1Gw6Pua55Z93O8uTR3yy00&quot;&gt;https://fernando.kr/15?fbclid=IwAR0y4sc8UYhcVLvU6HsgYhSIv1UkRWxq3MIpB1Gw6Pua55Z93O8uTR3yy00&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a href=&quot;https://subicura.com/2017/11/22/mac-os-development-environment-setup.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;subicura.com/2017/11/22/mac-os-development-environment-setup.html&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Forum/개발 환경</category>
      <category>Custom</category>
      <category>iTerm2 꾸미기</category>
      <category>itrem2</category>
      <category>oh my zsh</category>
      <category>Terminal</category>
      <category>zsh</category>
      <category>맥 꾸미기</category>
      <category>맥 터미널</category>
      <category>터미널 꾸미기</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/21</guid>
      <comments>https://ooeunz.tistory.com/21#entry21comment</comments>
      <pubDate>Sun, 7 Feb 2021 15:53:23 +0900</pubDate>
    </item>
    <item>
      <title>[Cert manager] SpringBoot(tomcat) HTTPS 적용하기</title>
      <link>https://ooeunz.tistory.com/144</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  주의&lt;/h4&gt;
&lt;p&gt;해당 포스팅은 단순히 Spring Boot에 HTTPS를 적용하는 것을 목적으로 하는 포스팅이 아닌 cert manager를 이해하고 kubernetes에서 https 적용을 자동화하는 것을 목적으로 합니다. 아직 cert manager에 대해 제대로 이해하고 있지 않다면 &lt;a href=&quot;https://ooeunz.tistory.com/143&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;을 참고해주시기 바랍니다. 해당 포스팅에선 &lt;a href=&quot;https://ooeunz.tistory.com/143&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;의 내용을 모두 이해하고 있다는 전제하에 포스팅을 진행합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번에는 cert-manager를 이용해서 tomcat 통신을 암호화 해보도록 하겠습니다. &quot;이전에 MySQL을 암호화하는 것처럼 쉽게 secret 파일 적용하면 되는 거 아냐?!&quot;라고 생각하실 수도 있지만, 이전과 조금 다른 부분이 있습니다. 이전에 사용했던 Certificate yaml 파일(아래의 코드)을 보면 keyEncoding 값이 &lt;b&gt;pkcs1&lt;/b&gt;로 되어져 있는 것을 확인하실 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 &lt;b&gt;PKCS&lt;/b&gt;란 &lt;u&gt;공개키 기반구조에서 인터넷을 이용해 안전하게 &lt;span style=&quot;color: #000000;&quot;&gt;정보를 교환하기 위한 제조사간 프로토콜로 RSA가 개발한 암호 작성 시스템&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입니다. cert-manager에서는 기본적으로 default key encoding 값인&amp;nbsp;&lt;b&gt;PKCS#1&lt;/b&gt;과&amp;nbsp;&lt;b&gt;PKCS#8&lt;/b&gt;만을 지원합니다. 그리고 Spring에선 &lt;b&gt;PKCS#11&lt;/b&gt;과 &lt;b&gt;PKCS#12&lt;/b&gt;만을 지원하고 있습니다. 자 이제 무엇이 문제인지 아시겠나요...?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;u&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리가 만든 secret 파일은 PKCS#1 인코딩이기 때문에 spring에 적용하기에 알맞지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 따라서 추가적으로 변환을 해주거나 cert-manager에서 제공하는 다른 기능을 사용해야 합니다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 포스팅에선 이 두 가지 방법을 모두 살펴보고 왜 cert-manager에선 PKCS#1과 PKCS#8만을 지원하는지 알아보도록 하겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;⚠️ 해당 포스팅의 예제는 이전 포스팅을 참고하여 secret 리소스가 Spring Boot deployment에 주입되어 있다고 가정하고 진행하겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1612530607268&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 이전 포스팅에서 사용했던 Certificate yaml파일

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: selfsigned-cert
  namespace: default
spec:
  secretName: selfsigned-cert-tls
  duration: 2880h # 120d
  renewBefore: 360h # 15d
  commonName: Selfsigned certificate
  isCA: false
  keySize: 4096
  keyAlgorithm: rsa
  keyEncoding: pkcs1
  usages:
    - digital signature
    - key encipherment
    - server auth
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  애플리케이션 내에서 Encoding 변환하기&lt;/h2&gt;
&lt;p&gt;첫 번째 방법으론 Spring Boot 애플리케이션 내에서 PKCS#1 인코딩 된 key들을 PKCS#12로 변환해서 사용하도록 하겠습니다. 먼저 이에 필요한 dependency를 추가하도록 하겠습니다. 해당 예시에선 maven 기반으로 예시를 진행하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1612588725135&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// poam.xml

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;de.dentrassi.crypto&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;pem-keystore&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.2.0&amp;lt;/version&amp;gt; &amp;lt;!-- check for most recent version --&amp;gt;
        &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1612588843257&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;ctron/pem-keystore&quot; data-og-description=&quot;A PKCS #1 PEM KeyStore for Java. Contribute to ctron/pem-keystore development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ctron/pem-keystore&quot; data-og-url=&quot;https://github.com/ctron/pem-keystore&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TgRPg/hyJbnpYK95/5XJMkyI1IaQGEAgWbz2wEK/img.jpg?width=400&amp;amp;height=400&amp;amp;face=165_192_277_304&quot;&gt;&lt;a href=&quot;https://github.com/ctron/pem-keystore&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ctron/pem-keystore&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TgRPg/hyJbnpYK95/5XJMkyI1IaQGEAgWbz2wEK/img.jpg?width=400&amp;amp;height=400&amp;amp;face=165_192_277_304');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;ctron/pem-keystore&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;A PKCS #1 PEM KeyStore for Java. Contribute to ctron/pem-keystore development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;dependency를 추가했으면 아래와 같이 PemKeyStoreProvider를 추가해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1612588949951&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import de.dentrassi.crypto.pem.PemKeyStoreProvider;

&amp;hellip;

public static void main(final String[] args) throws Exception {
  Security.addProvider(new PemKeyStoreProvider());
  SpringApplication.run(Application.class, args);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그런 다음 keystore.properties에 주입받은 key들을 입력해 준 다음, application.properties에서 아래와 같이 classpath를 지정해주면 SSL 적용이 완료됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612593055631&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# keystore.properties

alias=keycert
source.key=/etc/tomcat/tls/tls.key
source.cert=/etc/tomcat/tls/tls.crt&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1612593079057&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.properties

server.ssl.key-store-type=PEMCFG.MOD
server.ssl.key-store=classpath:keystore.properties
server.ssl.key-password=
server.ssl.key-alias=keycert&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Cert-manager로 jks 또는 PKCS#12 keystore 만들기&lt;/h2&gt;
&lt;p&gt;다음은 cert-manager 단에서 Spring에서 사용할 수 있도록 jks나 PKCS#12로 이루어진 keystore를 만드는 방법입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&quot;조금 전에 PKCS#12 인코딩은 지원하지 않는다고 했으면서 무슨 소리지?&quot; 라는 생각이 드실 수 있는데, &lt;/span&gt;&lt;span&gt;&lt;span&gt;정확히 말씀드리면 PKCS#12 인코딩은 지원하지 않는데 PKCS#12로 인코딩된 keystore 파일은 만들 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;왜 굳이 이렇게 지원 하느냐라는 의문이 드실 수 있는데 그건 cert manager가 내부적으로 golang을 이용하여 인증서를 생성하고 있기 때문에 그렇습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;우선 JKS와 Keystore에 대해 모르는 독자를 위해 JKS와 Keystore에 대해서 살펴보고 이 문제에 대해 이야기를 이어가도록 하겠습니다.&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  JKS와 Keystore의 차이점이 뭔가요?&lt;/h4&gt;
&lt;p&gt;JKS는 Java Key Store의 약자이고 자바 내에서 사용하는 keystore입니다. 그리고 keystore란 인증서, 비밀 키 등의 컨테이너입니다. 즉 우리가 이전 포스팅에서 만들었던 secret 파일의 데이터들이 컨테이너 혹은 객체처럼 생성된 파일이라고 생각하시면 편할 것 같습니다.&lt;/p&gt;
&lt;p&gt;이 둘은 단순히 키 저장소의 유형의 차이일 뿐 큰 차이는 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;좀 더 자세한 내용은 아래의 URL에서 확인합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1612594240906&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Difference between .keystore file and .jks file&quot; data-og-description=&quot;I have tried to find the difference between .keystore files and .jks files, yet I could not find it. I know jks is for &amp;quot;Java keystore&amp;quot; and both are a way to store key/value pairs. Is there any&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/8985685/difference-between-keystore-file-and-jks-file&quot; data-og-url=&quot;https://stackoverflow.com/questions/8985685/difference-between-keystore-file-and-jks-file&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/SVCDl/hyJbfMiJ1t/ouAYl5Jw50rvOte3Fj4nK1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/8985685/difference-between-keystore-file-and-jks-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/8985685/difference-between-keystore-file-and-jks-file&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/SVCDl/hyJbfMiJ1t/ouAYl5Jw50rvOte3Fj4nK1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Difference between .keystore file and .jks file&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;I have tried to find the difference between .keystore files and .jks files, yet I could not find it. I know jks is for &quot;Java keystore&quot; and both are a way to store key/value pairs. Is there any&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;그럼 계속해서 어째서 KeyEncoding은 PKCS#1과 PKCS#8은 지원하면서 왜 PKCS#12는 jks나 keystore만을 지원하는지 알아보도록 하겠습니다. 해당 문제에 관해서는 cert-manager의 &lt;a href=&quot;https://github.com/jetstack/cert-manager/blob/master/pkg/util/pki/generate.go&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내부 구현 코드&lt;/a&gt;를 확인하면 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래의 코드는 cert manager의 코드(114번째 라인)를&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;발췌한 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1612595024787&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// the type of key encoding and then inspecting the type of key provided.
// It only supports encoding RSA or ECDSA keys.
func EncodePrivateKey(pk crypto.PrivateKey, keyEncoding v1.PrivateKeyEncoding) ([]byte, error) {
	switch keyEncoding {
	case v1.PrivateKeyEncoding(&quot;&quot;), v1.PKCS1:
		switch k := pk.(type) {
		case *rsa.PrivateKey:
			return EncodePKCS1PrivateKey(k), nil
		case *ecdsa.PrivateKey:
			return EncodeECPrivateKey(k)
		default:
			return nil, fmt.Errorf(&quot;error encoding private key: unknown key type: %T&quot;, pk)
		}
	case v1.PKCS8:
		return EncodePKCS8PrivateKey(pk)
	default:
		return nil, fmt.Errorf(&quot;error encoding private key: unknown key encoding: %s&quot;, keyEncoding)
	}
}

// EncodePKCS1PrivateKey will marshal a RSA private key into x509 PEM format.
func EncodePKCS1PrivateKey(pk *rsa.PrivateKey) []byte {
	block := &amp;amp;pem.Block{Type: &quot;RSA PRIVATE KEY&quot;, Bytes: x509.MarshalPKCS1PrivateKey(pk)}

	return pem.EncodeToMemory(block)
}

// EncodePKCS8PrivateKey will marshal a private key into x509 PEM format.
func EncodePKCS8PrivateKey(pk interface{}) ([]byte, error) {
	keyBytes, err := x509.MarshalPKCS8PrivateKey(pk)
	if err != nil {
		return nil, err
	}
	block := &amp;amp;pem.Block{Type: &quot;PRIVATE KEY&quot;, Bytes: keyBytes}

	return pem.EncodeToMemory(block), nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드를 살펴보면 &lt;a href=&quot;https://golang.org/pkg/crypto/x509/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;x509&lt;/a&gt;.MarshalPKCS1PrivateKey() 와 같은 라이브러리&lt;span style=&quot;color: #333333;&quot;&gt;(go언어 자체로 내장하고 있는 라이브러리입니다.)&lt;/span&gt;를 호출해서 PKCS#1과 PKCS#8을 키를 생성하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;반면 PKCS#12에 관해서는 위 라이브러리에서 지원해주지 않기 때문에 cert manager는 &lt;a href=&quot;https://pkg.go.dev/software.sslmate.com/src/go-pkcs12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pkcs12&lt;/a&gt; 라이브러리를 사용합니다.&lt;/p&gt;
&lt;p&gt;왜 keyEncoding 값에 따라 라이브러리르 구분해둔지는 잘 모르겠지만... &lt;span style=&quot;color: #333333;&quot;&gt;(혹시 안다면 알려주세요  ) cert manager가 key encoding 값에 따라 certificate를 다르게 생성하는 이유에 대해서는 알 수 있었습니다.&lt;/span&gt;&lt;u&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj; JKS를 이용해서 Spring에 HTTPS 적용하기&lt;/h3&gt;
&lt;p&gt;서론이 길었습니다만... 어찌어찌 cert-manager에서 JKS와 Keystore를 왜 따로 지원하는지를 알았으니 해당 기능을 사용해서 tomcat에 HTTPS를 적용해보도록 하겠습니다. 해당 포스팅에선 JKS를 이용하도록 하겠습니다. (방법이 크게 다르지 않습니다. 옵션 하나 차이.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그러기 위해서 먼저 JKS 파일에 비밀번호를 부여해주기 위한 secret 파일 하나를 생성하도록 하겠습니다. 이 파일을 이용해서 우리는 JKS에 비밀번호를 부여하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;secret은 안에 포함하고 있는 데이터를 base64로 인코딩해서 넣어야 합니다. (자세한 내용은 &lt;a href=&quot;https://ooeunz.tistory.com/128?category=837108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참조하세요.) 해당 예시에선 password를 편의를 위해 &quot;1234&quot;로 지정하도록 하겠습니다. 그리고 이 password를 이용해서 secret을 만들기 위해서 먼저 base64 값으로 변환하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-06 오후 4.20.26.png&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;124&quot; width=&quot;300&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8bMU8/btqV2azFUoe/lJIYAzJLJLLDNbHiPKfKFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8bMU8/btqV2azFUoe/lJIYAzJLJLLDNbHiPKfKFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8bMU8/btqV2azFUoe/lJIYAzJLJLLDNbHiPKfKFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8bMU8%2FbtqV2azFUoe%2FlJIYAzJLJLLDNbHiPKfKFk%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-06 오후 4.20.26.png&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;124&quot; width=&quot;300&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이제 변환한 값을 이용해서 secret 파일을 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612596080463&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# jks-password-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: jks-password-secret
  namespace: default
type: Opaque
data:
  password-key: MTIzNA==  # 1234&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런 다음 해당 키 값을 이용해서 Certificate를 만들어보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;⚠️해당 포스팅에선 이전 포스팅을 통해서 ClusterIssuer가 생성되어져 있다고 가정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612596141800&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# selfsigned-jks.yaml

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: selfsigned-jks
  namespace: noah-test
spec:
  secretName: selfsigned-cert-jks
  duration: 2880h # 120d
  renewBefore: 360h # 15d
  commonName: ooeunz.tistory.com
  isCA: false
  keySize: 2048
  keyAlgorithm: rsa
  keyEncoding: pkcs1
  keystores:
    jks:
      create: true
      passwordSecretRef: # Password used to encrypt the keystore
        key: password-key
        name: jks-password-secret
  usages:
    - digital signature
    - key encipherment
    - server auth
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spec.keystores.passwordSecretRef를 보시면 name에 방금 생성한 secret의 이름을 지정하고 key에 jks-password-secret.yaml에서 넣은 key값이 들어간 것을 확인할 수 있습니다. &lt;u&gt;해당 데이터를 jks에 password로 사용하겠다는 뜻으로, spring에서 해당 jks를 사용할 때 아까 입력해둔 패스워드를 사용하게 됩니다.&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;⚠️만약 jks가 아니라 keystore를 사용하고 싶다면 jks가 아닌 &lt;b&gt;&lt;span&gt;pkcs12&lt;/span&gt;&lt;/b&gt;를 넣어주시면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 모든 준비가 끝났습니다. 아까와 같이 새로 만든 secret 파일을 deployment에 주입시켜 줍니다. 그런 다음, 이번에는 application.properties만 아래와 같이 변경해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612596638521&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.properties

server.ssl.enabled=true
server.ssl.key-store=/etc/tomcat/tls/keystore.jks
server.ssl.key-store-password=1234&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;조금 눈 여겨 볼 부분은, server.ssl.key-store에는 mount 해준 seccret의 경로를 잡아주고 server.ssl.key-store-password에 jks-password-secret에 넣어준 passrod를 넣어준다는 점입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;여기까지 Spring에서도 cert-manager를 이용해서 HTTPS를 적용해보았습니다. 이외에도 이전 포스팅에서 잠깐 언급했던 &lt;b&gt;isCa&lt;/b&gt; 옵션을 이용해서 namespace별로 CA를 별도로 관리하는 등 다양한 활용 방법이 있으니 참고하시면 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 아래의 URL에서 해당 포스팅에서 사용한 코드를 확인하실 수 있습니다.  &lt;/p&gt;
&lt;figure id=&quot;og_1613093372109&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;ooeunz/blog-code&quot; data-og-description=&quot;Contribute to ooeunz/blog-code development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/security&quot; data-og-url=&quot;https://github.com/ooeunz/blog-code&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1k0MO/hyJeu4lD06/cmOeaigyJYKg53HlR7JyQ0/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320&quot;&gt;&lt;a href=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/security&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/security&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1k0MO/hyJeu4lD06/cmOeaigyJYKg53HlR7JyQ0/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;ooeunz/blog-code&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Contribute to ooeunz/blog-code development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Cert manager</category>
      <category>cert manager</category>
      <category>HTTPS</category>
      <category>JKS</category>
      <category>jks keystore 차이</category>
      <category>keystore</category>
      <category>PKCS#12</category>
      <category>spring</category>
      <category>SSL</category>
      <category>TLS</category>
      <category>tomcat</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/144</guid>
      <comments>https://ooeunz.tistory.com/144#entry144comment</comments>
      <pubDate>Sat, 6 Feb 2021 16:37:45 +0900</pubDate>
    </item>
    <item>
      <title>[Cert manager] Kubernetes 통신 암호화 및 자동화 (MySQL HTTPS 적용)</title>
      <link>https://ooeunz.tistory.com/143</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Cert-manager란?&lt;/h2&gt;
&lt;p&gt;Cert-manager는 &lt;span&gt;Kubernetes 내부에서 HTTPS 통신을 위한 인증서를 생성하고, 또 인증서의 만료 기간이 되면 자동으로 인증서를 갱신해주는 역할을 하는 Certificate manager controller입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;쉽게 말해 &lt;u&gt;Kubernetes 내에서 외부에 존재하는 Issuers를 활용하거나 selfsigned Issuer를 직접 생성&lt;/u&gt;해서 생성하여 &lt;u&gt;Certificate를 생성&lt;/u&gt;하고, &lt;/span&gt;이때 생성된 Certificate를 관리하며 &lt;u&gt;인증서의 만료 시간이 가까워지면 인증서를 자동으로 갱신&lt;/u&gt;해줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Cert-manager가 사용하는 외부에 존재하는 Issuer는 아래의 이미지와 같은데,&lt;/span&gt;&lt;span&gt;&amp;nbsp;대표적인 Issuer로 무료로 사용되고 있는 &lt;b&gt;let's enscrypt&lt;/b&gt;를 많이 사용하고 있습니다. 하지만&amp;nbsp;&lt;/span&gt;&lt;span&gt;let's enscrypt를 사용한 예제를 구글링을 하면 쉽게 검색할 수 있기 때문에 해당 포스팅에서는 self-signed Issuer를 생성하고 그를 이용해 Certificate를 생성 및 관리하는 방법에 대해서 살펴보도록 하겠습니다. 그리고 마지막으로 생성된 Certificate를 이용해서 MySQL에 HTTPS를 적용해보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;해당 포스팅에선 아래와 같은 version을 사용하였습니다.&lt;br /&gt;&lt;br /&gt;kubernetes version : v1.17.12&lt;br /&gt;cert-manager version: v1.1.0&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;1072&quot; data-filename=&quot;스크린샷 2021-02-04 오후 2.41.31.png&quot; width=&quot;600&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PMDuo/btqVIandZlD/dM1GghAidVG04vSje922B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PMDuo/btqVIandZlD/dM1GghAidVG04vSje922B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PMDuo/btqVIandZlD/dM1GghAidVG04vSje922B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPMDuo%2FbtqVIandZlD%2FdM1GghAidVG04vSje922B1%2Fimg.png&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;1072&quot; data-filename=&quot;스크린샷 2021-02-04 오후 2.41.31.png&quot; width=&quot;600&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Cert-manager deploy&lt;/h2&gt;
&lt;p&gt;cert-manager는 오픈소스이며 &lt;a href=&quot;https://github.com/jetstack/cert-manager&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 공식적인 릴리즈 버전을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-04 오후 4.38.50.png&quot; data-origin-width=&quot;3950&quot; data-origin-height=&quot;1786&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD7LY1/btqVIbGrDnh/vFdDuULxSrQUzIOBXVGENK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD7LY1/btqVIbGrDnh/vFdDuULxSrQUzIOBXVGENK/img.png&quot; data-alt=&quot;jetstack / cert-manager&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD7LY1/btqVIbGrDnh/vFdDuULxSrQUzIOBXVGENK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD7LY1%2FbtqVIbGrDnh%2FvFdDuULxSrQUzIOBXVGENK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-04 오후 4.38.50.png&quot; data-origin-width=&quot;3950&quot; data-origin-height=&quot;1786&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;jetstack / cert-manager&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 이미지와 같이 Release 버전을 클릭하고 밑으로 스크롤을 내리다 보면 현재 release 버전에 해당하는 cert-manager.yaml 파일을 찾을 수 있습니다. 우리는 아래의 yaml 파일을 이용해서 kubernetes 클러스터에 cert-manager를 배포할 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-04 오후 4.41.21.png&quot; data-origin-width=&quot;1994&quot; data-origin-height=&quot;1020&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5YLbx/btqVK7Djf9F/MeWkJC4kyJjK8lVbssDEH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5YLbx/btqVK7Djf9F/MeWkJC4kyJjK8lVbssDEH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5YLbx/btqVK7Djf9F/MeWkJC4kyJjK8lVbssDEH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5YLbx%2FbtqVK7Djf9F%2FMeWkJC4kyJjK8lVbssDEH1%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-04 오후 4.41.21.png&quot; data-origin-width=&quot;1994&quot; data-origin-height=&quot;1020&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 cert-manager를 배포할 cert-manager라는 이름의 namespace를 생성하도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612426346092&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Create namespace
kubectl create namespace cert-manager&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그 후 아래의 명령어로 kuberetes에 바로 cert-manager를 배포할 수 있습니다. 아래의 명령어에선 현재 기준으로 최신 release 버전인 v1.1.0 버전을 사용하였지만, 사용하기에 따라 다른 버전을 사용하시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612423273105&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;저는 cert-manager를 선언적으로 또 다시 사용할 것을 염두해서 local에 cert-manager.yaml 파일을 받고, apply 명령어를 통해서 cert-manaer를 배포하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612425335368&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# cert-manager.yaml 파일 다운로드
curl -LO https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml

# 버전 관리를 위해 cert-manager.yaml -&amp;gt; cert-manager.1.1.0.yaml로 이름 변경
mv cert-manager.yaml cert-manager.1.1.0.yaml

# cert-manager install
kubectl apply -f cert-manager.1.1.0.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;install이 완료됐다면 `&lt;b&gt;kubectl get all&lt;/b&gt;` 명령어를 통해서 클러스터에 cert-manager가 정상적으로 설치되었는지 확인해봅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-02-04 오후 4.56.52.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;720&quot; width=&quot;700&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekCeBm/btqVQVPSmiE/CLQUt4atamlXSesFaih6rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekCeBm/btqVQVPSmiE/CLQUt4atamlXSesFaih6rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekCeBm/btqVQVPSmiE/CLQUt4atamlXSesFaih6rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekCeBm%2FbtqVQVPSmiE%2FCLQUt4atamlXSesFaih6rK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-04 오후 4.56.52.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;720&quot; width=&quot;700&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Cert-manager를 이용해 selfsigned 인증서 생성하기&lt;/h2&gt;
&lt;p&gt;Cert-manager는 기본적으로 let's enscypt가 아닌 Cluster 내부에서 사용할 수 있는 자체적으로 서명된 self-signed issuer를 생성할 수 있습니다. 여기서 Issuer란 흔히 CA라고 칭하는 서명할 수 있는 주체를 지칭하는 단어로 Certificate(인증서)를 생성할 수 있는 대상입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;self-signed 인증서를 사용하게 될 경우 브라우저에서 아래의 이미지와 같이 좋지 않은 사용자 경험을 줄 수 있지만, cluster 내부에서 사용하거나 테스트 용도로 사용하는 것이라면 self-signed 인증서를 사용하는 것도 좋은 방법이 될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;300&quot; data-filename=&quot;스크린샷 2021-02-04 오후 5.01.50.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;664&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nJ7UZ/btqVRu5z9PT/d4FTsDFxDdFWjFgDSnAIe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nJ7UZ/btqVRu5z9PT/d4FTsDFxDdFWjFgDSnAIe1/img.png&quot; data-alt=&quot;self-signed 인증서를 사용하게 될 경우 브라우저의 형태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nJ7UZ/btqVRu5z9PT/d4FTsDFxDdFWjFgDSnAIe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnJ7UZ%2FbtqVRu5z9PT%2Fd4FTsDFxDdFWjFgDSnAIe1%2Fimg.png&quot; width=&quot;300&quot; data-filename=&quot;스크린샷 2021-02-04 오후 5.01.50.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;664&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;self-signed 인증서를 사용하게 될 경우 브라우저의 형태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;우리가 구성하게 될 대략적인 시스템 구성도는 아래의 이미지와 같습니다. 제일 먼저 cert-manager를 이용해서 클러스터 전역에서 사용할 수 있는 Cluster Issuer를 생성하고, 해당 Issuer를 이용해서 각각의 Namespace 별로 Certificate를 생성하도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;500&quot; data-filename=&quot;SecureSystem.png&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;683&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tGLWh/btqVV4mGY7l/Oc0WXUaR6iEbN64Qtjnt30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tGLWh/btqVV4mGY7l/Oc0WXUaR6iEbN64Qtjnt30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tGLWh/btqVV4mGY7l/Oc0WXUaR6iEbN64Qtjnt30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtGLWh%2FbtqVV4mGY7l%2FOc0WXUaR6iEbN64Qtjnt30%2Fimg.png&quot; width=&quot;500&quot; data-filename=&quot;SecureSystem.png&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;683&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Issuer 생성&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자, 이제 본격적으로 Cluster에 인증서를 만들어보도록 하겠습니다. 가장 먼저 할 일은 Self-signed Issuer를 만드는 일입니다. 조금 전에 이야기했듯이 Issuer는 Certificate를 만들 수 있는 주체인데, Issuer에는 그냥 Issuer와 ClusterIssuer가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 &lt;u&gt;&lt;b&gt;Issuer&lt;/b&gt;는 Namespace의 리소스로 속해있는 Namespace 안에서만 사용할 수 있는 리소스입니다. &lt;/u&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;u&gt;반면 &lt;b&gt;ClusterIssuer&lt;/b&gt;는 Namespace를 가리지 않고 Cluster 전역에서 접근할 수 있는 리소스입니다.&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;저는 ClusterIssuer로 생성하도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1612426083043&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# selfsigned-issuer.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Certificate 생성&lt;/h3&gt;
&lt;p&gt;이번에는 생성한 ClusterIssuer를 사용해서 self-signed Certificate를 생성하고 이를 이용해서 MySQL에 적용해보도록 하겠습니다. cert-manager가 Issuer를 통해 Certificate를 생성하게 되면 해당 Certificate는 속해있는 Namespace 내의 모든 서비스가 사용할 수 있는 인증서가 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해당 인증서가 생성됨과 동시에 Certificate는 Kubernetes내에서 사용할 수 있도록&amp;nbsp; Public key, Secret key와 같은 데이터를 가진 secret 리소스가 생성하게 되는데, 이때 생성되는 secret 리소스를 이용해서 kubernetes 내에서 pod에 주입하는 등 다양하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;secret에 관한 내용은 아래의 &lt;a href=&quot;https://ooeunz.tistory.com/128?category=837108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참조합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1612426475228&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# selfsigned-cert.yaml

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: selfsigned-cert
  namespace: default
spec:
  secretName: selfsigned-cert-tls
  duration: 2880h # 120d
  renewBefore: 360h # 15d
  commonName: ooeunz.tistory.com
  isCA: false
  keySize: 2048
  keyAlgorithm: rsa
  keyEncoding: pkcs1
  usages:
    - digital signature
    - key encipherment
    - server auth
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
    group: cert-manager.io&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Certificate의 spec을 간략하게 살펴보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;secretName&lt;/b&gt;: Certificate와 동시에 함께 생성되는 secret의 이름을 지정합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;duration&lt;/b&gt;: 인증서의 유효기간을 지정합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;renewBefore&lt;/b&gt;: 자동으로 인증서를 갱신할 때를 지정합니다. 여기서 duration과 renewBefore는 go time을 사용하기 때문에 &quot;ms&quot;, &quot;s&quot;, &quot;m&quot;, &quot;h&quot;만을 사용할 수 있습니다. 더 자세한 내용은 &lt;a href=&quot;https://golang.org/pkg/time/#ParseDuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 확인 하실 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;commonName&lt;/b&gt;: host name을 지정하는 필드입니다. dnsName 옵션과 함게 사용할 수 있으며 만약 commonName이 설정되지 않았을 경우엔 dnsName의 가장 첫번째 값을 commonName으로 사용하게 됩니다. 하지만 해당 예제에선 dnsName은 지정하지 않겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;isCa&lt;/b&gt;: 이 인증서를 CA서명이 유효하도록 하는 옵션입니다. 해당 옵션을 true로 해줄 경우 `cert sign`값이 usages 리스트에 자동으로 추가됩니다. 자세한 내용은 &lt;a href=&quot;https://cert-manager.io/docs/reference/api-docs/#acme.cert-manager.io/v1alpha2.Order&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 도큐먼트&lt;/a&gt;에서 확인하실 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;keySize&lt;/b&gt;: 암호화할 key의 길이를 지정합니다. 길이가 길어질수록 암호화의 수준이 높아지며 지정하지 않을 시에 default로 &lt;/span&gt;2048 값을 가집니다. 지정할 수 있는 옵션으론 2048 이외에 4096, 8192가 있습니다. 보안강도에 대한 더 자세한 내용은 &lt;a href=&quot;https://drive.google.com/file/d/1RhUPXCNiGA9tcXwTeMUV5BGdEOYDjYB_/view&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;서 확인할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;keyAlgorithm&lt;/b&gt;: 사용할 암호화 알고리즘을 지정합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;keyEncoding&lt;/b&gt;: 어떤 keyEncoding을 사용할 것인지에 관한 부분입니다. PKCS#1과 PKCS#8만을 사용할 수 있고 default로 PKCS#1을 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위의 yaml 파일을 apply 하면 말씀드린 것처럼 certificate와 secret 리소스가 함께 생성됩니다. secret에는 아래와 같은 data를 가지고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;ca.crt: &lt;/b&gt;public certificate file&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tls.crt: &lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Public Key&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tls.key: &lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Private Key&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;758&quot; data-filename=&quot;스크린샷 2021-02-04 오후 5.30.10.png&quot; width=&quot;600&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s8bEK/btqVCclErjg/crTEZ1ScvFJWIczd56IL4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s8bEK/btqVCclErjg/crTEZ1ScvFJWIczd56IL4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s8bEK/btqVCclErjg/crTEZ1ScvFJWIczd56IL4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs8bEK%2FbtqVCclErjg%2FcrTEZ1ScvFJWIczd56IL4k%2Fimg.png&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;758&quot; data-filename=&quot;스크린샷 2021-02-04 오후 5.30.10.png&quot; width=&quot;600&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이와 같이 생성된 Secret을 MySQL에 주입시켜주고, 동시에 아래와 같이 my.cnf 파일에 secret안에 있는 키들을 넣어주도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612528258586&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# mysql-pvc.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: &quot;/mnt/data&quot;
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1612525208853&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# mysql-config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
  namespace: default
data:
  my.cnf: |-
    [mysqld]
    ssl-ca=/etc/mysql/tls/ca.crt
    ssl-cert=/etc/mysql/tls/tls.crt
    ssl-key=/etc/mysql/tls/tls.key
    require_secure_transport=ON   ## This line is the only setting required to enforce secure connections&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1612528272856&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# mysql-server.yaml

apiVersion: v1
kind: Service
metadata:
  name: mysql-http
spec:
  ports:
  - port: 3306
  selector:
    app: mysql
  clusterIP: None&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1612525228409&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# mysql-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: mysql
          image: mysql:8.0.21
          env:
          - name: MYSQL_ROOT_PASSWORD
            value: password
          imagePullPolicy: Always
          ports:
            - containerPort: 3306
              name: mysql
          volumeMounts:
            - name: mysql-persistent-storage
              mountPath: /var/lib/mysql
            - name: mysql-cnf
              mountPath: /etc/mysql/conf.d/my.cnf
              subPath: my.cnf
            - name: mysql-tls
              mountPath: /etc/mysql/tls
              readOnly: true
      volumes:
        - name: mysql-persistent-storage
          persistentVolumeClaim:
            claimName: mysql-pv-claim
        - name: mysql-cnf
          configMap:
            name: mysql-config
        - name: mysql-tls
          secret:
            secretName: selfsigned-cert-tls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 위와 같이 secret을 주입하고, mysql에 들어가서 아래의 명령어로 SSL이 정상적으로 적용됐는지 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1612528577579&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW VARIABLES LIKE '%ssl%';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;500&quot; data-filename=&quot;스크린샷 2021-02-05 오후 9.30.03.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;1102&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu1dT4/btqVZTd4jsk/SqX5cH8GcoZSkSKlSWMny1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu1dT4/btqVZTd4jsk/SqX5cH8GcoZSkSKlSWMny1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu1dT4/btqVZTd4jsk/SqX5cH8GcoZSkSKlSWMny1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu1dT4%2FbtqVZTd4jsk%2FSqX5cH8GcoZSkSKlSWMny1%2Fimg.png&quot; width=&quot;500&quot; data-filename=&quot;스크린샷 2021-02-05 오후 9.30.03.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;1102&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;have_openssl과 have_ssl 옵션이 YES로 되어있고, ssl_ca, ssl_cert, ssl_key에 방금 mount해준 key가 정상적으로 적용된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Jdbc로 연결할 때 주의할 점&lt;/h3&gt;
&lt;p&gt;이제 Spring Boot에서 MySQL에 연결을 해볼 텐데요. jdbc로 연결을 할 경우에 주의할 점이 몇 가지가 있습니다. 현재 require_secure_transport=ON 옵션을 주었기 때문에 MySQL에 접속하기 위해선 필수적으로 SSL 접속을 하여야 합니다. 그런데 우리는 self-sigend 인증서를 사용하고 있기 때문에 tomcat에서 인증서를 vailation 하는 과정에서 커넥션을 끊어버리게 됩니다. 따라서 인증서에 대하여 validation 하지 않는 옵션을 주도록 하겠습니다. (또는 자바에도 인증서를 포함하여 양방향 인증하는 방법도 있지만, 여기선 단순히 validation을 하지 않도록 하겠습니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 MySQL 버전에 따라 validation 옵션을 주는 방법이 조금 차이가 납니다. 자세한 내용은 &lt;a href=&quot;https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-using-ssl.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 도큐먼트&lt;/a&gt;를 참고합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;MySQL 8.0.12&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;MySQL 8.0.12 이하 버전에선 useSSL=true, &lt;span&gt;verifyServerCertificate=true 일 경우 서버 인증서의 유효성 검증이 활성화 되게 됩니다. (다만 hostname은 검증하지 않습니다.) 따라서 8.0.12 버전 이하에선 &lt;span style=&quot;color: #333333;&quot;&gt;verifyServerCertificate=false를 주어 유효성 검증을 하지 않도록 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1612529285252&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# MySQL 8.0.12 버전 이하

jdbc:mysql://localhost:3306/cruise?useSSL=true&amp;amp;verifyServerCertificate=false&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;MySQL 8.0.13 이후&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;반면 MySQL 8.0.13 이후 버전에서는 SSLMode가 VERTIFY_CA이거나 &lt;span&gt;VERIFY_IDENTITY일 때만 서버 인증서 유효성을 확인하게 됩니다. 또한 useSSL옵션 역시 true값이 default이므로 따로 옵션을 주지 않으면 useSSL=true&amp;amp; &lt;span&gt;verifyServerCertificate=false의 기능을 하게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1612529505529&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# MySQL 8.0.13 버전 이후

jdbc:mysql://localhost:3306/cruise?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;포스팅에 사용한 코드는 아래의 URL에서 확인할 수 있습니다.  &lt;/p&gt;
&lt;figure id=&quot;og_1613093343764&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;object&quot; data-og-title=&quot;ooeunz/blog-code&quot; data-og-description=&quot;Contribute to ooeunz/blog-code development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/security&quot; data-og-url=&quot;https://github.com/ooeunz/blog-code&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1k0MO/hyJeu4lD06/cmOeaigyJYKg53HlR7JyQ0/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320&quot;&gt;&lt;a href=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/security&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ooeunz/blog-code/tree/master/devops/kubernetes/security&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1k0MO/hyJeu4lD06/cmOeaigyJYKg53HlR7JyQ0/img.jpg?width=400&amp;amp;height=400&amp;amp;face=96_128_272_320');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;ooeunz/blog-code&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Contribute to ooeunz/blog-code development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Cert manager</category>
      <category>cert manager</category>
      <category>certificate</category>
      <category>HTTPS</category>
      <category>Issuer</category>
      <category>Kubernetes</category>
      <category>mysql https</category>
      <category>mysql 암호화</category>
      <category>SSL</category>
      <category>TLS</category>
      <category>자동화</category>
      <author>ooeunz</author>
      <guid isPermaLink="true">https://ooeunz.tistory.com/143</guid>
      <comments>https://ooeunz.tistory.com/143#entry143comment</comments>
      <pubDate>Fri, 5 Feb 2021 21:58:19 +0900</pubDate>
    </item>
  </channel>
</rss>