<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>바이너리</title>
    <link>https://binarynum.tistory.com/</link>
    <description>0과1로 바뀌는 세상</description>
    <language>ko</language>
    <pubDate>Mon, 25 May 2026 19:31:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>BinaryNumber</managingEditor>
    <image>
      <title>바이너리</title>
      <url>https://tistory1.daumcdn.net/tistory/3004757/attach/1b3b15485c47494eaa8e91fe08d5defd</url>
      <link>https://binarynum.tistory.com</link>
    </image>
    <item>
      <title>&amp;quot;구글로 로그인&amp;quot;은 어떻게 동작할까?</title>
      <link>https://binarynum.tistory.com/104</link>
      <description>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot; /&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
  &lt;title&gt;&quot;구글로 로그인&quot;은 어떻게 동작할까? — OIDC 동작 원리&lt;/title&gt;
  &lt;script src=&quot;https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js&quot;&gt;&lt;/script&gt;
  &lt;style&gt;
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: 'Pretendard', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
      background: #f8f9fa;
      color: #1a1a2e;
      line-height: 1.8;
    }

    .container {
      max-width: 760px;
      margin: 0 auto;
      padding: 60px 24px 120px;
    }

    /* 헤더 */
    .post-header {
      margin-bottom: 48px;
      border-bottom: 2px solid #e9ecef;
      padding-bottom: 32px;
    }

    .post-tag {
      display: inline-block;
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: #4361ee;
      background: #eef2ff;
      padding: 4px 10px;
      border-radius: 4px;
      margin-bottom: 16px;
    }

    h1 {
      font-size: 2rem;
      font-weight: 800;
      line-height: 1.3;
      color: #0d1b2a;
      margin-bottom: 8px;
    }

    .post-meta {
      font-size: 14px;
      color: #868e96;
      margin-top: 16px;
    }

    /* 본문 타이포그래피 */
    h2 {
      font-size: 1.4rem;
      font-weight: 700;
      color: #0d1b2a;
      margin: 48px 0 16px;
      padding-left: 12px;
      border-left: 4px solid #4361ee;
    }

    p {
      margin-bottom: 16px;
      color: #343a40;
    }

    strong {
      color: #0d1b2a;
      font-weight: 700;
    }

    /* 구분선 */
    hr {
      border: none;
      border-top: 1px solid #e9ecef;
      margin: 40px 0;
    }

    /* 인용 */
    blockquote {
      background: #f1f3f9;
      border-left: 4px solid #4361ee;
      padding: 16px 20px;
      border-radius: 0 8px 8px 0;
      margin: 24px 0;
      color: #343a40;
    }

    blockquote p { margin: 0; }
    blockquote p + p { margin-top: 8px; }

    /* 테이블 */
    table {
      width: 100%;
      border-collapse: collapse;
      margin: 24px 0;
      font-size: 15px;
    }

    th {
      background: #4361ee;
      color: #fff;
      font-weight: 600;
      text-align: left;
      padding: 12px 16px;
    }

    td {
      padding: 11px 16px;
      border-bottom: 1px solid #e9ecef;
      color: #343a40;
    }

    tr:last-child td { border-bottom: none; }
    tr:nth-child(even) td { background: #f8f9fa; }

    /* 코드 */
    code {
      font-family: 'JetBrains Mono', 'Fira Code', 'D2Coding', monospace;
      font-size: 0.88em;
      background: #eef2ff;
      color: #3b4fd4;
      padding: 2px 6px;
      border-radius: 4px;
    }

    pre {
      background: #1e1e2e;
      border-radius: 10px;
      padding: 24px;
      overflow-x: auto;
      margin: 24px 0;
    }

    pre code {
      background: none;
      color: #cdd6f4;
      font-size: 0.9rem;
      padding: 0;
      line-height: 1.7;
    }

    /* JSON 하이라이트 */
    .json-key    { color: #89b4fa; }
    .json-string { color: #a6e3a1; }
    .json-number { color: #fab387; }
    .json-comment{ color: #6c7086; font-style: italic; }

    /* URL 하이라이트 */
    .url-base    { color: #89dceb; }
    .url-param   { color: #fab387; }
    .url-value   { color: #a6e3a1; }

    /* 목록 */
    ul, ol {
      padding-left: 24px;
      margin: 16px 0;
    }

    li {
      margin-bottom: 8px;
      color: #343a40;
    }

    li code { font-size: 0.9em; }

    /* Mermaid 다이어그램 */
    .mermaid {
      background: #fff;
      border: 1px solid #e9ecef;
      border-radius: 12px;
      padding: 24px;
      margin: 24px 0;
      text-align: center;
    }

    /* 정리 카드 */
    .summary-list {
      list-style: none;
      padding: 0;
      margin: 24px 0;
    }

    .summary-list li {
      background: #fff;
      border: 1px solid #e9ecef;
      border-radius: 10px;
      padding: 16px 20px;
      margin-bottom: 12px;
      display: flex;
      align-items: flex-start;
      gap: 12px;
      color: #343a40;
    }

    .summary-list li::before {
      content: attr(data-num);
      background: #4361ee;
      color: #fff;
      font-size: 13px;
      font-weight: 700;
      min-width: 26px;
      height: 26px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-top: 1px;
    }

    /* 다음 편 배너 */
    .next-post {
      background: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%);
      border-radius: 12px;
      padding: 28px 32px;
      margin-top: 56px;
      color: #fff;
    }

    .next-post p {
      color: rgba(255,255,255,0.8);
      margin: 0 0 8px;
      font-size: 14px;
    }

    .next-post strong {
      color: #fff;
      font-size: 1.1rem;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;

  &lt;section&gt;
    &lt;h2&gt;들어가며&lt;/h2&gt;
    &lt;p&gt;&quot;카카오로 로그인&quot;, &quot;구글로 로그인&quot; — 요즘 웹서비스에서 정말 흔하게 보이는 버튼이죠.&lt;/p&gt;
    &lt;p&gt;그런데 이 버튼을 누르면 내 뒤에서 무슨 일이 벌어지는 걸까요? 내 비밀번호를 그 서비스가 알게 되는 건 아닐까요?&lt;/p&gt;
    &lt;p&gt;그 비밀을 풀어주는 게 바로 &lt;strong&gt;OIDC(OpenID Connect)&lt;/strong&gt;입니다.&lt;/p&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;먼저, OAuth 2.0을 알아야 해요&lt;/h2&gt;
    &lt;p&gt;OIDC를 이해하려면 먼저 &lt;strong&gt;OAuth 2.0&lt;/strong&gt;을 짚고 넘어가야 합니다.&lt;/p&gt;
    &lt;p&gt;OAuth 2.0은 &quot;내 비밀번호를 알려주지 않고도, 특정 권한을 다른 서비스에게 위임&quot;하는 프로토콜이에요.&lt;/p&gt;
    &lt;p&gt;예시를 들어볼게요.&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;어떤 앱이 &quot;내 구글 드라이브 파일을 읽고 싶다&quot;고 해요.&lt;/p&gt;
      &lt;p&gt;이때 내 구글 계정 비밀번호를 그 앱에 알려줄 순 없잖아요.&lt;/p&gt;
      &lt;p&gt;OAuth 2.0은 구글이 &quot;이 앱은 드라이브 파일을 읽을 수 있다&quot;는 허락증(Access Token)을 대신 발급해줍니다.&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;근데 여기서 문제가 생겼어요.&lt;/p&gt;
    &lt;p&gt;&lt;strong&gt;OAuth 2.0은 &quot;권한 위임&quot;에 집중하다 보니, &quot;이 사용자가 누구인가&quot;를 표준으로 정의하지 않았어요.&lt;/strong&gt;&lt;/p&gt;
    &lt;p&gt;그래서 서비스마다 사용자 정보를 가져오는 방식이 제각각이었죠.&lt;/p&gt;
    &lt;p&gt;이 문제를 해결하기 위해 OAuth 2.0 위에 &lt;strong&gt;&quot;사용자 인증&quot;&lt;/strong&gt; 기능을 표준으로 얹은 것이 바로 &lt;strong&gt;OIDC&lt;/strong&gt;입니다.&lt;/p&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;OIDC의 등장인물&lt;/h2&gt;
    &lt;p&gt;OIDC에는 3명의 주인공이 있어요.&lt;/p&gt;
    &lt;table&gt;
      &lt;thead&gt;
        &lt;tr&gt;&lt;th&gt;역할&lt;/th&gt;&lt;th&gt;이름&lt;/th&gt;&lt;th&gt;실제 예시&lt;/th&gt;&lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
        &lt;tr&gt;&lt;td&gt;사용자&lt;/td&gt;&lt;td&gt;End User&lt;/td&gt;&lt;td&gt;나 (로그인하는 사람)&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;내 정보를 관리하는 곳&lt;/td&gt;&lt;td&gt;Identity Provider (IdP)&lt;/td&gt;&lt;td&gt;구글, 카카오, 네이버&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;로그인을 요청하는 서비스&lt;/td&gt;&lt;td&gt;Relying Party (RP)&lt;/td&gt;&lt;td&gt;쇼핑몰, 블로그, 회사 앱&lt;/td&gt;&lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
    &lt;p&gt;핵심은 이거예요.&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;&lt;strong&gt;내 비밀번호는 IdP(구글)만 알고, RP(쇼핑몰)는 절대 알 수 없다.&lt;/strong&gt;&lt;/p&gt;
    &lt;/blockquote&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;OIDC 흐름 한눈에 보기&lt;/h2&gt;
    &lt;p&gt;아래는 &quot;구글로 로그인&quot; 버튼을 눌렀을 때 실제로 일어나는 일이에요.&lt;/p&gt;
    &lt;div class=&quot;mermaid&quot;&gt;
sequenceDiagram
    actor 나
    participant 쇼핑몰
    participant 구글

    나-&gt;&gt;쇼핑몰: 로그인 버튼 클릭
    쇼핑몰-&gt;&gt;구글: 인증 요청 (client_id, scope=openid ...)
    구글--&gt;&gt;나: 구글 로그인 화면 표시
    나-&gt;&gt;구글: ID/PW 입력 + 권한 허용
    구글--&gt;&gt;나: 쇼핑몰로 리다이렉트 (code 전달)
    나-&gt;&gt;쇼핑몰: code 도착
    쇼핑몰-&gt;&gt;구글: code로 토큰 요청 (서버↔서버)
    구글--&gt;&gt;쇼핑몰: ID Token + Access Token 발급
    쇼핑몰--&gt;&gt;나: 로그인 완료!
    &lt;/div&gt;
    &lt;p&gt;단계별로 차근차근 살펴봅시다.&lt;/p&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;단계 1 — 인증 요청 (Authorization Request)&lt;/h2&gt;
    &lt;p&gt;쇼핑몰이 구글에게 이렇게 요청해요.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;&lt;span class=&quot;url-base&quot;&gt;https://accounts.google.com/o/oauth2/auth&lt;/span&gt;
  ?&lt;span class=&quot;url-param&quot;&gt;client_id&lt;/span&gt;=&lt;span class=&quot;url-value&quot;&gt;쇼핑몰_ID&lt;/span&gt;
  &amp;amp;&lt;span class=&quot;url-param&quot;&gt;redirect_uri&lt;/span&gt;=&lt;span class=&quot;url-value&quot;&gt;https://shop.example.com/callback&lt;/span&gt;
  &amp;amp;&lt;span class=&quot;url-param&quot;&gt;response_type&lt;/span&gt;=&lt;span class=&quot;url-value&quot;&gt;code&lt;/span&gt;
  &amp;amp;&lt;span class=&quot;url-param&quot;&gt;scope&lt;/span&gt;=&lt;span class=&quot;url-value&quot;&gt;openid email profile&lt;/span&gt;
  &amp;amp;&lt;span class=&quot;url-param&quot;&gt;state&lt;/span&gt;=&lt;span class=&quot;url-value&quot;&gt;abc123&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;주요 파라미터를 해석하면:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code&gt;client_id&lt;/code&gt; : 쇼핑몰이 구글에 등록된 ID&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;redirect_uri&lt;/code&gt; : 로그인 후 사용자를 어디로 보낼지&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;scope=openid&lt;/code&gt; : &quot;OIDC를 쓸게요&quot; 라는 선언. 이게 있어야 OIDC예요&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;scope=email profile&lt;/code&gt; : 이메일, 이름도 알고 싶어요&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;state&lt;/code&gt; : 나중에 위조 요청 방어에 쓰는 랜덤 값&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;단계 2 — 사용자 로그인&lt;/h2&gt;
    &lt;p&gt;구글 로그인 화면이 뜨고, 사용자가 아이디/비밀번호를 입력해요.&lt;/p&gt;
    &lt;p&gt;구글은 인증에 성공하면 이런 화면을 보여줄 수도 있어요.&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;&quot;쇼핑몰이 이메일과 이름을 요청합니다. 허용하시겠어요?&quot;&lt;/p&gt;
    &lt;/blockquote&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;단계 3 — Authorization Code 전달&lt;/h2&gt;
    &lt;p&gt;사용자가 허락하면, 구글은 아까 쇼핑몰이 알려준 &lt;code&gt;redirect_uri&lt;/code&gt;로 사용자를 보내요.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;&lt;span class=&quot;url-base&quot;&gt;https://shop.example.com/callback&lt;/span&gt;
  ?&lt;span class=&quot;url-param&quot;&gt;code&lt;/span&gt;=&lt;span class=&quot;url-value&quot;&gt;4/P7q7W91a&lt;/span&gt;
  &amp;amp;&lt;span class=&quot;url-param&quot;&gt;state&lt;/span&gt;=&lt;span class=&quot;url-value&quot;&gt;abc123&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;여기서 &lt;code&gt;code&lt;/code&gt;는 &lt;strong&gt;일회용 코드&lt;/strong&gt;예요. 그 자체로는 아무것도 아니고, 토큰으로 교환해야 비로소 의미가 생겨요.&lt;/p&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;단계 4 — 토큰 발급 (Token Exchange)&lt;/h2&gt;
    &lt;p&gt;쇼핑몰 서버는 이 &lt;code&gt;code&lt;/code&gt;를 가지고 구글에게 직접 토큰을 요청해요.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;POST https://oauth2.googleapis.com/token

&lt;span class=&quot;url-param&quot;&gt;code&lt;/span&gt;          = &lt;span class=&quot;url-value&quot;&gt;4/P7q7W91a&lt;/span&gt;
&lt;span class=&quot;url-param&quot;&gt;client_id&lt;/span&gt;     = &lt;span class=&quot;url-value&quot;&gt;쇼핑몰_ID&lt;/span&gt;
&lt;span class=&quot;url-param&quot;&gt;client_secret&lt;/span&gt; = &lt;span class=&quot;url-value&quot;&gt;쇼핑몰_비밀키&lt;/span&gt;
&lt;span class=&quot;url-param&quot;&gt;redirect_uri&lt;/span&gt;  = &lt;span class=&quot;url-value&quot;&gt;https://shop.example.com/callback&lt;/span&gt;
&lt;span class=&quot;url-param&quot;&gt;grant_type&lt;/span&gt;    = &lt;span class=&quot;url-value&quot;&gt;authorization_code&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;이 요청은 브라우저가 아니라 &lt;strong&gt;서버끼리 직접&lt;/strong&gt; 이루어지기 때문에, 사용자가 볼 수 없어요.&lt;/p&gt;
    &lt;p&gt;구글은 이에 응답으로 두 가지 토큰을 줘요.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;{
  &lt;span class=&quot;json-key&quot;&gt;&quot;access_token&quot;&lt;/span&gt;: &lt;span class=&quot;json-string&quot;&gt;&quot;ya29.xxx&quot;&lt;/span&gt;,
  &lt;span class=&quot;json-key&quot;&gt;&quot;id_token&quot;&lt;/span&gt;:     &lt;span class=&quot;json-string&quot;&gt;&quot;eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...&quot;&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;OIDC의 핵심 — ID Token&lt;/h2&gt;
    &lt;p&gt;OIDC에서 가장 중요한 것은 바로 &lt;strong&gt;ID Token&lt;/strong&gt;이에요.&lt;/p&gt;
    &lt;p&gt;&lt;code&gt;access_token&lt;/code&gt;이 &quot;이 사람이 어떤 걸 할 수 있다&quot;는 허가증이라면,&lt;br&gt;
    &lt;code&gt;id_token&lt;/code&gt;은 &quot;이 사람이 누구다&quot;를 증명하는 &lt;strong&gt;신분증&lt;/strong&gt;이에요.&lt;/p&gt;
    &lt;p&gt;ID Token은 &lt;strong&gt;JWT(JSON Web Token)&lt;/strong&gt; 형식이에요. 점(&lt;code&gt;.&lt;/code&gt;)으로 구분된 세 파트로 이루어져 있어요.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;&lt;span class=&quot;json-comment&quot;&gt;# 헤더 — 어떤 알고리즘으로 서명했는지&lt;/span&gt;
eyJhbGciOiJSUzI1NiJ9

&lt;span class=&quot;json-comment&quot;&gt;# 페이로드 — 실제 사용자 정보 (Claims)&lt;/span&gt;
eyJzdWIiOiIxMjM0NTY3ODkwIn0

&lt;span class=&quot;json-comment&quot;&gt;# 서명 — 위조 방지&lt;/span&gt;
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;페이로드를 Base64로 디코딩하면 이런 정보가 나와요.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;{
  &lt;span class=&quot;json-key&quot;&gt;&quot;iss&quot;&lt;/span&gt;:   &lt;span class=&quot;json-string&quot;&gt;&quot;https://accounts.google.com&quot;&lt;/span&gt;,  &lt;span class=&quot;json-comment&quot;&gt;// 발급자 (구글)&lt;/span&gt;
  &lt;span class=&quot;json-key&quot;&gt;&quot;sub&quot;&lt;/span&gt;:   &lt;span class=&quot;json-string&quot;&gt;&quot;1234567890&quot;&lt;/span&gt;,                  &lt;span class=&quot;json-comment&quot;&gt;// 사용자 고유 ID&lt;/span&gt;
  &lt;span class=&quot;json-key&quot;&gt;&quot;aud&quot;&lt;/span&gt;:   &lt;span class=&quot;json-string&quot;&gt;&quot;쇼핑몰_ID&quot;&lt;/span&gt;,                   &lt;span class=&quot;json-comment&quot;&gt;// 이 토큰을 받을 대상&lt;/span&gt;
  &lt;span class=&quot;json-key&quot;&gt;&quot;exp&quot;&lt;/span&gt;:   &lt;span class=&quot;json-number&quot;&gt;1716153600&lt;/span&gt;,                    &lt;span class=&quot;json-comment&quot;&gt;// 만료 시간&lt;/span&gt;
  &lt;span class=&quot;json-key&quot;&gt;&quot;iat&quot;&lt;/span&gt;:   &lt;span class=&quot;json-number&quot;&gt;1716150000&lt;/span&gt;,                    &lt;span class=&quot;json-comment&quot;&gt;// 발급 시간&lt;/span&gt;
  &lt;span class=&quot;json-key&quot;&gt;&quot;email&quot;&lt;/span&gt;: &lt;span class=&quot;json-string&quot;&gt;&quot;user@example.com&quot;&lt;/span&gt;,
  &lt;span class=&quot;json-key&quot;&gt;&quot;name&quot;&lt;/span&gt;:  &lt;span class=&quot;json-string&quot;&gt;&quot;홍길동&quot;&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;이 정보들을 &lt;strong&gt;Claims&lt;/strong&gt;라고 불러요.&lt;/p&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;서명은 왜 중요한가요?&lt;/h2&gt;
    &lt;p&gt;ID Token의 세 번째 파트인 서명 덕분에, 쇼핑몰은 이 토큰이 &lt;strong&gt;진짜 구글이 발급했는지&lt;/strong&gt; 검증할 수 있어요.&lt;/p&gt;
    &lt;p&gt;구글은 자신의 &lt;strong&gt;공개 키&lt;/strong&gt;를 공개해 두는데, 쇼핑몰은 이 키로 서명을 확인해요.&lt;/p&gt;
    &lt;blockquote&gt;
      &lt;p&gt;누군가 토큰 내용을 살짝 바꾼다면? → 서명이 맞지 않아서 즉시 탄로나요.&lt;/p&gt;
    &lt;/blockquote&gt;
    &lt;p&gt;이 덕분에 쇼핑몰이 직접 사용자 정보 DB를 가지지 않아도, 구글이 &quot;이 사람 맞아요&quot;라고 보증해주는 구조가 만들어져요.&lt;/p&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;정리 — OIDC가 해결한 것&lt;/h2&gt;
    &lt;table&gt;
      &lt;thead&gt;
        &lt;tr&gt;&lt;th&gt;문제&lt;/th&gt;&lt;th&gt;OIDC의 해결책&lt;/th&gt;&lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
        &lt;tr&gt;&lt;td&gt;내 비밀번호를 서비스에 알려줘야 하나?&lt;/td&gt;&lt;td&gt;비밀번호는 IdP(구글)에만 입력, 서비스는 절대 모름&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;서비스마다 사용자 정보 가져오는 방식이 다름&lt;/td&gt;&lt;td&gt;ID Token이라는 표준 형식으로 통일&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;토큰이 위조될 수 있지 않나?&lt;/td&gt;&lt;td&gt;JWT 서명으로 위조 즉시 탐지&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;&quot;이 사람이 누구야?&quot;를 어떻게 알아?&lt;/td&gt;&lt;td&gt;&lt;code&gt;sub&lt;/code&gt; claim이 사용자 고유 식별자 역할&lt;/td&gt;&lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/section&gt;

  &lt;hr&gt;

  &lt;section&gt;
    &lt;h2&gt;마치며&lt;/h2&gt;
    &lt;p&gt;&quot;구글로 로그인&quot;은 단순해 보이지만, 뒤에서는 꽤 정교한 흐름이 돌아가고 있어요.&lt;/p&gt;
    &lt;p&gt;핵심만 기억하세요.&lt;/p&gt;
    &lt;ul class=&quot;summary-list&quot;&gt;
      &lt;li data-num=&quot;1&quot;&gt;&lt;strong&gt;내 비밀번호는 구글만 알고, 다른 서비스는 절대 모른다&lt;/strong&gt;&lt;/li&gt;
      &lt;li data-num=&quot;2&quot;&gt;&lt;strong&gt;구글은 ID Token(신분증)을 발급해서 &quot;이 사람 맞아요&quot;를 보증해준다&lt;/strong&gt;&lt;/li&gt;
      &lt;li data-num=&quot;3&quot;&gt;&lt;strong&gt;ID Token은 JWT 형식이라 위조가 불가능하다&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;이 세 가지만 이해해도 OIDC의 절반은 이해한 거예요.&lt;/p&gt;
  &lt;/section&gt;


&lt;/div&gt;

&lt;script&gt;
  mermaid.initialize({ startOnLoad: true, theme: 'neutral', fontFamily: 'inherit' });
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</description>
      <category>Etc</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/104</guid>
      <comments>https://binarynum.tistory.com/104#entry104comment</comments>
      <pubDate>Mon, 18 May 2026 22:36:23 +0900</pubDate>
    </item>
    <item>
      <title>AWS Load Balancer Controller, Pod Identity로 설치하기</title>
      <link>https://binarynum.tistory.com/103</link>
      <description>&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;EKS에서 Ingress를 쓰려면 AWS Load Balancer Controller가 필요합니다. 그리고 Controller가 ALB를 만들려면 IAM 권한이 있어야 해요.&lt;/p&gt;
&lt;p&gt;이 글에서는 기존 IRSA(OIDC) 방식 대신 &lt;strong&gt;EKS Pod Identity&lt;/strong&gt;를 사용해 권한을 연결하는 방법을 다룹니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Ingress Controller가 뭔가요?&lt;/h2&gt;
&lt;p&gt;Ingress Controller는 &lt;strong&gt;AWS 리소스(ALB)와 Kubernetes 사이의 중개 역할을 하는 컴포넌트&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;Ingress 리소스를 배포해도 Controller가 없으면 AWS 콘솔에 아무것도 생기지 않아요. &lt;strong&gt;클러스터 생성 시 자동으로 설치되지 않기 때문에&lt;/strong&gt; 직접 설치해야 합니다.&lt;/p&gt;
&lt;p&gt;Controller가 하는 일:&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse; width:100%&quot;&gt;
  &lt;tr&gt;
    &lt;th style=&quot;border:1px solid #ddd; padding:8px; background-color:#f5f5f5; text-align:left&quot;&gt;역할&lt;/th&gt;
    &lt;th style=&quot;border:1px solid #ddd; padding:8px; background-color:#f5f5f5; text-align:left&quot;&gt;설명&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;이벤트 감시&lt;/td&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;API 서버로부터 Ingress 관련 이벤트를 감시하고 AWS 리소스 생성&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;ALB 생성&lt;/td&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;Ingress 1개당 ALB 1개 생성, Internet-facing / Internal 선택 가능&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;Target Group&lt;/td&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;Kubernetes 서비스 단위로 생성&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;Listener&lt;/td&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;포트 미지정 시 80/443으로 생성&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;Rule&lt;/td&gt;
    &lt;td style=&quot;border:1px solid #ddd; padding:8px&quot;&gt;Ingress에 정의한 경로 규칙대로 생성&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;

&lt;hr&gt;
&lt;h2&gt;EKS Pod Identity — IRSA 없이 IAM 권한 연결하기&lt;/h2&gt;
&lt;p&gt;Pod Identity는 &lt;strong&gt;Pod가 IAM 역할을 직접 부여받아 AWS 리소스에 접근하는 인증 방식&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;기존 IRSA와의 차이점:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;IRSA (기존)&lt;/th&gt;
&lt;th&gt;Pod Identity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;OIDC 공급자 등록&lt;/td&gt;
&lt;td&gt;필요&lt;/td&gt;
&lt;td&gt;불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServiceAccount 어노테이션&lt;/td&gt;
&lt;td&gt;필요&lt;/td&gt;
&lt;td&gt;불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM 신뢰 정책 Principal&lt;/td&gt;
&lt;td&gt;OIDC Provider ARN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pods.eks.amazonaws.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추가 컴포넌트&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;Pod Identity Agent 애드온 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;동작 흐름:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pod 실행
  → Pod Identity Agent가 임시 자격증명 요청
  → EKS가 연결된 IAM 역할 기반으로 자격증명 발급
  → Pod 내 AWS SDK가 자격증명 자동 사용&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;1. Pod Identity Agent 애드온 설치&lt;/h2&gt;
&lt;p&gt;Pod Identity를 사용하려면 클러스터에 &lt;strong&gt;EKS Pod Identity Agent 애드온&lt;/strong&gt;이 먼저 설치되어 있어야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws eks create-addon \
    --cluster-name {Cluster Name} \
    --addon-name eks-pod-identity-agent \
    --region ap-northeast-2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 완료 여부를 확인합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws eks describe-addon \
    --cluster-name {Cluster Name} \
    --addon-name eks-pod-identity-agent \
    --region ap-northeast-2 \
    --query &amp;quot;addon.status&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;ACTIVE&amp;quot;&lt;/code&gt;가 출력되면 준비 완료입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. IAM 정책 생성&lt;/h2&gt;
&lt;p&gt;ALB를 생성·수정할 수 있는 IAM 정책을 만듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 정책 파일 다운로드
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v3.0.0/docs/install/iam_policy.json

# 정책 생성
aws iam create-policy \
    --policy-name AWSLoadBalancerControllerIAMPolicy \
    --policy-document file://iam_policy.json&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성 후 출력되는 &lt;code&gt;Policy.Arn&lt;/code&gt; 값을 메모해 두세요. 이후 단계에서 사용합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Pod Identity용 IAM 역할 생성&lt;/h2&gt;
&lt;p&gt;Pod Identity에서는 &lt;code&gt;pods.eks.amazonaws.com&lt;/code&gt;을 신뢰하는 IAM 역할을 만들어야 합니다.&lt;/p&gt;
&lt;p&gt;아래 내용으로 &lt;code&gt;trust-policy.json&lt;/code&gt;을 생성합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;Version&amp;quot;: &amp;quot;2012-10-17&amp;quot;,
  &amp;quot;Statement&amp;quot;: [
    {
      &amp;quot;Effect&amp;quot;: &amp;quot;Allow&amp;quot;,
      &amp;quot;Principal&amp;quot;: {
        &amp;quot;Service&amp;quot;: &amp;quot;pods.eks.amazonaws.com&amp;quot;
      },
      &amp;quot;Action&amp;quot;: [
        &amp;quot;sts:AssumeRole&amp;quot;,
        &amp;quot;sts:TagSession&amp;quot;
      ]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;역할을 생성하고 앞서 만든 정책을 연결합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 역할 생성
aws iam create-role \
    --role-name AWSLoadBalancerControllerRole \
    --assume-role-policy-document file://trust-policy.json

# 정책 연결
aws iam attach-role-policy \
    --role-name AWSLoadBalancerControllerRole \
    --policy-arn {Policy ARN}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;4. Pod Identity Association 생성&lt;/h2&gt;
&lt;p&gt;IAM 역할과 Kubernetes ServiceAccount를 연결합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws eks create-pod-identity-association \
    --cluster-name {Cluster Name} \
    --namespace kube-system \
    --service-account aws-load-balancer-controller \
    --role-arn {Role ARN} \
    --region ap-northeast-2&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;5. ServiceAccount 생성&lt;/h2&gt;
&lt;p&gt;Pod Identity에서는 ServiceAccount에 어노테이션이 필요 없습니다. 이름만 맞춰서 생성하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl create serviceaccount aws-load-balancer-controller \
    -n kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;6. AWS Load Balancer Controller 설치&lt;/h2&gt;
&lt;p&gt;Helm으로 설치합니다. Helm이 없다면 먼저 설치해주세요.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EKS Helm 레포지토리를 추가합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm repo add eks https://aws.github.io/eks-charts&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Controller를 설치합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
    -n kube-system \
    --set clusterName={Cluster Name} \
    --set serviceAccount.create=false \
    --set serviceAccount.name=aws-load-balancer-controller \
    --set image.repository=602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon/aws-load-balancer-controller \
    --set region=ap-northeast-2 \
    --set vpcId={VPC ID}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정상 설치 여부를 확인합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get deployment -n kube-system aws-load-balancer-controller&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;7. Ingress 생성해보기&lt;/h2&gt;
&lt;p&gt;2048 게임 예제로 Ingress가 잘 동작하는지 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 생성
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/examples/2048/2048_full.yaml

# 삭제
kubectl delete -f https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/examples/2048/2048_full.yaml&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Kubernetes</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/103</guid>
      <comments>https://binarynum.tistory.com/103#entry103comment</comments>
      <pubDate>Sun, 17 May 2026 20:34:37 +0900</pubDate>
    </item>
    <item>
      <title>HTTPS는 어떻게 안전한 걸까?</title>
      <link>https://binarynum.tistory.com/102</link>
      <description>&lt;h1&gt;TLS 인증서 파헤치기&lt;/h1&gt;
&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;브라우저 주소창에 자물쇠 아이콘이 뜨면 &amp;quot;안전하다&amp;quot;고 느끼죠. 그런데 그게 왜 안전한 건지 설명할 수 있나요?&lt;/p&gt;
&lt;p&gt;그 핵심에 &lt;strong&gt;TLS 인증서&lt;/strong&gt;가 있습니다. 이 글에서는 TLS가 뭔지, 인증서가 어떻게 신뢰를 만들어내는지를 최대한 쉽게 풀어봅니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;TLS가 뭔가요? SSL이랑 다른 건가요?&lt;/h2&gt;
&lt;p&gt;TLS(Transport Layer Security)는 네트워크 통신을 암호화하는 프로토콜입니다. SSL(Secure Sockets Layer)의 후속 버전인데, SSL이라는 이름이 워낙 익숙해져서 지금도 혼용해서 부르는 경우가 많아요.&lt;/p&gt;
&lt;p&gt;정리하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSL → 오래된 버전, 현재는 보안 취약점으로 사용 안 함&lt;/li&gt;
&lt;li&gt;TLS 1.2 → 현재도 널리 쓰임&lt;/li&gt;
&lt;li&gt;TLS 1.3 → 최신 버전, 더 빠르고 안전함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;quot;SSL 인증서 발급받으세요&amp;quot;라고 하면 실제로는 TLS 인증서를 말하는 겁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;TLS가 해주는 세 가지&lt;/h2&gt;
&lt;p&gt;TLS는 크게 세 가지를 보장합니다.&lt;/p&gt;
&lt;h3&gt;암호화 (Encryption)&lt;/h3&gt;
&lt;p&gt;주고받는 데이터를 제3자가 볼 수 없도록 암호화합니다. 카페 와이파이에서 누군가 패킷을 가로채도 내용을 읽을 수 없어요.&lt;/p&gt;
&lt;h3&gt;인증 (Authentication)&lt;/h3&gt;
&lt;p&gt;지금 연결한 서버가 진짜 그 서버인지 확인합니다. 피싱 사이트가 &lt;code&gt;naver.com&lt;/code&gt;인 척 흉내 내도, 인증서가 없으면 브라우저가 경고를 띄웁니다.&lt;/p&gt;
&lt;h3&gt;무결성 (Integrity)&lt;/h3&gt;
&lt;p&gt;전송 중에 데이터가 변조되지 않았음을 보장합니다. 중간에서 누군가 내용을 슬쩍 바꿔도 감지할 수 있어요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인증서란 무엇인가요?&lt;/h2&gt;
&lt;p&gt;인증서는 쉽게 말해 &lt;strong&gt;&amp;quot;이 서버는 믿어도 됩니다&amp;quot;라는 공인된 신분증&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;인증서 안에는 이런 정보가 들어 있어요:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;도메인 이름 (예: &lt;code&gt;example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;공개 키 (public key)&lt;/li&gt;
&lt;li&gt;발급자 (인증 기관, CA)&lt;/li&gt;
&lt;li&gt;유효 기간&lt;/li&gt;
&lt;li&gt;디지털 서명&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;브라우저는 서버에 접속할 때 이 인증서를 받아서 &amp;quot;이게 진짜인지&amp;quot; 검증합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;CA가 뭔데 믿을 수 있는 거예요?&lt;/h2&gt;
&lt;p&gt;CA(Certificate Authority, 인증 기관)는 인증서를 발급하고 서명하는 기관입니다. DigiCert, Let&amp;#39;s Encrypt, Comodo 같은 곳들이에요.&lt;/p&gt;
&lt;p&gt;브라우저(크롬, 파이어폭스 등)와 OS(윈도우, 맥 등)에는 &lt;strong&gt;신뢰하는 CA 목록&lt;/strong&gt;이 미리 내장돼 있습니다. 이걸 루트 CA 스토어라고 해요.&lt;/p&gt;
&lt;p&gt;흐름을 따라가면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;서버 운영자가 CA에 인증서 발급 신청&lt;/li&gt;
&lt;li&gt;CA가 도메인 소유권 등을 검증&lt;/li&gt;
&lt;li&gt;CA가 자신의 개인 키로 인증서에 &lt;strong&gt;서명&lt;/strong&gt; 발급&lt;/li&gt;
&lt;li&gt;브라우저가 인증서를 받아서 CA의 공개 키로 서명 검증&lt;/li&gt;
&lt;li&gt;서명이 유효하면 → 신뢰할 수 있는 서버로 간주&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;즉, CA를 신뢰하기 때문에 CA가 서명한 인증서도 신뢰하는 구조입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인증서 체인 (Certificate Chain)&lt;/h2&gt;
&lt;p&gt;인증서는 보통 혼자 존재하지 않고 &lt;strong&gt;체인&lt;/strong&gt;을 이룹니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;루트 CA (Root CA)
  └── 중간 CA (Intermediate CA)
        └── 서버 인증서 (End-entity Certificate)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;루트 CA가 직접 모든 인증서에 서명하지 않고, 중간 CA에 권한을 위임합니다. 루트 CA의 개인 키는 오프라인에 보관할 정도로 중요하게 취급해요.&lt;/p&gt;
&lt;p&gt;브라우저가 서버 인증서를 받으면, 체인을 따라 루트 CA까지 거슬러 올라가며 검증합니다. 체인이 끊기거나 루트 CA가 신뢰 목록에 없으면 경고가 뜨는 거예요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;TLS 핸드셰이크 — 연결이 맺어지는 과정&lt;/h2&gt;
&lt;h3&gt;TLS 1.2 — 기본 흐름 (2-RTT)&lt;/h3&gt;
&lt;p&gt;브라우저와 서버가 처음 연결할 때 왕복 2회가 필요합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;클라이언트              서버
    │                    │
    │── ClientHello ────►│  1) TLS 버전, 암호화 방식 제안
    │                    │
    │◄─ ServerHello ─────│  2) 암호화 방식 결정 + 인증서 전달
    │   + 인증서           │
    │                    │
    │   [인증서 검증]       │
    │                    │
    │── 키 교환 ─────────► │  3) 공개 키로 암호화된 프리마스터 시크릿 전달
    │                    │
    │◄─ Finished ────────│  4) 핸드셰이크 완료
    │                    │
    │══ 암호화 통신 시작 ══  │&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;핵심은 &lt;strong&gt;세션 키(대칭 키)&lt;/strong&gt; 를 안전하게 교환하는 과정입니다. 이 세션 키로 이후 모든 데이터를 암호화해요.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;TLS 1.3 — 더 빠르고 단순해진 과정 (1-RTT)&lt;/h3&gt;
&lt;p&gt;TLS 1.3은 ClientHello에 키 교환 정보(&lt;code&gt;key_share&lt;/code&gt;)를 함께 보내서 왕복을 1회로 줄였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key_share란?
ECDHE 키 교환에서 클라이언트가 미리 생성한 임시 공개 키입니다.
클라이언트는 연결 시작 전에 임시 키 쌍(공개 키 + 개인 키)을 만들어두고, 공개 키만 key_share에 담아 보냅니다.
서버도 자신의 임시 키 쌍을 만들어 응답에 공개 키를 담아 보내면,
양쪽이 상대방의 공개 키와 자신의 개인 키를 조합해 동일한 세션 키를 독립적으로 계산합니다.
세션 키 자체는 네트워크로 전송되지 않으므로 가로챌 수 없습니다.&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;클라이언트              서버
    │                    │
    │── ClientHello ────►│  1) TLS 버전 + key_share 동시 전송
    │   + key_share      │
    │                    │
    │◄─ ServerHello ─────│  2) {인증서 + Finished} 암호화해서 전달
    │   + {인증서}         │     (이미 세션 키 생성 완료)
    │   + {Finished}     │
    │                    │
    │── {Finished} ── ──►│  3) 확인 완료
    │                    │
    │══ 암호화 통신 시작 ══│&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;{}&lt;/code&gt; 표시는 이미 암호화된 상태로 전송됨을 의미합니다. TLS 1.2보다 훨씬 일찍 암호화가 시작돼요.&lt;/p&gt;
&lt;h4&gt;0-RTT 재연결 — 이전에 접속한 적 있다면&lt;/h4&gt;
&lt;p&gt;같은 서버에 재연결할 때는 이전 세션의 키를 재활용해 첫 패킷부터 데이터를 보낼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;클라이언트              서버
    │                    │
    │── ClientHello ────►│
    │   + 0-RTT 데이터     │  연결과 동시에 요청 전송
    │                    │
    │◄─ ServerHello ─────│
    │   + {응답}          │
    │                    │
    │ ══ 암호화 통신 시작 ══ │&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;단, 0-RTT는 &lt;strong&gt;재전송 공격(Replay Attack)&lt;/strong&gt; 위험이 있어서 멱등성이 없는 요청(예: 결제)에는 사용하지 않는 게 좋습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;멱등성(Idempotency)이란?
같은 요청을 여러 번 보내도 결과가 똑같은 성질을 말합니다.

멱등성 있음: GET /users/1 (몇 번 요청해도 같은 데이터)
             DELETE /users/1 (이미 삭제된 걸 또 삭제해도 결과 동일)
멱등성 없음: POST /orders (요청이 2번 전송되면 주문이 2개 생김)
             POST /payments (중복 결제 발생)

공격자가 캡처한 0-RTT 패킷을 재전송하면 멱등성 없는 요청은 의도치 않게 두 번 실행될 수 있습니다.&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;TLS 1.2 vs 1.3 비교&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;TLS 1.2&lt;/th&gt;
&lt;th&gt;TLS 1.3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;핸드셰이크 왕복&lt;/td&gt;
&lt;td&gt;2-RTT&lt;/td&gt;
&lt;td&gt;1-RTT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재연결&lt;/td&gt;
&lt;td&gt;1-RTT&lt;/td&gt;
&lt;td&gt;0-RTT 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;키 교환 방식&lt;/td&gt;
&lt;td&gt;RSA 또는 DH&lt;/td&gt;
&lt;td&gt;ECDHE 필수 (전방향 비밀성 보장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;암호화 시작 시점&lt;/td&gt;
&lt;td&gt;Finished 이후&lt;/td&gt;
&lt;td&gt;ServerHello 직후&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지원 암호화 방식&lt;/td&gt;
&lt;td&gt;다양 (취약한 것 포함)&lt;/td&gt;
&lt;td&gt;5가지로 제한, 강한 것만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h4&gt;키 교환 방식이란?&lt;/h4&gt;
&lt;p&gt;클라이언트와 서버가 &lt;strong&gt;세션 키를 안전하게 공유하는 방법&lt;/strong&gt;입니다. 세션 키 자체를 네트워크로 직접 보내면 가로챌 수 있기 때문에, 수학적 방법으로 양쪽이 같은 키를 독립적으로 계산해냅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RSA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;클라이언트가 서버의 공개 키로 암호화해서 세션 키 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DH&lt;/strong&gt; (Diffie-Hellman)&lt;/td&gt;
&lt;td&gt;양쪽이 각자 난수를 만들어 수학적으로 같은 값을 도출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECDHE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DH의 타원곡선 버전, 매 연결마다 새 키 쌍 생성 (TLS 1.3 필수)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;ECDHE의 핵심은 &lt;strong&gt;전방향 비밀성(Forward Secrecy)&lt;/strong&gt; 입니다. 매 연결마다 임시 키를 새로 만들기 때문에, 나중에 서버의 개인 키가 유출되더라도 과거 통신 내용은 해독할 수 없어요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인증서 종류 — 뭐가 다를까요?&lt;/h2&gt;
&lt;h3&gt;도메인 기준&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;단일 도메인&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; 하나만 커버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;와일드카드&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*.example.com&lt;/code&gt; — 서브도메인 전체 커버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티 도메인(SAN)&lt;/td&gt;
&lt;td&gt;여러 도메인을 하나의 인증서로 커버&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;검증 수준 기준&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;검증 내용&lt;/th&gt;
&lt;th&gt;발급 속도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;DV (Domain Validation)&lt;/td&gt;
&lt;td&gt;도메인 소유권만 확인&lt;/td&gt;
&lt;td&gt;수 분 ~ 즉시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OV (Organization Validation)&lt;/td&gt;
&lt;td&gt;도메인 + 조직 실체 확인&lt;/td&gt;
&lt;td&gt;수 일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EV (Extended Validation)&lt;/td&gt;
&lt;td&gt;도메인 + 조직 법적 실체 엄격 확인&lt;/td&gt;
&lt;td&gt;수 주&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;일반 서비스라면 DV로 충분합니다. Let&amp;#39;s Encrypt도 DV 인증서를 발급해줘요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 마주치는 인증서 오류들&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;NET::ERR_CERT_AUTHORITY_INVALID&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;인증서를 발급한 CA가 브라우저 신뢰 목록에 없을 때. 자체 서명 인증서(self-signed)나 사설 CA 사용 시 발생합니다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;NET::ERR_CERT_DATE_INVALID&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;인증서 유효 기간이 만료됐을 때. 갱신을 깜빡하면 바로 서비스 장애로 이어집니다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;NET::ERR_CERT_COMMON_NAME_INVALID&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;인증서의 도메인과 실제 접속한 도메인이 다를 때. &lt;code&gt;www.example.com&lt;/code&gt; 인증서로 &lt;code&gt;example.com&lt;/code&gt;에 접속하면 뜰 수 있어요.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;SSL_ERROR_RX_RECORD_TOO_LONG&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;HTTP 포트(80)로 HTTPS 요청이 들어왔을 때 종종 발생합니다. 서버 설정 문제예요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;인증서 관련 자주 쓰는 명령어&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 서버 인증서 정보 확인
openssl s_client -connect example.com:443 -showcerts

# 인증서 파일 내용 확인
openssl x509 -in cert.pem -text -noout

# 만료일만 확인
openssl x509 -in cert.pem -noout -enddate

# CSR (인증서 서명 요청) 생성
openssl req -new -newkey rsa:2048 -nodes \
  -keyout server.key \
  -out server.csr

# 개인 키와 인증서로 만료일 확인 (원격)
echo | openssl s_client -connect example.com:443 2&amp;gt;/dev/null \
  | openssl x509 -noout -dates&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;TLS 인증서는 단순히 &amp;quot;자물쇠 아이콘&amp;quot;이 아니라, 암호화·인증·무결성을 한 번에 보장하는 핵심 보안 인프라입니다.&lt;/p&gt;
&lt;p&gt;구조를 이해하면 인증서 오류가 왜 뜨는지, 어떻게 해결해야 하는지 훨씬 빠르게 파악할 수 있어요. 다음 번에 브라우저 경고가 뜨면 막연히 &amp;quot;위험하다&amp;quot;가 아니라 어디서 체인이 끊겼는지 찾아보세요.&lt;/p&gt;</description>
      <category>Etc</category>
      <category>tls</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/102</guid>
      <comments>https://binarynum.tistory.com/102#entry102comment</comments>
      <pubDate>Sun, 17 May 2026 13:59:56 +0900</pubDate>
    </item>
    <item>
      <title>Helm upgrade</title>
      <link>https://binarynum.tistory.com/101</link>
      <description>&lt;h1&gt;Helm upgrade, 이것만 알면 됩니다&lt;/h1&gt;
&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;Helm은 Kubernetes 패키지 매니저로, &lt;code&gt;helm upgrade&lt;/code&gt; 명령을 통해 배포된 애플리케이션을 업데이트합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시작 전에 확인할 것들&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 현재 설치된 릴리스 목록 확인
helm list -n &amp;lt;namespace&amp;gt;

# 릴리스의 현재 values 확인
helm get values &amp;lt;release-name&amp;gt; -n &amp;lt;namespace&amp;gt;

# 현재 사용 중인 차트 버전 확인
helm get chart &amp;lt;release-name&amp;gt; -n &amp;lt;namespace&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;이미지 태그만 바꾸고 싶을 때&lt;/h2&gt;
&lt;p&gt;차트 버전은 그대로 두고 컨테이너 이미지만 교체할 때 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm upgrade &amp;lt;release-name&amp;gt; &amp;lt;chart&amp;gt; \
  -n &amp;lt;namespace&amp;gt; \
  --reuse-values \
  --set image.tag=&amp;lt;new-tag&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--reuse-values&lt;/code&gt;&lt;/strong&gt;: 기존 values를 그대로 유지하면서 지정한 값만 덮어씁니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설정 여러 개를 한 번에 바꾸려면&lt;/h2&gt;
&lt;p&gt;변경 사항이 여러 개일 때는 values 파일을 수정한 후 적용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# values.yaml 수정 후
helm upgrade &amp;lt;release-name&amp;gt; &amp;lt;chart&amp;gt; \
  -n &amp;lt;namespace&amp;gt; \
  -f values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여러 values 파일을 겹쳐 쓸 수도 있습니다 (뒤에 오는 파일이 우선).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm upgrade &amp;lt;release-name&amp;gt; &amp;lt;chart&amp;gt; \
  -n &amp;lt;namespace&amp;gt; \
  -f values-base.yaml \
  -f values-prod.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;차트 버전 자체를 올려야 할 때&lt;/h2&gt;
&lt;p&gt;차트 자체의 버전을 올릴 때 사용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 최신 차트 정보 가져오기
helm repo update

# 사용 가능한 버전 확인
helm search repo &amp;lt;chart-name&amp;gt; --versions

# 특정 버전으로 업그레이드
helm upgrade &amp;lt;release-name&amp;gt; &amp;lt;repo&amp;gt;/&amp;lt;chart-name&amp;gt; \
  -n &amp;lt;namespace&amp;gt; \
  --version &amp;lt;chart-version&amp;gt; \
  -f values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;적용 전에 뭐가 바뀌는지 먼저 보는 법&lt;/h2&gt;
&lt;p&gt;실제 적용 전에 무엇이 바뀌는지 확인합니다. &lt;code&gt;helm-diff&lt;/code&gt; 플러그인이 필요합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# helm-diff 플러그인 설치 (최초 1회)
helm plugin install https://github.com/databus23/helm-diff

# 변경 사항 미리 보기
helm diff upgrade &amp;lt;release-name&amp;gt; &amp;lt;chart&amp;gt; \
  -n &amp;lt;namespace&amp;gt; \
  -f values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--dry-run&lt;/code&gt;으로 렌더링된 매니페스트만 확인할 수도 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm upgrade &amp;lt;release-name&amp;gt; &amp;lt;chart&amp;gt; \
  -n &amp;lt;namespace&amp;gt; \
  -f values.yaml \
  --dry-run&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;업데이트하고 나서 꼭 확인할 것들&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 릴리스 상태 확인
helm status &amp;lt;release-name&amp;gt; -n &amp;lt;namespace&amp;gt;

# 히스토리 확인
helm history &amp;lt;release-name&amp;gt; -n &amp;lt;namespace&amp;gt;

# Pod 상태 확인
kubectl get pods -n &amp;lt;namespace&amp;gt;

# 롤아웃 완료 대기
kubectl rollout status deployment/&amp;lt;deployment-name&amp;gt; -n &amp;lt;namespace&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;뭔가 잘못됐다면? 롤백하면 됩니다&lt;/h2&gt;
&lt;p&gt;업데이트 후 문제가 생기면 이전 리비전으로 되돌립니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 히스토리에서 롤백할 리비전 번호 확인
helm history &amp;lt;release-name&amp;gt; -n &amp;lt;namespace&amp;gt;

# 직전 버전으로 롤백
helm rollback &amp;lt;release-name&amp;gt; -n &amp;lt;namespace&amp;gt;

# 특정 리비전으로 롤백
helm rollback &amp;lt;release-name&amp;gt; &amp;lt;revision&amp;gt; -n &amp;lt;namespace&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;알아두면 유용한 옵션 모음&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--reuse-values&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기존 values 유지, 지정한 값만 덮어씀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--reset-values&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;values를 차트 기본값으로 초기화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--set key=value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;개별 값 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-f values.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;values 파일 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;차트 버전 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--atomic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실패 시 자동 롤백&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;대기 시간 설정 (기본 5m)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--dry-run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 적용 없이 렌더링 결과만 출력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;배포 전 체크리스트 — 이것만은 꼭&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 현재 values 백업: &lt;code&gt;helm get values &amp;lt;release-name&amp;gt; -n &amp;lt;namespace&amp;gt; &amp;gt; values-backup.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;--dry-run&lt;/code&gt; 또는 &lt;code&gt;helm diff&lt;/code&gt;로 변경 사항 사전 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 업데이트 전 현재 리비전 번호 메모 (&lt;code&gt;helm history&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 업데이트 후 Pod 상태 및 로그 확인&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 문제 발생 시 즉시 &lt;code&gt;helm rollback&lt;/code&gt; 실행&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kubernetes</category>
      <category>helm</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/101</guid>
      <comments>https://binarynum.tistory.com/101#entry101comment</comments>
      <pubDate>Sun, 17 May 2026 13:28:52 +0900</pubDate>
    </item>
    <item>
      <title>ArgoCD Notifications Slack 연동</title>
      <link>https://binarynum.tistory.com/100</link>
      <description>&lt;!-- 티스토리 HTML 에디터에 그대로 붙여넣기 --&gt;
&lt;style&gt;
.argo-post { font-family: 'Noto Sans KR', sans-serif; color: #1a1a1a; line-height: 1.8; max-width: 780px; margin: 0 auto; }
.argo-post h2 { font-size: 1.45rem; font-weight: 700; margin: 2.4rem 0 0.8rem; padding-bottom: 0.4rem; border-bottom: 2px solid #e8e8e8; color: #111; }
.argo-post h3 { font-size: 1.15rem; font-weight: 700; margin: 1.8rem 0 0.5rem; color: #222; }
.argo-post p { margin: 0.7rem 0 1rem; }
.argo-post ul, .argo-post ol { padding-left: 1.4rem; margin: 0.5rem 0 1rem; }
.argo-post li { margin: 0.3rem 0; }
.argo-post code { background: #f3f4f6; border-radius: 4px; padding: 2px 6px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.88em; color: #d63031; }
.argo-post pre { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 10px; padding: 1.2rem 1.4rem; overflow-x: auto; margin: 1.2rem 0; }
.argo-post pre code { background: none; color: #24292f; font-size: 0.85rem; padding: 0; line-height: 1.7; }
.argo-post .callout { border-left: 4px solid #6c63ff; background: #f5f3ff; border-radius: 0 8px 8px 0; padding: 0.9rem 1.2rem; margin: 1.2rem 0; font-size: 0.95rem; color: #3d3565; }
.argo-post .callout.tip { border-color: #00b894; background: #f0fdf8; color: #1a5c45; }
.argo-post .callout.warn { border-color: #e17055; background: #fff5f3; color: #7a2a1a; }
.argo-post .series-box { background: #f8f9ff; border: 1px solid #dde1ff; border-radius: 10px; padding: 1rem 1.3rem; margin: 1.5rem 0; font-size: 0.92rem; }
.argo-post .series-box strong { display: block; margin-bottom: 0.4rem; color: #4a4aaa; }
.argo-post .series-box ul { margin: 0; padding-left: 1.3rem; }
.argo-post .series-box li { margin: 0.25rem 0; color: #555; }
.argo-post .series-box li.current { font-weight: 700; color: #333; }
.argo-post table { width: 100%; border-collapse: collapse; margin: 1.3rem 0; font-size: 0.92rem; }
.argo-post th { background: #2d2d3f; color: #fff; padding: 0.65rem 1rem; text-align: left; font-weight: 600; }
.argo-post td { padding: 0.6rem 1rem; border-bottom: 1px solid #eee; vertical-align: top; }
.argo-post tr:nth-child(even) td { background: #fafafa; }
.argo-post .step { display: flex; gap: 14px; margin-bottom: 2rem; align-items: flex-start; }
.argo-post .step-num { width: 28px; height: 28px; border-radius: 50%; background: #2d2d3f; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; flex-shrink: 0; margin-top: 2px; }
.argo-post .step-body { flex: 1; }
.argo-post .step-body h3 { margin: 0 0 0.4rem; font-size: 1.05rem; }
.argo-post .slack-preview { background: #1a1d21; border-radius: 10px; padding: 1.2rem 1.4rem; margin: 1rem 0; font-family: monospace; font-size: 0.85rem; line-height: 1.7; }
.argo-post .slack-preview .sp-bar { display: inline-block; width: 4px; border-radius: 2px; margin-right: 10px; vertical-align: top; }
.argo-post .slack-preview .sp-content { display: inline-block; vertical-align: top; width: calc(100% - 20px); }
.argo-post .slack-preview .sp-title { font-weight: 700; font-size: 0.9rem; margin-bottom: 4px; }
.argo-post .slack-preview .sp-field { color: #9aa0a6; font-size: 0.82rem; }
.argo-post .slack-preview .sp-val  { color: #e8eaed; font-size: 0.82rem; }
.argo-post .success .sp-bar { background: #2eb67d; }
.argo-post .success .sp-title { color: #2eb67d; }
.argo-post .failure .sp-bar { background: #e01e5a; }
.argo-post .failure .sp-title { color: #e01e5a; }
&lt;/style&gt;

&lt;div class=&quot;argo-post&quot;&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  GitOps / ArgoCD 시리즈&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li&gt;1편 — ArgoCD Helm 설치 시 생성되는 컴포넌트 역할 정리&lt;/li&gt;
    &lt;li&gt;2편 — ArgoCD Application, AppProject 개념 정리&lt;/li&gt;
    &lt;li&gt;3편 — ApplicationSet으로 멀티 클러스터 배포 자동화&lt;/li&gt;
    &lt;li class=&quot;current&quot;&gt;4편 — ArgoCD Notifications Slack 연동 가이드 (현재 글)&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;ArgoCD는 기본적으로 배포 결과를 알려주지 않습니다. &lt;code&gt;argocd-notifications-controller&lt;/code&gt;를 설정하면 Sync 성공/실패, Health 상태 변화를 Slack으로 받을 수 있습니다. 이 글에서는 Slack Webhook 방식으로 알림을 설정하는 방법을 단계별로 정리합니다.&lt;/p&gt;

&lt;div class=&quot;callout&quot;&gt;
  ArgoCD Notifications는 &lt;strong&gt;trigger&lt;/strong&gt;(언제 알릴지) + &lt;strong&gt;template&lt;/strong&gt;(어떻게 알릴지) + &lt;strong&gt;subscription&lt;/strong&gt;(어디로 알릴지) 세 가지 조합으로 동작합니다.
&lt;/div&gt;

&lt;h2&gt;동작 원리&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;[Application 상태 변화]
       │
       ▼
[trigger 조건 평가] ── 조건 불충족 → 무시
       │ 조건 충족
       ▼
[template으로 메시지 생성]
       │
       ▼
[subscription에 등록된 채널로 발송]
       │
       ▼
[Slack / Email / PagerDuty / ...]&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;설정 방법&lt;/h2&gt;

&lt;div class=&quot;step&quot;&gt;
  &lt;div class=&quot;step-num&quot;&gt;1&lt;/div&gt;
  &lt;div class=&quot;step-body&quot;&gt;
    &lt;h3&gt;Slack Webhook URL 발급&lt;/h3&gt;
    &lt;p&gt;Slack 워크스페이스에서 &lt;strong&gt;앱 관리 → Incoming Webhooks → 새 Webhook 추가&lt;/strong&gt;로 채널별 Webhook URL을 발급합니다.&lt;/p&gt;
    &lt;p&gt;URL 형식: &lt;code&gt;https://hooks.slack.com/services/T.../B.../...&lt;/code&gt;&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;step&quot;&gt;
  &lt;div class=&quot;step-num&quot;&gt;2&lt;/div&gt;
  &lt;div class=&quot;step-body&quot;&gt;
    &lt;h3&gt;Webhook URL을 Secret으로 저장&lt;/h3&gt;
    &lt;pre&gt;&lt;code&gt;kubectl create secret generic argocd-notifications-secret \
  --from-literal=slack-token=https://hooks.slack.com/services/T.../B.../... \
  -n argocd&lt;/code&gt;&lt;/pre&gt;
    &lt;p&gt;또는 이미 Secret이 있다면 패치:&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;kubectl patch secret argocd-notifications-secret \
  -n argocd \
  -p '{&quot;stringData&quot;: {&quot;slack-token&quot;: &quot;https://hooks.slack.com/services/...&quot;}}'&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;step&quot;&gt;
  &lt;div class=&quot;step-num&quot;&gt;3&lt;/div&gt;
  &lt;div class=&quot;step-body&quot;&gt;
    &lt;h3&gt;values.yaml에 Notifications 설정 추가&lt;/h3&gt;
    &lt;p&gt;Helm으로 설치했다면 &lt;code&gt;values.yaml&lt;/code&gt;에 아래 설정을 추가하고 &lt;code&gt;helm upgrade&lt;/code&gt;로 적용합니다.&lt;/p&gt;
    &lt;pre&gt;&lt;code&gt;notifications:
  enabled: true

  secret:
    create: false              # 위에서 직접 만든 Secret 사용

  cm:
    create: true

  # 발송 서비스 설정
  notifiers:
    service.slack: |
      token: $slack-token      # Secret의 slack-token 키 참조

  # Trigger: 언제 알릴지
  triggers:
    trigger.on-sync-succeeded: |
      - when: app.status.operationState.phase in ['Succeeded']
        send: [app-sync-succeeded]

    trigger.on-sync-failed: |
      - when: app.status.operationState.phase in ['Error', 'Failed']
        send: [app-sync-failed]

    trigger.on-health-degraded: |
      - when: app.status.health.status == 'Degraded'
        send: [app-health-degraded]

    trigger.on-deployed: |
      - when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'
        send: [app-deployed]

  # Template: 어떻게 알릴지
  templates:
    template.app-sync-succeeded: |
      slack:
        attachments: |
          [{
            &quot;color&quot;: &quot;#2eb67d&quot;,
            &quot;title&quot;: &quot;✅ Sync 성공: {{.app.metadata.name}}&quot;,
            &quot;fields&quot;: [
              {&quot;title&quot;: &quot;환경&quot;, &quot;value&quot;: &quot;{{.app.spec.destination.server}}&quot;, &quot;short&quot;: true},
              {&quot;title&quot;: &quot;Revision&quot;, &quot;value&quot;: &quot;{{.app.status.sync.revision}}&quot;, &quot;short&quot;: true},
              {&quot;title&quot;: &quot;시각&quot;, &quot;value&quot;: &quot;{{.app.status.operationState.finishedAt}}&quot;, &quot;short&quot;: false}
            ]
          }]

    template.app-sync-failed: |
      slack:
        attachments: |
          [{
            &quot;color&quot;: &quot;#e01e5a&quot;,
            &quot;title&quot;: &quot;❌ Sync 실패: {{.app.metadata.name}}&quot;,
            &quot;fields&quot;: [
              {&quot;title&quot;: &quot;에러&quot;, &quot;value&quot;: &quot;{{.app.status.operationState.message}}&quot;, &quot;short&quot;: false},
              {&quot;title&quot;: &quot;Revision&quot;, &quot;value&quot;: &quot;{{.app.status.sync.revision}}&quot;, &quot;short&quot;: true}
            ]
          }]

    template.app-health-degraded: |
      slack:
        attachments: |
          [{
            &quot;color&quot;: &quot;#ff6b35&quot;,
            &quot;title&quot;: &quot;⚠️ Health Degraded: {{.app.metadata.name}}&quot;,
            &quot;fields&quot;: [
              {&quot;title&quot;: &quot;상태&quot;, &quot;value&quot;: &quot;{{.app.status.health.status}}&quot;, &quot;short&quot;: true},
              {&quot;title&quot;: &quot;Message&quot;, &quot;value&quot;: &quot;{{.app.status.health.message}}&quot;, &quot;short&quot;: false}
            ]
          }]

    template.app-deployed: |
      slack:
        attachments: |
          [{
            &quot;color&quot;: &quot;#2eb67d&quot;,
            &quot;title&quot;: &quot;  배포 완료: {{.app.metadata.name}}&quot;,
            &quot;fields&quot;: [
              {&quot;title&quot;: &quot;Namespace&quot;, &quot;value&quot;: &quot;{{.app.spec.destination.namespace}}&quot;, &quot;short&quot;: true},
              {&quot;title&quot;: &quot;Revision&quot;, &quot;value&quot;: &quot;{{.app.status.sync.revision}}&quot;, &quot;short&quot;: true}
            ]
          }]

  # 기본 Subscription: 모든 Application에 적용
  subscriptions: |
    - recipients:
        - slack:#devops-alerts    # 알림 받을 Slack 채널
      triggers:
        - on-sync-failed
        - on-health-degraded&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;step&quot;&gt;
  &lt;div class=&quot;step-num&quot;&gt;4&lt;/div&gt;
  &lt;div class=&quot;step-body&quot;&gt;
    &lt;h3&gt;Helm upgrade로 적용&lt;/h3&gt;
    &lt;pre&gt;&lt;code&gt;helm upgrade argocd argo/argo-cd \
  -n argocd \
  -f values.yaml&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;Slack 알림 미리보기&lt;/h2&gt;

&lt;p&gt;위 template 설정을 적용하면 Slack에서 이렇게 보입니다.&lt;/p&gt;

&lt;div class=&quot;slack-preview success&quot;&gt;
  &lt;span class=&quot;sp-bar&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;sp-content&quot;&gt;
    &lt;div class=&quot;sp-title&quot;&gt;✅ Sync 성공: my-app-production&lt;/div&gt;
    &lt;div&gt;&lt;span class=&quot;sp-field&quot;&gt;환경 &lt;/span&gt;&lt;span class=&quot;sp-val&quot;&gt;https://prod.example.com&lt;/span&gt;&lt;/div&gt;
    &lt;div&gt;&lt;span class=&quot;sp-field&quot;&gt;Revision &lt;/span&gt;&lt;span class=&quot;sp-val&quot;&gt;a1b2c3d&lt;/span&gt;&lt;/div&gt;
    &lt;div&gt;&lt;span class=&quot;sp-field&quot;&gt;시각 &lt;/span&gt;&lt;span class=&quot;sp-val&quot;&gt;2026-05-16T10:32:00Z&lt;/span&gt;&lt;/div&gt;
  &lt;/span&gt;
&lt;/div&gt;

&lt;div class=&quot;slack-preview failure&quot;&gt;
  &lt;span class=&quot;sp-bar&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;sp-content&quot;&gt;
    &lt;div class=&quot;sp-title&quot;&gt;❌ Sync 실패: my-app-production&lt;/div&gt;
    &lt;div&gt;&lt;span class=&quot;sp-field&quot;&gt;에러 &lt;/span&gt;&lt;span class=&quot;sp-val&quot;&gt;Failed to create resource: namespaces &quot;production&quot; not found&lt;/span&gt;&lt;/div&gt;
    &lt;div&gt;&lt;span class=&quot;sp-field&quot;&gt;Revision &lt;/span&gt;&lt;span class=&quot;sp-val&quot;&gt;d4e5f6g&lt;/span&gt;&lt;/div&gt;
  &lt;/span&gt;
&lt;/div&gt;

&lt;h2&gt;Application별 채널 분리 — Subscription 어노테이션&lt;/h2&gt;

&lt;p&gt;Helm values의 &lt;code&gt;subscriptions&lt;/code&gt;는 전체 기본값입니다. 특정 Application에만 다른 채널로 알림을 보내려면 Application 매니페스트에 어노테이션을 추가합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
  annotations:
    # 이 Application은 별도 채널로 알림 수신
    notifications.argoproj.io/subscribe.on-sync-failed.slack: backend-alerts
    notifications.argoproj.io/subscribe.on-deployed.slack: backend-deploys
    notifications.argoproj.io/subscribe.on-health-degraded.slack: backend-alerts&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;ApplicationSet으로 생성하는 경우 template에 어노테이션을 추가합니다:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  template:
    metadata:
      name: &quot;my-app-{{env}}&quot;
      annotations:
        notifications.argoproj.io/subscribe.on-sync-failed.slack: &quot;{{team}}-alerts&quot;
        notifications.argoproj.io/subscribe.on-deployed.slack: &quot;{{team}}-deploys&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Trigger 조건 커스터마이징&lt;/h2&gt;

&lt;p&gt;Trigger는 &lt;a href=&quot;https://expr.medv.io/&quot; target=&quot;_blank&quot;&gt;expr-lang&lt;/a&gt; 문법을 사용합니다. 주요 조건 예시입니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th&gt;조건&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;app.status.operationState.phase in ['Succeeded']&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Sync 성공&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;app.status.operationState.phase in ['Error', 'Failed']&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Sync 실패&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;app.status.health.status == 'Degraded'&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Health Degraded&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;app.status.health.status == 'Healthy'&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Health 정상&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;app.metadata.labels['env'] == 'production'&lt;/code&gt;&lt;/td&gt;&lt;td&gt;production 레이블 Application만&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;time.Now().Sub(time.Parse(app.status.operationState.startedAt)).Minutes() &gt;= 10&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Sync가 10분 이상 걸린 경우&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3&gt;운영 환경만 알림 받기&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;triggers:
  trigger.on-sync-failed: |
    - when: &gt;
        app.status.operationState.phase in ['Error', 'Failed'] and
        app.metadata.labels['env'] == 'production'
      send: [app-sync-failed]&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;알림 동작 테스트&lt;/h2&gt;

&lt;p&gt;설정 후 실제로 알림이 가는지 CLI로 테스트할 수 있습니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# 특정 Application에 대해 알림 강제 발송 테스트
argocd admin notifications trigger run on-sync-failed my-app \
  --recipient slack:devops-alerts \
  -n argocd

# 현재 구독 목록 확인
argocd admin notifications service list -n argocd&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;자주 하는 실수&lt;/h2&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;Secret 키 이름이 틀린 경우&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  &lt;code&gt;service.slack&lt;/code&gt;에서 &lt;code&gt;$slack-token&lt;/code&gt;으로 참조할 때, Secret의 키 이름이 정확히 &lt;code&gt;slack-token&lt;/code&gt;이어야 합니다. 키 이름이 다르면 알림 컨트롤러 로그에 &lt;code&gt;secret not found&lt;/code&gt; 에러가 납니다.
  &lt;br&gt;&lt;br&gt;
  확인 명령: &lt;code&gt;kubectl get secret argocd-notifications-secret -n argocd -o jsonpath='{.data}' | base64 -d&lt;/code&gt;
&lt;/div&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;알림이 안 올 때 확인할 것&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  &lt;ol style=&quot;margin:0.5rem 0 0;padding-left:1.3rem;&quot;&gt;
    &lt;li&gt;notifications-controller 파드 로그 확인: &lt;code&gt;kubectl logs -n argocd -l app.kubernetes.io/name=argocd-notifications-controller&lt;/code&gt;&lt;/li&gt;
    &lt;li&gt;ConfigMap 확인: &lt;code&gt;kubectl get cm argocd-notifications-cm -n argocd -o yaml&lt;/code&gt;&lt;/li&gt;
    &lt;li&gt;Application 어노테이션에 subscription이 제대로 달려있는지 확인&lt;/li&gt;
    &lt;li&gt;Slack Webhook URL이 유효한지 curl로 직접 테스트&lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;

&lt;div class=&quot;callout tip&quot;&gt;
  &lt;strong&gt;알림 피로도 줄이기&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  기본 subscription에는 &lt;code&gt;on-sync-failed&lt;/code&gt;와 &lt;code&gt;on-health-degraded&lt;/code&gt;만 걸어두고, 성공 알림(&lt;code&gt;on-deployed&lt;/code&gt;)은 팀 채널에만 선택적으로 구독하는 것을 권장합니다. 모든 Application의 Sync 성공 알림이 오면 알림 피로도가 높아집니다.
&lt;/div&gt;

&lt;h2&gt;마치며&lt;/h2&gt;

&lt;p&gt;ArgoCD Notifications는 trigger + template + subscription 세 가지만 이해하면 충분히 원하는 형태로 커스터마이징할 수 있습니다. 처음에는 &lt;code&gt;on-sync-failed&lt;/code&gt;와 &lt;code&gt;on-health-degraded&lt;/code&gt;만 설정해서 시작하고, 필요에 따라 trigger와 채널을 추가해가는 것을 권장합니다.&lt;/p&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  시리즈 완결&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li&gt;1편 — ArgoCD Helm 설치 시 생성되는 컴포넌트 역할 정리&lt;/li&gt;
    &lt;li&gt;2편 — ArgoCD Application, AppProject 개념 정리&lt;/li&gt;
    &lt;li&gt;3편 — ApplicationSet으로 멀티 클러스터 배포 자동화&lt;/li&gt;
    &lt;li class=&quot;current&quot;&gt;4편 — ArgoCD Notifications Slack 연동 가이드&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;/div&gt;</description>
      <category>Kubernetes</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/100</guid>
      <comments>https://binarynum.tistory.com/100#entry100comment</comments>
      <pubDate>Sat, 16 May 2026 16:29:29 +0900</pubDate>
    </item>
    <item>
      <title>ArgoCD ApplicationSet</title>
      <link>https://binarynum.tistory.com/99</link>
      <description>&lt;!-- 티스토리 HTML 에디터에 그대로 붙여넣기 --&gt;
&lt;style&gt;
.argo-post { font-family: 'Noto Sans KR', sans-serif; color: #1a1a1a; line-height: 1.8; max-width: 780px; margin: 0 auto; }
.argo-post h2 { font-size: 1.45rem; font-weight: 700; margin: 2.4rem 0 0.8rem; padding-bottom: 0.4rem; border-bottom: 2px solid #e8e8e8; color: #111; }
.argo-post h3 { font-size: 1.15rem; font-weight: 700; margin: 1.8rem 0 0.5rem; color: #222; }
.argo-post p { margin: 0.7rem 0 1rem; }
.argo-post ul, .argo-post ol { padding-left: 1.4rem; margin: 0.5rem 0 1rem; }
.argo-post li { margin: 0.3rem 0; }
.argo-post code { background: #f3f4f6; border-radius: 4px; padding: 2px 6px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.88em; color: #d63031; }
.argo-post pre { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 10px; padding: 1.2rem 1.4rem; overflow-x: auto; margin: 1.2rem 0; }
.argo-post pre code { background: none; color: #24292f; font-size: 0.85rem; padding: 0; line-height: 1.7; }
.argo-post .callout { border-left: 4px solid #6c63ff; background: #f5f3ff; border-radius: 0 8px 8px 0; padding: 0.9rem 1.2rem; margin: 1.2rem 0; font-size: 0.95rem; color: #3d3565; }
.argo-post .callout.tip { border-color: #00b894; background: #f0fdf8; color: #1a5c45; }
.argo-post .callout.warn { border-color: #e17055; background: #fff5f3; color: #7a2a1a; }
.argo-post .series-box { background: #f8f9ff; border: 1px solid #dde1ff; border-radius: 10px; padding: 1rem 1.3rem; margin: 1.5rem 0; font-size: 0.92rem; }
.argo-post .series-box strong { display: block; margin-bottom: 0.4rem; color: #4a4aaa; }
.argo-post .series-box ul { margin: 0; padding-left: 1.3rem; }
.argo-post .series-box li { margin: 0.25rem 0; color: #555; }
.argo-post .series-box li.current { font-weight: 700; color: #333; }
.argo-post table { width: 100%; border-collapse: collapse; margin: 1.3rem 0; font-size: 0.92rem; }
.argo-post th { background: #2d2d3f; color: #fff; padding: 0.65rem 1rem; text-align: left; font-weight: 600; }
.argo-post td { padding: 0.6rem 1rem; border-bottom: 1px solid #eee; vertical-align: top; }
.argo-post tr:nth-child(even) td { background: #fafafa; }
.argo-post .gen-card { border-radius: 12px; padding: 1.2rem 1.4rem; border: 1px solid; margin-bottom: 14px; }
.argo-post .gen-card h3 { margin: 0 0 0.4rem; font-size: 1rem; font-family: monospace; }
.argo-post .gen-card .badge { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 20px; margin-bottom: 8px; }
.argo-post .gen-card p { margin: 0 0 0.4rem; font-size: 0.9rem; }
.argo-post .gc-blue   { background: #f0f6ff; border-color: #b8d4f8; }
.argo-post .gc-blue h3 { color: #0a3f7a; }
.argo-post .gc-blue .badge { background: #d8ebff; color: #0a3060; }
.argo-post .gc-teal   { background: #f0fdf8; border-color: #a3dfc4; }
.argo-post .gc-teal h3 { color: #0a6640; }
.argo-post .gc-teal .badge { background: #d1f5e5; color: #0a5533; }
.argo-post .gc-coral  { background: #fff5f0; border-color: #f5c4a8; }
.argo-post .gc-coral h3 { color: #8a3a10; }
.argo-post .gc-coral .badge { background: #ffe8db; color: #7a2f0a; }
.argo-post .gc-purple { background: #f3f0ff; border-color: #c4b8f8; }
.argo-post .gc-purple h3 { color: #3a2f9e; }
.argo-post .gc-purple .badge { background: #e2dbff; color: #2e2580; }
&lt;/style&gt;

&lt;div class=&quot;argo-post&quot;&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  GitOps / ArgoCD 시리즈&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li&gt;1편 — ArgoCD Helm 설치 시 생성되는 컴포넌트 역할 정리&lt;/li&gt;
    &lt;li&gt;2편 — ArgoCD Application, AppProject 개념 정리&lt;/li&gt;
    &lt;li class=&quot;current&quot;&gt;3편 — ApplicationSet으로 멀티 클러스터 배포 자동화 (현재 글)&lt;/li&gt;
    &lt;li&gt;4편 — ArgoCD Notifications Slack 연동 가이드&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;Application을 하나씩 수동으로 만들다 보면 클러스터나 환경이 늘어날수록 관리가 힘들어집니다. &lt;code&gt;ApplicationSet&lt;/code&gt;은 이 문제를 해결하는 ArgoCD의 자동화 리소스입니다. &lt;strong&gt;Generator&lt;/strong&gt;가 만들어내는 파라미터 목록을 템플릿에 주입해서 Application을 자동으로 생성·삭제합니다.&lt;/p&gt;

&lt;div class=&quot;callout&quot;&gt;
  &lt;strong&gt;ApplicationSet의 핵심 구조:&lt;/strong&gt; Generator (파라미터 목록 생성) + Template (Application 템플릿) → Application 자동 생성
&lt;/div&gt;

&lt;h2&gt;기본 구조&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-appset
  namespace: argocd
spec:
  generators:
    - list:                    # Generator: 파라미터 목록
        elements:
          - cluster: prod
            url: https://prod.example.com
          - cluster: staging
            url: https://staging.example.com

  template:                    # Template: Application 청사진
    metadata:
      name: &quot;my-app-{{cluster}}&quot;   # Generator 파라미터 사용
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/my-repo
        targetRevision: main
        path: &quot;k8s/overlays/{{cluster}}&quot;
      destination:
        server: &quot;{{url}}&quot;
        namespace: my-app&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위 ApplicationSet은 &lt;code&gt;my-app-prod&lt;/code&gt;와 &lt;code&gt;my-app-staging&lt;/code&gt; 두 Application을 자동으로 생성합니다. 환경이 추가되면 &lt;code&gt;elements&lt;/code&gt;에 항목만 추가하면 됩니다.&lt;/p&gt;

&lt;h2&gt;Generator 종류&lt;/h2&gt;

&lt;div class=&quot;gen-card gc-blue&quot;&gt;
  &lt;h3&gt;List Generator&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;가장 단순&lt;/span&gt;
  &lt;p&gt;직접 작성한 값 목록으로 Application을 생성합니다. 환경 수가 적고 정적일 때 사용합니다.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;gen-card gc-teal&quot;&gt;
  &lt;h3&gt;Cluster Generator&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;멀티 클러스터 핵심&lt;/span&gt;
  &lt;p&gt;ArgoCD에 등록된 클러스터 목록을 자동으로 읽어서 파라미터를 생성합니다. 클러스터가 추가되면 Application도 자동으로 생성됩니다.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;gen-card gc-coral&quot;&gt;
  &lt;h3&gt;Git Generator&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;디렉토리 / 파일 기반&lt;/span&gt;
  &lt;p&gt;Git 레포의 디렉토리 구조나 JSON/YAML 파일을 읽어서 파라미터를 생성합니다. 디렉토리 하나 = Application 하나 패턴에 유용합니다.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;gen-card gc-purple&quot;&gt;
  &lt;h3&gt;Matrix / Merge Generator&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;조합 / 병합&lt;/span&gt;
  &lt;p&gt;두 Generator의 결과를 곱집합(Matrix)하거나 병합(Merge)해서 복잡한 파라미터 조합을 만듭니다. &quot;모든 앱 × 모든 환경&quot; 패턴에 사용합니다.&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Cluster Generator — 멀티 클러스터 자동 배포&lt;/h2&gt;

&lt;p&gt;가장 많이 쓰는 패턴입니다. ArgoCD에 클러스터를 등록하면 자동으로 해당 클러스터에 Application이 생성됩니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: cluster-addons
  namespace: argocd
spec:
  generators:
    - clusters:
        selector:
          matchLabels:
            env: production    # 레이블로 대상 클러스터 필터링

  template:
    metadata:
      name: &quot;cluster-addons-{{name}}&quot;
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/cluster-addons
        targetRevision: main
        path: addons
      destination:
        server: &quot;{{server}}&quot;   # 클러스터 URL 자동 주입
        namespace: kube-system
      syncPolicy:
        automated:
          prune: true
          selfHeal: true&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;클러스터를 ArgoCD에 등록하는 명령:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# ArgoCD CLI로 클러스터 등록
argocd cluster add my-prod-context --name prod-ap-northeast-2

# 레이블 추가 (Cluster Generator 필터링용)
kubectl label secret -n argocd \
  $(argocd cluster list -o json | jq -r '.[] | select(.name==&quot;prod-ap-northeast-2&quot;) | .connectionState.attemptedAt' ) \
  env=production&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Git Generator — 디렉토리 기반 자동 생성&lt;/h2&gt;

&lt;p&gt;Git 레포의 디렉토리 구조를 그대로 Application으로 매핑합니다. 새 서비스를 추가할 때 디렉토리만 만들면 Application이 자동 생성됩니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# 레포 구조 예시
apps/
├── frontend/
│   └── kustomization.yaml
├── backend-api/
│   └── kustomization.yaml
└── batch-worker/
    └── kustomization.yaml&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: apps-from-dirs
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/my-org/my-repo
        revision: main
        directories:
          - path: apps/*        # apps/ 하위 모든 디렉토리

  template:
    metadata:
      name: &quot;{{path.basename}}&quot;    # 디렉토리 이름이 Application 이름
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/my-repo
        targetRevision: main
        path: &quot;{{path}}&quot;           # 해당 디렉토리 경로
      destination:
        server: https://kubernetes.default.svc
        namespace: &quot;{{path.basename}}&quot;
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Matrix Generator — 앱 × 환경 조합&lt;/h2&gt;

&lt;p&gt;&quot;모든 앱을 모든 환경에 배포&quot;하는 패턴입니다. 앱 목록과 환경 목록의 곱집합으로 Application을 생성합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: all-apps-all-envs
  namespace: argocd
spec:
  generators:
    - matrix:
        generators:
          # Generator 1: 앱 목록 (Git 디렉토리)
          - git:
              repoURL: https://github.com/my-org/my-repo
              revision: main
              directories:
                - path: apps/*

          # Generator 2: 환경 목록
          - list:
              elements:
                - env: staging
                  server: https://staging.example.com
                - env: production
                  server: https://prod.example.com

  template:
    metadata:
      name: &quot;{{path.basename}}-{{env}}&quot;
    spec:
      project: &quot;{{env}}&quot;
      source:
        repoURL: https://github.com/my-org/my-repo
        targetRevision: main
        path: &quot;{{path}}/overlays/{{env}}&quot;
      destination:
        server: &quot;{{server}}&quot;
        namespace: &quot;{{path.basename}}&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;PR 환경 자동화 — Pull Request Generator&lt;/h2&gt;

&lt;p&gt;PR이 열릴 때 임시 환경을 자동으로 만들고, PR이 닫히면 삭제하는 패턴입니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: pr-preview
  namespace: argocd
spec:
  generators:
    - pullRequest:
        github:
          owner: my-org
          repo: my-repo
          tokenRef:
            secretName: github-token
            key: token
          labels:
            - preview              # 'preview' 레이블이 달린 PR만 대상

  template:
    metadata:
      name: &quot;pr-{{number}}-preview&quot;
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/my-repo
        targetRevision: &quot;{{head_sha}}&quot;    # PR의 최신 커밋
        path: k8s/overlays/preview
      destination:
        server: https://kubernetes.default.svc
        namespace: &quot;pr-{{number}}&quot;        # PR 번호별 namespace
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;syncPolicy.preserveResourcesOnDeletion&lt;/h2&gt;

&lt;p&gt;ApplicationSet이 삭제되거나 Generator에서 항목이 제거될 때, 생성된 Application과 그 리소스를 어떻게 처리할지 설정합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;spec:
  syncPolicy:
    preserveResourcesOnDeletion: true   # true: Application 삭제 시 클러스터 리소스 유지
                                        # false (기본): Application 삭제 시 리소스도 삭제&lt;/code&gt;&lt;/pre&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;PR Generator 사용 시 주의&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  PR이 닫히면 namespace와 그 안의 모든 리소스가 삭제됩니다. &lt;code&gt;preserveResourcesOnDeletion: true&lt;/code&gt;를 사용하면 리소스는 남기고 Application만 삭제합니다. 데이터가 중요한 서비스라면 반드시 확인하세요.
&lt;/div&gt;

&lt;h2&gt;실무 팁&lt;/h2&gt;

&lt;div class=&quot;callout tip&quot;&gt;
  &lt;strong&gt;tempate.metadata.annotations으로 알림 연동&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  ApplicationSet으로 생성된 Application에도 Notifications 어노테이션을 템플릿에 추가하면 자동으로 알림이 붙습니다.
  &lt;br&gt;&lt;br&gt;
  &lt;code&gt;notifications.argoproj.io/subscribe.on-sync-failed.slack: devops-alerts&lt;/code&gt;
&lt;/div&gt;

&lt;div class=&quot;callout tip&quot;&gt;
  &lt;strong&gt;ignoreApplicationDifferences로 일부 필드 무시&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  ApplicationSet이 관리하지 않는 필드(예: 수동으로 추가한 어노테이션)가 있을 때 ApplicationSet이 계속 덮어쓰는 문제를 방지합니다.
&lt;/div&gt;

&lt;pre&gt;&lt;code&gt;spec:
  ignoreApplicationDifferences:
    - jsonPointers:
        - /spec/source/targetRevision   # 이미지 태그 등 별도 관리하는 필드 무시&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;마치며&lt;/h2&gt;

&lt;p&gt;ApplicationSet은 &quot;Application을 어떻게 자동화할 것인가&quot;의 답입니다. List Generator로 시작해서 Git Generator, Cluster Generator 순서로 익혀가면 자연스럽게 멀티 클러스터 GitOps 체계를 갖출 수 있습니다.&lt;/p&gt;

&lt;p&gt;다음 편에서는 &lt;code&gt;argocd-notifications-controller&lt;/code&gt;를 이용해 Slack 알림을 설정하는 방법을 다룹니다.&lt;/p&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  다음 편&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li&gt;4편 — ArgoCD Notifications Slack 연동 가이드&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;/div&gt;</description>
      <category>Kubernetes</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/99</guid>
      <comments>https://binarynum.tistory.com/99#entry99comment</comments>
      <pubDate>Sat, 16 May 2026 16:28:39 +0900</pubDate>
    </item>
    <item>
      <title>ArgoCD Application, AppProject</title>
      <link>https://binarynum.tistory.com/98</link>
      <description>&lt;!-- 티스토리 HTML 에디터에 그대로 붙여넣기 --&gt;
&lt;style&gt;
.argo-post { font-family: 'Noto Sans KR', sans-serif; color: #1a1a1a; line-height: 1.8; max-width: 780px; margin: 0 auto; }
.argo-post h2 { font-size: 1.45rem; font-weight: 700; margin: 2.4rem 0 0.8rem; padding-bottom: 0.4rem; border-bottom: 2px solid #e8e8e8; color: #111; }
.argo-post h3 { font-size: 1.15rem; font-weight: 700; margin: 1.8rem 0 0.5rem; color: #222; }
.argo-post p { margin: 0.7rem 0 1rem; }
.argo-post ul, .argo-post ol { padding-left: 1.4rem; margin: 0.5rem 0 1rem; }
.argo-post li { margin: 0.3rem 0; }
.argo-post code { background: #f3f4f6; border-radius: 4px; padding: 2px 6px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.88em; color: #d63031; }
.argo-post pre { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 10px; padding: 1.2rem 1.4rem; overflow-x: auto; margin: 1.2rem 0; }
.argo-post pre code { background: none; color: #24292f; font-size: 0.85rem; padding: 0; line-height: 1.7; }
.argo-post .callout { border-left: 4px solid #6c63ff; background: #f5f3ff; border-radius: 0 8px 8px 0; padding: 0.9rem 1.2rem; margin: 1.2rem 0; font-size: 0.95rem; color: #3d3565; }
.argo-post .callout.tip { border-color: #00b894; background: #f0fdf8; color: #1a5c45; }
.argo-post .callout.warn { border-color: #e17055; background: #fff5f3; color: #7a2a1a; }
.argo-post .series-box { background: #f8f9ff; border: 1px solid #dde1ff; border-radius: 10px; padding: 1rem 1.3rem; margin: 1.5rem 0; font-size: 0.92rem; }
.argo-post .series-box strong { display: block; margin-bottom: 0.4rem; color: #4a4aaa; }
.argo-post .series-box ul { margin: 0; padding-left: 1.3rem; }
.argo-post .series-box li { margin: 0.25rem 0; color: #555; }
.argo-post .series-box li.current { font-weight: 700; color: #333; }
.argo-post table { width: 100%; border-collapse: collapse; margin: 1.3rem 0; font-size: 0.92rem; }
.argo-post th { background: #2d2d3f; color: #fff; padding: 0.65rem 1rem; text-align: left; font-weight: 600; }
.argo-post td { padding: 0.6rem 1rem; border-bottom: 1px solid #eee; vertical-align: top; }
.argo-post tr:nth-child(even) td { background: #fafafa; }
.argo-post .concept-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 1.5rem 0; }
.argo-post .concept-card { border-radius: 12px; padding: 1.2rem 1.4rem; border: 1px solid; }
.argo-post .concept-card h3 { margin: 0 0 0.5rem; font-size: 1rem; font-family: monospace; }
.argo-post .concept-card p { margin: 0; font-size: 0.9rem; }
.argo-post .c-purple { background: #f3f0ff; border-color: #c4b8f8; }
.argo-post .c-purple h3 { color: #3a2f9e; }
.argo-post .c-teal { background: #f0fdf8; border-color: #a3dfc4; }
.argo-post .c-teal h3 { color: #0a6640; }
.argo-post .status-row { display: flex; gap: 10px; flex-wrap: wrap; margin: 1rem 0; }
.argo-post .status-badge { padding: 4px 12px; border-radius: 20px; font-size: 0.85rem; font-weight: 600; }
.argo-post .st-synced    { background: #d1f5e5; color: #0a5533; }
.argo-post .st-outofsync { background: #ffe8db; color: #7a2f0a; }
.argo-post .st-unknown   { background: #f3f4f6; color: #444; }
.argo-post .st-healthy   { background: #d1f5e5; color: #0a5533; }
.argo-post .st-degraded  { background: #ffe0e0; color: #7a1010; }
.argo-post .st-progressing { background: #e8f4ff; color: #0a3f7a; }
.argo-post .st-missing   { background: #fff8e0; color: #6a4a00; }
@media(max-width:600px){ .argo-post .concept-grid { grid-template-columns: 1fr; } }
&lt;/style&gt;

&lt;div class=&quot;argo-post&quot;&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  GitOps / ArgoCD 시리즈&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li&gt;1편 — ArgoCD Helm 설치 시 생성되는 컴포넌트 역할 정리&lt;/li&gt;
    &lt;li class=&quot;current&quot;&gt;2편 — ArgoCD Application, AppProject 개념 정리 (현재 글)&lt;/li&gt;
    &lt;li&gt;3편 — ApplicationSet으로 멀티 클러스터 배포 자동화&lt;/li&gt;
    &lt;li&gt;4편 — ArgoCD Notifications Slack 연동 가이드&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;ArgoCD를 처음 쓰면 &lt;code&gt;Application&lt;/code&gt;과 &lt;code&gt;AppProject&lt;/code&gt;가 헷갈립니다. 이 두 가지는 ArgoCD의 핵심 커스텀 리소스로, 이 개념을 잘 이해해야 멀티 팀 환경에서 ArgoCD를 제대로 운영할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;callout&quot;&gt;
  이 글에서는 ArgoCD의 두 핵심 CRD인 &lt;code&gt;Application&lt;/code&gt;과 &lt;code&gt;AppProject&lt;/code&gt;의 역할과 관계, 그리고 실무에서 어떻게 구성하는지를 다룹니다.
&lt;/div&gt;

&lt;h2&gt;Application — &quot;무엇을 어디에 배포할 것인가&quot;&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Application&lt;/code&gt;은 ArgoCD에서 배포의 기본 단위입니다. &lt;strong&gt;Git 레포의 어느 경로&lt;/strong&gt;에 있는 매니페스트를 &lt;strong&gt;어느 클러스터/네임스페이스&lt;/strong&gt;에 배포할지를 정의합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd            # ArgoCD가 설치된 namespace
spec:
  project: default             # 어느 AppProject에 속하는지

  # 소스: Git 어디에서 가져오나
  source:
    repoURL: https://github.com/my-org/my-repo
    targetRevision: main       # 브랜치, 태그, 커밋 SHA 모두 가능
    path: k8s/overlays/prod    # 레포 내 경로 (Kustomize, Helm 등)

  # 목적지: 어느 클러스터/네임스페이스에 배포하나
  destination:
    server: https://kubernetes.default.svc   # 현재 클러스터
    namespace: production

  # Sync 정책
  syncPolicy:
    automated:
      prune: true              # Git에서 삭제된 리소스 자동 제거
      selfHeal: true           # 수동으로 변경된 리소스 자동 복구
    syncOptions:
      - CreateNamespace=true   # namespace 없으면 자동 생성&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;source — Helm 차트 배포&lt;/h3&gt;

&lt;p&gt;Helm 차트를 사용할 경우 &lt;code&gt;source&lt;/code&gt;에 &lt;code&gt;helm&lt;/code&gt; 섹션을 추가합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  source:
    repoURL: https://charts.bitnami.com/bitnami
    chart: redis               # 차트 이름
    targetRevision: 18.x.x     # 차트 버전
    helm:
      releaseName: my-redis
      values: |
        auth:
          enabled: false
        replica:
          replicaCount: 2&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;syncPolicy — Sync 동작 제어&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th&gt;옵션&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;th&gt;권장&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;automated.prune&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Git에서 삭제된 리소스를 클러스터에서도 삭제&lt;/td&gt;&lt;td&gt;운영 환경 주의&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;automated.selfHeal&lt;/code&gt;&lt;/td&gt;&lt;td&gt;클러스터에서 수동 변경 시 Git 상태로 되돌림&lt;/td&gt;&lt;td&gt;GitOps 원칙상 권장&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;CreateNamespace=true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;destination namespace가 없으면 자동 생성&lt;/td&gt;&lt;td&gt;권장&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;ServerSideApply=true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;kubectl apply 대신 server-side apply 사용&lt;/td&gt;&lt;td&gt;CRD 충돌 방지 시 유용&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;RespectIgnoreDifferences=true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ignoreDifferences 설정을 Sync 시에도 적용&lt;/td&gt;&lt;td&gt;상황에 따라&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;Application 상태 이해하기&lt;/h2&gt;

&lt;p&gt;ArgoCD Web UI에서 보이는 상태는 &lt;strong&gt;Sync 상태&lt;/strong&gt;와 &lt;strong&gt;Health 상태&lt;/strong&gt; 두 가지 축으로 표시됩니다.&lt;/p&gt;

&lt;h3&gt;Sync 상태&lt;/h3&gt;
&lt;div class=&quot;status-row&quot;&gt;
  &lt;span class=&quot;status-badge st-synced&quot;&gt;Synced&lt;/span&gt;
  &lt;span class=&quot;status-badge st-outofsync&quot;&gt;OutOfSync&lt;/span&gt;
  &lt;span class=&quot;status-badge st-unknown&quot;&gt;Unknown&lt;/span&gt;
&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Synced&lt;/strong&gt; — Git desired state와 클러스터 실제 상태가 일치&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OutOfSync&lt;/strong&gt; — 차이가 있음. 아직 Sync 안 했거나 수동 변경이 있는 경우&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Unknown&lt;/strong&gt; — 상태를 알 수 없음 (클러스터 연결 문제 등)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Health 상태&lt;/h3&gt;
&lt;div class=&quot;status-row&quot;&gt;
  &lt;span class=&quot;status-badge st-healthy&quot;&gt;Healthy&lt;/span&gt;
  &lt;span class=&quot;status-badge st-progressing&quot;&gt;Progressing&lt;/span&gt;
  &lt;span class=&quot;status-badge st-degraded&quot;&gt;Degraded&lt;/span&gt;
  &lt;span class=&quot;status-badge st-missing&quot;&gt;Missing&lt;/span&gt;
&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Healthy&lt;/strong&gt; — 모든 리소스가 정상 동작 중&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Progressing&lt;/strong&gt; — Deployment rollout 중 등 아직 완료되지 않은 상태&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Degraded&lt;/strong&gt; — 일부 리소스가 비정상 (Pod CrashLoopBackOff 등)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Missing&lt;/strong&gt; — Git에는 있는데 클러스터에 리소스가 없음&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;AppProject — &quot;누가 어디에 배포할 수 있나&quot;&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AppProject&lt;/code&gt;는 Application을 묶는 논리적 그룹입니다. 팀별 또는 환경별로 &lt;strong&gt;배포 가능한 Git 레포, 클러스터, 네임스페이스를 제한&lt;/strong&gt;하는 RBAC 경계 역할을 합니다.&lt;/p&gt;

&lt;div class=&quot;concept-grid&quot;&gt;
  &lt;div class=&quot;concept-card c-purple&quot;&gt;
    &lt;h3&gt;Application&lt;/h3&gt;
    &lt;p&gt;&quot;무엇을 어디에 배포할 것인가&quot;를 정의하는 배포 단위. 반드시 하나의 AppProject에 속해야 합니다.&lt;/p&gt;
  &lt;/div&gt;
  &lt;div class=&quot;concept-card c-teal&quot;&gt;
    &lt;h3&gt;AppProject&lt;/h3&gt;
    &lt;p&gt;Application들의 논리적 묶음. 허용된 Git 레포, 클러스터, 네임스페이스, 리소스 종류를 제한합니다.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-backend
  namespace: argocd
spec:
  description: &quot;백엔드 팀 프로젝트&quot;

  # 이 프로젝트의 Application이 사용할 수 있는 Git 레포
  sourceRepos:
    - https://github.com/my-org/backend-*   # 와일드카드 가능
    - https://charts.bitnami.com/bitnami

  # 배포 가능한 클러스터와 네임스페이스
  destinations:
    - server: https://kubernetes.default.svc
      namespace: backend-prod
    - server: https://kubernetes.default.svc
      namespace: backend-staging

  # 배포 허용 리소스 종류 (화이트리스트)
  clusterResourceWhitelist:
    - group: &quot;&quot;
      kind: Namespace          # Namespace 생성 허용
  namespaceResourceBlacklist:
    - group: &quot;&quot;
      kind: ResourceQuota      # ResourceQuota는 배포 금지

  # RBAC 역할 정의
  roles:
    - name: developer
      description: &quot;배포 권한만 있는 개발자 역할&quot;
      policies:
        - p, proj:team-backend:developer, applications, get, team-backend/*, allow
        - p, proj:team-backend:developer, applications, sync, team-backend/*, allow
      groups:
        - my-org:backend-team  # GitHub org 팀과 연동&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;default 프로젝트&lt;/h3&gt;

&lt;p&gt;ArgoCD 설치 시 &lt;code&gt;default&lt;/code&gt; AppProject가 자동으로 생성됩니다. &lt;code&gt;sourceRepos: [&quot;*&quot;]&lt;/code&gt;, &lt;code&gt;destinations: [서버: *, 네임스페이스: *]&lt;/code&gt;로 모든 것을 허용하기 때문에, 운영 환경에서는 반드시 팀별 AppProject를 별도로 만들고 default 프로젝트 사용을 제한하는 것을 권장합니다.&lt;/p&gt;

&lt;h2&gt;실무 구성 패턴&lt;/h2&gt;

&lt;h3&gt;패턴 1 — 팀별 AppProject 분리&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;# 백엔드 팀 프로젝트
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-backend
spec:
  sourceRepos:
    - https://github.com/my-org/backend-*
  destinations:
    - server: https://kubernetes.default.svc
      namespace: &quot;backend-*&quot;    # backend- 로 시작하는 네임스페이스만 허용
---
# 프론트엔드 팀 프로젝트
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-frontend
spec:
  sourceRepos:
    - https://github.com/my-org/frontend-*
  destinations:
    - server: https://kubernetes.default.svc
      namespace: &quot;frontend-*&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;패턴 2 — 환경별 AppProject 분리&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;# 운영 환경 프로젝트 (수동 Sync만 허용)
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: env-production
spec:
  sourceRepos:
    - https://github.com/my-org/*
  destinations:
    - server: https://prod-cluster.example.com
      namespace: &quot;*&quot;
  # 운영 환경: Auto-sync 비활성화를 정책으로 강제
  syncWindows:
    - kind: deny
      schedule: &quot;* * * * *&quot;    # 항상 자동 배포 차단
      duration: 1h
      applications: [&quot;*&quot;]
      manualSync: true         # 수동 Sync는 허용&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;패턴 3 — ignoreDifferences로 Sync 노이즈 줄이기&lt;/h3&gt;

&lt;p&gt;Deployment의 &lt;code&gt;replicas&lt;/code&gt;처럼 HPA가 관리하는 필드는 ArgoCD가 OutOfSync로 감지합니다. &lt;code&gt;ignoreDifferences&lt;/code&gt;로 제외할 수 있습니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas        # HPA가 관리하는 replicas 무시
    - group: &quot;&quot;
      kind: Secret
      jsonPointers:
        - /data                 # 외부에서 주입된 Secret 데이터 무시&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;자주 하는 실수&lt;/h2&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;prune: true를 운영 환경에 바로 적용하는 경우&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  &lt;code&gt;prune: true&lt;/code&gt;는 Git에서 파일을 삭제하면 클러스터 리소스도 같이 삭제합니다. 처음에는 &lt;code&gt;prune: false&lt;/code&gt;로 시작해서 ArgoCD가 어떤 리소스를 prune 대상으로 잡는지 확인한 뒤 활성화하세요.
&lt;/div&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;모든 Application을 default 프로젝트에 넣는 경우&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  &lt;code&gt;default&lt;/code&gt; 프로젝트는 모든 레포와 클러스터에 접근 가능합니다. 팀이 늘어나면 반드시 AppProject를 분리해서 격리하세요.
&lt;/div&gt;

&lt;div class=&quot;callout tip&quot;&gt;
  &lt;strong&gt;App of Apps 패턴&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  Application을 하나씩 수동으로 만들지 않고, 하나의 &quot;루트 Application&quot;이 다른 Application YAML들을 Git에서 읽어서 자동으로 생성하게 할 수 있습니다. 이를 App of Apps 패턴이라고 부릅니다. ArgoCD를 처음 부트스트랩할 때 유용합니다.
&lt;/div&gt;

&lt;h2&gt;마치며&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Application&lt;/code&gt;은 배포 단위, &lt;code&gt;AppProject&lt;/code&gt;는 그 배포의 경계와 권한을 정의합니다. 작은 팀에서는 &lt;code&gt;default&lt;/code&gt; 프로젝트 하나로 시작해도 되지만, 팀이 여러 개거나 운영/개발 환경을 명확히 분리해야 한다면 처음부터 AppProject 구조를 잡아두는 것을 권장합니다.&lt;/p&gt;

&lt;p&gt;다음 편에서는 &lt;code&gt;ApplicationSet&lt;/code&gt;을 사용해서 여러 클러스터에 배포를 자동화하는 방법을 다룹니다.&lt;/p&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  다음 편&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li&gt;3편 — ApplicationSet으로 멀티 클러스터 배포 자동화&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;/div&gt;</description>
      <category>Kubernetes</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/98</guid>
      <comments>https://binarynum.tistory.com/98#entry98comment</comments>
      <pubDate>Sat, 16 May 2026 16:25:26 +0900</pubDate>
    </item>
    <item>
      <title>ArgoCD Helm 설치 시 생성되는 컴포넌트 역할 정리</title>
      <link>https://binarynum.tistory.com/97</link>
      <description>&lt;!-- 티스토리 HTML 에디터에 그대로 붙여넣기 --&gt;
&lt;style&gt;
.argo-post { font-family: 'Noto Sans KR', sans-serif; color: #1a1a1a; line-height: 1.8; max-width: 780px; margin: 0 auto; }
.argo-post h2 { font-size: 1.45rem; font-weight: 700; margin: 2.4rem 0 0.8rem; padding-bottom: 0.4rem; border-bottom: 2px solid #e8e8e8; color: #111; }
.argo-post h3 { font-size: 1.15rem; font-weight: 700; margin: 1.8rem 0 0.5rem; color: #222; }
.argo-post p { margin: 0.7rem 0 1rem; }
.argo-post ul { padding-left: 1.4rem; margin: 0.5rem 0 1rem; }
.argo-post li { margin: 0.3rem 0; }
.argo-post code { background: #f3f4f6; border-radius: 4px; padding: 2px 6px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.88em; color: #d63031; }
.argo-post pre { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 10px; padding: 1.2rem 1.4rem; overflow-x: auto; margin: 1.2rem 0; }
.argo-post pre code { background: none; color: #24292f; font-size: 0.85rem; padding: 0; line-height: 1.7; }
.argo-post .callout { border-left: 4px solid #6c63ff; background: #f5f3ff; border-radius: 0 8px 8px 0; padding: 0.9rem 1.2rem; margin: 1.2rem 0; font-size: 0.95rem; color: #3d3565; }
.argo-post .callout.tip { border-color: #00b894; background: #f0fdf8; color: #1a5c45; }
.argo-post .callout.warn { border-color: #e17055; background: #fff5f3; color: #7a2a1a; }
.argo-post .series-box { background: #f8f9ff; border: 1px solid #dde1ff; border-radius: 10px; padding: 1rem 1.3rem; margin: 1.5rem 0; font-size: 0.92rem; }
.argo-post .series-box strong { display: block; margin-bottom: 0.4rem; color: #4a4aaa; }
.argo-post .series-box ul { margin: 0; padding-left: 1.3rem; }
.argo-post .series-box li { margin: 0.25rem 0; color: #555; }
.argo-post .series-box li.current { font-weight: 700; color: #333; }
.argo-post table { width: 100%; border-collapse: collapse; margin: 1.3rem 0; font-size: 0.92rem; }
.argo-post th { background: #2d2d3f; color: #fff; padding: 0.65rem 1rem; text-align: left; font-weight: 600; }
.argo-post td { padding: 0.6rem 1rem; border-bottom: 1px solid #eee; vertical-align: top; }
.argo-post tr:nth-child(even) td { background: #fafafa; }
.argo-post .comp-card { border-radius: 12px; padding: 1.2rem 1.4rem; margin-bottom: 16px; border: 1px solid; }
.argo-post .comp-card h3 { margin: 0 0 0.3rem; font-size: 1rem; font-family: monospace; }
.argo-post .comp-card .badge { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 20px; margin-bottom: 8px; }
.argo-post .comp-card p { margin: 0.4rem 0; font-size: 0.93rem; }
.argo-post .comp-card ul { margin: 0.4rem 0; padding-left: 1.3rem; font-size: 0.9rem; }
.argo-post .c-teal  { background: #f0fdf8; border-color: #a3dfc4; }
.argo-post .c-teal h3 { color: #0a6640; }
.argo-post .c-teal .badge { background: #d1f5e5; color: #0a5533; }
.argo-post .c-purple { background: #f3f0ff; border-color: #c4b8f8; }
.argo-post .c-purple h3 { color: #3a2f9e; }
.argo-post .c-purple .badge { background: #e2dbff; color: #2e2580; }
.argo-post .c-coral  { background: #fff5f0; border-color: #f5c4a8; }
.argo-post .c-coral h3 { color: #8a3a10; }
.argo-post .c-coral .badge { background: #ffe8db; color: #7a2f0a; }
.argo-post .c-amber  { background: #fffbf0; border-color: #f5dfa8; }
.argo-post .c-amber h3 { color: #7a5010; }
.argo-post .c-amber .badge { background: #fef3d0; color: #6a420a; }
.argo-post .flow-box { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 10px; padding: 1.3rem 1.5rem; font-family: monospace; font-size: 0.85rem; line-height: 2; margin: 1.3rem 0; color: #24292f; }
.argo-post .flow-box .fg { color: #1a7f4b; font-weight: 600; }
.argo-post .flow-box .fp { color: #5e3fbe; font-weight: 600; }
.argo-post .flow-box .fa { color: #8a6010; font-weight: 600; }
.argo-post .flow-box .fc { color: #993c1d; font-weight: 600; }
@media(max-width:600px){ .argo-post th, .argo-post td { padding: 0.5rem 0.6rem; font-size: 0.85rem; } }
&lt;/style&gt;

&lt;div class=&quot;argo-post&quot;&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  GitOps / ArgoCD 시리즈&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li class=&quot;current&quot;&gt;1편 — ArgoCD Helm 설치 시 생성되는 컴포넌트 역할 정리 (현재 글)&lt;/li&gt;
    &lt;li&gt;2편 — ArgoCD Application, AppProject 개념 정리&lt;/li&gt;
    &lt;li&gt;3편 — ApplicationSet으로 멀티 클러스터 배포 자동화&lt;/li&gt;
    &lt;li&gt;4편 — ArgoCD Notifications Slack 연동 가이드&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;Helm으로 ArgoCD를 설치하면 한 namespace 안에 여러 Deployment와 StatefulSet이 생깁니다. 처음 보면 뭐가 뭔지 헷갈리는데, 각 컴포넌트가 담당하는 역할이 명확히 나뉩니다. 이 글에서는 &lt;code&gt;kubectl get pod -n argocd&lt;/code&gt; 했을 때 보이는 각 파드가 무슨 일을 하는지 정리합니다.&lt;/p&gt;

&lt;div class=&quot;callout&quot;&gt;
  테스트 환경: ArgoCD v2.x, argo-cd Helm chart (argoproj/argo-cd)
&lt;/div&gt;

&lt;h2&gt;설치 후 생성되는 파드 목록&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get pod -n argocd

NAME                                                READY   STATUS
argocd-server-xxxxxxxxx-xxxxx                       1/1     Running   ← Deployment
argocd-repo-server-xxxxxxxxx-xxxxx                  1/1     Running   ← Deployment
argocd-application-controller-0                     1/1     Running   ← StatefulSet
argocd-applicationset-controller-xxxxxxxxx-xxxxx    1/1     Running   ← Deployment
argocd-notifications-controller-xxxxxxxxx-xxxxx     1/1     Running   ← Deployment
argocd-redis-xxxxxxxxx-xxxxx                        1/1     Running   ← Deployment
argocd-dex-server-xxxxxxxxx-xxxxx                   1/1     Running   ← Deployment (SSO 사용 시)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;application-controller&lt;/code&gt;만 StatefulSet이고 나머지는 전부 Deployment입니다. StatefulSet인 이유는 클러스터 상태 캐시를 안정적으로 유지하고, HA 구성 시 샤딩을 위해 고정된 파드 식별자가 필요하기 때문입니다.&lt;/p&gt;

&lt;h2&gt;컴포넌트별 역할 상세&lt;/h2&gt;

&lt;!-- argocd-server --&gt;
&lt;div class=&quot;comp-card c-teal&quot;&gt;
  &lt;h3&gt;argocd-server&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;Deployment&lt;/span&gt;
  &lt;p&gt;&lt;strong&gt;ArgoCD의 API 게이트웨이이자 Web UI 서버&lt;/strong&gt;입니다. 외부에서 들어오는 모든 요청이 여기를 통합니다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;gRPC / REST API 제공 — CLI(&lt;code&gt;argocd&lt;/code&gt;)와 Web UI가 이 서버와 통신&lt;/li&gt;
    &lt;li&gt;RBAC 인증 처리 — 어떤 사용자가 어떤 Application에 접근할 수 있는지 판단&lt;/li&gt;
    &lt;li&gt;Dex와 연동해서 SSO 토큰 검증&lt;/li&gt;
    &lt;li&gt;Webhook 수신 — GitHub/GitLab Push 이벤트를 받아서 즉시 Sync 트리거&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;!-- argocd-repo-server --&gt;
&lt;div class=&quot;comp-card c-teal&quot;&gt;
  &lt;h3&gt;argocd-repo-server&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;Deployment&lt;/span&gt;
  &lt;p&gt;&lt;strong&gt;Git 레포지토리에서 매니페스트를 가져와 최종 YAML로 변환하는 컴포넌트&lt;/strong&gt;입니다. ArgoCD에서 CPU를 가장 많이 쓰는 컴포넌트입니다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Git clone / fetch 수행&lt;/li&gt;
    &lt;li&gt;Helm, Kustomize, Jsonnet, Plain YAML 렌더링 — &lt;code&gt;helm template&lt;/code&gt; 실행이 여기서 일어남&lt;/li&gt;
    &lt;li&gt;렌더링 결과를 Redis에 캐시해서 동일 커밋 반복 렌더링 방지&lt;/li&gt;
    &lt;li&gt;커스텀 플러그인(CMP, Config Management Plugin) 실행도 여기서 처리&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;!-- argocd-application-controller --&gt;
&lt;div class=&quot;comp-card c-purple&quot;&gt;
  &lt;h3&gt;argocd-application-controller&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;StatefulSet&lt;/span&gt;
  &lt;p&gt;&lt;strong&gt;ArgoCD의 핵심 컴포넌트&lt;/strong&gt;입니다. Git의 desired state와 실제 클러스터 상태를 지속적으로 비교하고, 차이가 있으면 Sync(배포)를 실행합니다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;주기적으로 클러스터 상태를 조회해서 Redis에 캐시&lt;/li&gt;
    &lt;li&gt;Repo Server에서 받은 매니페스트와 실제 상태 비교 → OutOfSync / Synced 판단&lt;/li&gt;
    &lt;li&gt;Auto-sync 설정 시 자동으로 &lt;code&gt;kubectl apply&lt;/code&gt; 수행&lt;/li&gt;
    &lt;li&gt;Health 상태 평가 — Deployment rollout, PVC 바인딩 등 리소스별 상태 판단&lt;/li&gt;
    &lt;li&gt;HA 구성 시 &lt;strong&gt;샤딩&lt;/strong&gt;으로 여러 클러스터를 분산 처리 (인스턴스 인덱스 기반)&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;!-- argocd-applicationset-controller --&gt;
&lt;div class=&quot;comp-card c-coral&quot;&gt;
  &lt;h3&gt;argocd-applicationset-controller&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;Deployment&lt;/span&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code&gt;ApplicationSet&lt;/code&gt; 커스텀 리소스를 감시해서 Application을 자동으로 생성·업데이트·삭제&lt;/strong&gt;합니다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;List, Cluster, Git, Matrix 등 다양한 generator로 Application 목록 생성&lt;/li&gt;
    &lt;li&gt;멀티 클러스터 배포 자동화 — 클러스터가 추가되면 Application 자동 생성&lt;/li&gt;
    &lt;li&gt;PR 환경 자동화 — PR 오픈 시 임시 환경 생성, 머지 시 삭제&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;!-- argocd-notifications-controller --&gt;
&lt;div class=&quot;comp-card c-coral&quot;&gt;
  &lt;h3&gt;argocd-notifications-controller&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;Deployment&lt;/span&gt;
  &lt;p&gt;&lt;strong&gt;Application 상태 변화를 감지해서 외부로 알림을 보내는 컴포넌트&lt;/strong&gt;입니다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Sync 성공 / 실패, Health 상태 변화 감지&lt;/li&gt;
    &lt;li&gt;Slack, 이메일, PagerDuty, Teams, Webhook 등 다양한 채널 지원&lt;/li&gt;
    &lt;li&gt;trigger + template 조합으로 유연한 알림 조건 정의 가능&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;!-- redis --&gt;
&lt;div class=&quot;comp-card c-amber&quot;&gt;
  &lt;h3&gt;argocd-redis&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;Deployment&lt;/span&gt;
  &lt;p&gt;&lt;strong&gt;API Server와 Application Controller가 클러스터 상태 캐시를 공유하는 저장소&lt;/strong&gt;입니다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;클러스터 리소스 상태 캐시 — 매번 kube-apiserver를 호출하지 않도록&lt;/li&gt;
    &lt;li&gt;Repo Server 렌더링 결과 캐시&lt;/li&gt;
    &lt;li&gt;재시작하면 데이터가 초기화됨 (캐시 전용, 영구 저장 아님)&lt;/li&gt;
    &lt;li&gt;HA 구성 시 Redis Sentinel 또는 Redis Cluster로 대체 가능&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;!-- dex --&gt;
&lt;div class=&quot;comp-card c-amber&quot;&gt;
  &lt;h3&gt;argocd-dex-server&lt;/h3&gt;
  &lt;span class=&quot;badge&quot;&gt;Deployment (선택)&lt;/span&gt;
  &lt;p&gt;&lt;strong&gt;OIDC 브로커 역할을 하는 SSO 컴포넌트&lt;/strong&gt;입니다. GitHub, Google, LDAP, SAML 등 외부 IdP와 ArgoCD 사이에서 인증을 중계합니다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;외부 IdP로부터 받은 토큰을 ArgoCD가 이해할 수 있는 형태로 변환&lt;/li&gt;
    &lt;li&gt;SSO가 필요 없으면 &lt;code&gt;dex.enabled: false&lt;/code&gt;로 비활성화 가능&lt;/li&gt;
    &lt;li&gt;비활성화 시 ArgoCD 로컬 사용자 또는 직접 OIDC 연동으로 대체&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;h2&gt;한눈에 정리&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;컴포넌트&lt;/th&gt;&lt;th&gt;종류&lt;/th&gt;&lt;th&gt;핵심 역할&lt;/th&gt;&lt;th&gt;리소스 특성&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;argocd-server&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Deployment&lt;/td&gt;&lt;td&gt;API / Web UI / Webhook 수신&lt;/td&gt;&lt;td&gt;외부 트래픽 집중&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;argocd-repo-server&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Deployment&lt;/td&gt;&lt;td&gt;Git clone + 매니페스트 렌더링&lt;/td&gt;&lt;td&gt;CPU 집중&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;argocd-application-controller&lt;/code&gt;&lt;/td&gt;&lt;td&gt;StatefulSet&lt;/td&gt;&lt;td&gt;상태 비교 + Sync 실행&lt;/td&gt;&lt;td&gt;메모리 집중, 핵심&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;argocd-applicationset-controller&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Deployment&lt;/td&gt;&lt;td&gt;ApplicationSet → Application 생성&lt;/td&gt;&lt;td&gt;경량&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;argocd-notifications-controller&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Deployment&lt;/td&gt;&lt;td&gt;상태 변화 감지 + 알림 발송&lt;/td&gt;&lt;td&gt;경량&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;argocd-redis&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Deployment&lt;/td&gt;&lt;td&gt;상태 캐시 공유&lt;/td&gt;&lt;td&gt;메모리, 재시작 시 초기화&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;argocd-dex-server&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Deployment&lt;/td&gt;&lt;td&gt;OIDC / SSO 인증 브로커&lt;/td&gt;&lt;td&gt;선택, SSO 불필요 시 비활성화&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;Git Push부터 배포까지 전체 흐름&lt;/h2&gt;

&lt;div class=&quot;flow-box&quot;&gt;
&lt;span class=&quot;fg&quot;&gt;[Git repo]&lt;/span&gt; ─ push ──────────────────────────────────────────▶ &lt;span class=&quot;fg&quot;&gt;[argocd-server]&lt;/span&gt; (Webhook 수신)
                                                                        │
                                                                        ▼
&lt;span class=&quot;fg&quot;&gt;[argocd-repo-server]&lt;/span&gt; ◀── 매니페스트 렌더링 요청 ────────── &lt;span class=&quot;fp&quot;&gt;[argocd-application-controller]&lt;/span&gt;
        │                                                               │
        │  Helm/Kustomize 실행                                          │  상태 비교
        ▼                                                               │  (desired vs actual)
&lt;span class=&quot;fa&quot;&gt;[argocd-redis]&lt;/span&gt; ◀── 캐시 저장 ──────────────────────────────────────▶ &lt;span class=&quot;fp&quot;&gt;[argocd-application-controller]&lt;/span&gt;
                                                                        │
                                                                        │  kubectl apply
                                                                        ▼
                                                              &lt;span class=&quot;fp&quot;&gt;[Kubernetes cluster]&lt;/span&gt;
                                                                        │
                                                                        │  Sync 결과
                                                                        ▼
                                                             &lt;span class=&quot;fc&quot;&gt;[argocd-notifications-controller]&lt;/span&gt; ──▶ Slack
&lt;/div&gt;

&lt;h2&gt;리소스 설정 예시&lt;/h2&gt;

&lt;p&gt;처음 설치할 때 최소한으로 잡아야 할 리소스 설정입니다. &lt;code&gt;repo-server&lt;/code&gt;는 Helm 렌더링이 많을수록 CPU를 많이 쓰고, &lt;code&gt;application-controller&lt;/code&gt;는 관리하는 Application/클러스터 수에 비례해서 메모리가 늘어납니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# values.yaml

server:
  resources:
    requests:
      cpu: &quot;100m&quot;
      memory: &quot;128Mi&quot;
    limits:
      cpu: &quot;500m&quot;
      memory: &quot;256Mi&quot;

repoServer:
  resources:
    requests:
      cpu: &quot;200m&quot;
      memory: &quot;256Mi&quot;
    limits:
      cpu: &quot;1000m&quot;      # Helm 렌더링 시 CPU 급증
      memory: &quot;512Mi&quot;

controller:
  resources:
    requests:
      cpu: &quot;250m&quot;
      memory: &quot;512Mi&quot;
    limits:
      cpu: &quot;1000m&quot;
      memory: &quot;2Gi&quot;     # Application 수에 비례해서 증가

redis:
  resources:
    requests:
      cpu: &quot;100m&quot;
      memory: &quot;64Mi&quot;
    limits:
      cpu: &quot;200m&quot;
      memory: &quot;128Mi&quot;

applicationSet:
  resources:
    requests:
      cpu: &quot;100m&quot;
      memory: &quot;128Mi&quot;
    limits:
      cpu: &quot;200m&quot;
      memory: &quot;256Mi&quot;

notifications:
  resources:
    requests:
      cpu: &quot;100m&quot;
      memory: &quot;64Mi&quot;
    limits:
      cpu: &quot;200m&quot;
      memory: &quot;128Mi&quot;

# SSO 불필요 시 Dex 비활성화
dex:
  enabled: false&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;자주 하는 실수&lt;/h2&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;repo-server CPU limit을 너무 낮게 잡는 경우&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  Helm 차트가 크거나 Application이 많으면 &lt;code&gt;repo-server&lt;/code&gt;에서 동시에 여러 렌더링이 일어납니다. CPU limit이 너무 낮으면 Sync가 느려지거나 타임아웃이 발생합니다. 처음에는 넉넉하게 잡고 실제 사용량을 보면서 줄이세요.
&lt;/div&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;application-controller 메모리 부족&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  관리하는 클러스터와 Application이 늘어날수록 &lt;code&gt;application-controller&lt;/code&gt;의 메모리 사용량이 선형으로 증가합니다. OOMKilled가 반복되면 limit을 올리거나 HA 샤딩 구성을 검토하세요.
&lt;/div&gt;

&lt;div class=&quot;callout tip&quot;&gt;
  &lt;strong&gt;Webhook 설정으로 Sync 속도 개선&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  기본 설정에서 ArgoCD는 3분마다 Git을 polling합니다. GitHub/GitLab Webhook을 &lt;code&gt;argocd-server&lt;/code&gt;에 연결하면 Push 즉시 Sync가 트리거되어 배포 지연이 없어집니다.
&lt;/div&gt;

&lt;h2&gt;마치며&lt;/h2&gt;

&lt;p&gt;ArgoCD는 컴포넌트가 많아 보이지만, 역할을 나눠 보면 구조가 명확합니다. 문제가 생겼을 때 어느 파드 로그를 봐야 할지 파악하는 것만으로도 트러블슈팅 시간이 크게 줄어듭니다. Sync가 안 되면 &lt;code&gt;application-controller&lt;/code&gt;, 렌더링 에러면 &lt;code&gt;repo-server&lt;/code&gt;, 로그인 문제면 &lt;code&gt;dex&lt;/code&gt;나 &lt;code&gt;argocd-server&lt;/code&gt;를 먼저 확인하세요.&lt;/p&gt;

&lt;div class=&quot;series-box&quot;&gt;
  &lt;strong&gt;  다음 편&lt;/strong&gt;
  &lt;ul&gt;
    &lt;li&gt;2편 — ArgoCD Application, AppProject 개념 정리&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;/div&gt;</description>
      <category>Kubernetes</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/97</guid>
      <comments>https://binarynum.tistory.com/97#entry97comment</comments>
      <pubDate>Sat, 16 May 2026 16:21:08 +0900</pubDate>
    </item>
    <item>
      <title>Loki write / read / backend 역할 정리</title>
      <link>https://binarynum.tistory.com/96</link>
      <description>&lt;!-- 티스토리 HTML 에디터에 그대로 붙여넣기 --&gt;
&lt;style&gt;
.loki-post { font-family: 'Noto Sans KR', sans-serif; color: #1a1a1a; line-height: 1.8; max-width: 780px; margin: 0 auto; }
.loki-post h2 { font-size: 1.45rem; font-weight: 700; margin: 2.4rem 0 0.8rem; padding-bottom: 0.4rem; border-bottom: 2px solid #e8e8e8; color: #111; }
.loki-post h3 { font-size: 1.15rem; font-weight: 700; margin: 1.8rem 0 0.5rem; color: #222; }
.loki-post p { margin: 0.7rem 0 1rem; }
.loki-post code { background: #f3f4f6; border-radius: 4px; padding: 2px 6px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.88em; color: #d63031; }
.loki-post pre { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 10px; padding: 1.2rem 1.4rem; overflow-x: auto; margin: 1.2rem 0; }
.loki-post pre code { background: none; color: #24292f; font-size: 0.85rem; padding: 0; line-height: 1.7; }
.loki-post .callout { border-left: 4px solid #6c63ff; background: #f5f3ff; border-radius: 0 8px 8px 0; padding: 0.9rem 1.2rem; margin: 1.2rem 0; font-size: 0.95rem; color: #3d3565; }
.loki-post .callout.tip { border-color: #00b894; background: #f0fdf8; color: #1a5c45; }
.loki-post .callout.warn { border-color: #e17055; background: #fff5f3; color: #7a2a1a; }
.loki-post .series-box { background: #f8f9ff; border: 1px solid #dde1ff; border-radius: 10px; padding: 1rem 1.3rem; margin: 1.5rem 0; font-size: 0.92rem; }
.loki-post .series-box strong { display: block; margin-bottom: 0.4rem; color: #4a4aaa; }
.loki-post .series-box ul { margin: 0; padding-left: 1.3rem; }
.loki-post .series-box li { margin: 0.25rem 0; color: #555; }
.loki-post .series-box li.current { font-weight: 700; color: #333; }
.loki-post table { width: 100%; border-collapse: collapse; margin: 1.3rem 0; font-size: 0.92rem; }
.loki-post th { background: #2d2d3f; color: #fff; padding: 0.65rem 1rem; text-align: left; font-weight: 600; }
.loki-post td { padding: 0.6rem 1rem; border-bottom: 1px solid #eee; vertical-align: top; }
.loki-post tr:nth-child(even) td { background: #fafafa; }
.loki-post .role-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin: 1.5rem 0; }
.loki-post .role-card { border-radius: 12px; padding: 1.2rem; }
.loki-post .role-card.write { background: #e8faf3; border: 1px solid #a3dfc4; }
.loki-post .role-card.read  { background: #eeecff; border: 1px solid #c0b8f8; }
.loki-post .role-card.back  { background: #fff3ee; border: 1px solid #f5c5a8; }
.loki-post .role-card h4 { font-size: 1.05rem; font-weight: 700; margin: 0 0 0.4rem; font-family: monospace; }
.loki-post .role-card.write h4 { color: #0a6640; }
.loki-post .role-card.read  h4 { color: #3a2f9e; }
.loki-post .role-card.back  h4 { color: #8a3a10; }
.loki-post .role-card p { font-size: 0.88rem; color: #444; margin: 0 0 0.5rem; }
.loki-post .role-card .comp { font-size: 0.8rem; background: rgba(0,0,0,0.06); border-radius: 4px; padding: 2px 7px; display: inline-block; margin: 2px 2px 0 0; font-family: monospace; }
.loki-post .flow-box { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 10px; padding: 1.3rem 1.5rem; font-family: monospace; font-size: 0.85rem; line-height: 2; margin: 1.3rem 0; color: #24292f; }
.loki-post .flow-box .w { color: #1a7f4b; font-weight: 600; }
.loki-post .flow-box .r { color: #5e3fbe; font-weight: 600; }
.loki-post .flow-box .b { color: #c25a0a; font-weight: 600; }
.loki-post .flow-box .s { color: #0a6cb5; font-weight: 600; }
@media(max-width:600px){ .loki-post .role-grid { grid-template-columns: 1fr; } }
&lt;/style&gt;

&lt;div class=&quot;loki-post&quot;&gt;


&lt;p&gt;Helm으로 Loki를 설치하면 &lt;code&gt;loki-write&lt;/code&gt;, &lt;code&gt;loki-backend&lt;/code&gt; StatefulSet과 &lt;code&gt;loki-read&lt;/code&gt; Deployment가 생깁니다. 처음 보면 &quot;왜 파드가 이렇게 많지?&quot; 싶은데, 이건 Loki의 &lt;strong&gt;Simple Scalable&lt;/strong&gt; 배포 모드 때문입니다.&lt;/p&gt;

&lt;p&gt;이 글에서는 세 가지 배포 모드가 무엇인지, 그리고 각 StatefulSet이 내부적으로 어떤 컴포넌트를 묶어놓은 건지 정리합니다.&lt;/p&gt;

&lt;div class=&quot;callout&quot;&gt;
  테스트 환경: Loki v3.x, loki-stack Helm chart
&lt;/div&gt;

&lt;h2&gt;Loki의 3가지 배포 모드&lt;/h2&gt;

&lt;p&gt;Loki는 하나의 바이너리로 배포 모드를 &lt;code&gt;target&lt;/code&gt; 파라미터로 제어합니다.&lt;/p&gt;

&lt;h3&gt;1. Monolithic (단일 모드)&lt;/h3&gt;

&lt;p&gt;모든 컴포넌트가 하나의 파드에서 실행됩니다. 개발 환경이나 소규모 로그 수집에 적합하지만 수평 확장이 안 됩니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;deploymentMode: SingleBinary

loki:
  commonConfig:
    replication_factor: 1
  storage:
    type: filesystem&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;2. Simple Scalable (권장 모드) — Helm 기본값&lt;/h3&gt;

&lt;p&gt;역할을 &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt; 세 그룹으로 나눠서 배포합니다. 각 그룹을 독립적으로 스케일링할 수 있어서 운영 환경에 적합합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;deploymentMode: SimpleScalable&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;3. Microservices (완전 분산 모드)&lt;/h3&gt;

&lt;p&gt;모든 컴포넌트를 개별 Deployment로 분리합니다. 세밀한 리소스 제어가 가능하지만 관리 복잡도가 높아서 대규모 환경에서만 사용합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;deploymentMode: Distributed&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Simple Scalable의 세 컴포넌트 그룹&lt;/h2&gt;

&lt;p&gt;Helm 설치 후 확인하면 이렇게 나타납니다. &lt;code&gt;read&lt;/code&gt;는 Stateless라서 Deployment, &lt;code&gt;write&lt;/code&gt;와 &lt;code&gt;backend&lt;/code&gt;는 디스크 상태(WAL, 인덱스)가 필요해서 StatefulSet입니다.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get statefulset -n monitoring

NAME             READY   AGE
loki-backend     1/1     5m
loki-write       3/3     5m

$ kubectl get deployment -n monitoring

NAME             READY   AGE
loki-read        2/2     5m&lt;/code&gt;&lt;/pre&gt;

&lt;div class=&quot;role-grid&quot;&gt;
  &lt;div class=&quot;role-card write&quot;&gt;
    &lt;h4&gt;write&lt;/h4&gt;
    &lt;p&gt;로그 수집 입구. 받아서 버퍼링하고 저장.&lt;/p&gt;
    &lt;span class=&quot;comp&quot;&gt;Distributor&lt;/span&gt;
    &lt;span class=&quot;comp&quot;&gt;Ingester&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;role-card read&quot;&gt;
    &lt;h4&gt;read&lt;/h4&gt;
    &lt;p&gt;Grafana 쿼리 처리. 분할하고 병렬 조회.&lt;/p&gt;
    &lt;span class=&quot;comp&quot;&gt;Query Frontend&lt;/span&gt;
    &lt;span class=&quot;comp&quot;&gt;Querier&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;role-card back&quot;&gt;
    &lt;h4&gt;backend&lt;/h4&gt;
    &lt;p&gt;백그라운드 작업. 압축, 알림, 인덱스 캐시.&lt;/p&gt;
    &lt;span class=&quot;comp&quot;&gt;Compactor&lt;/span&gt;
    &lt;span class=&quot;comp&quot;&gt;Ruler&lt;/span&gt;
    &lt;span class=&quot;comp&quot;&gt;Index Gateway&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;write — 로그를 받아서 저장&lt;/h2&gt;

&lt;div class=&quot;flow-box&quot;&gt;
&lt;span class=&quot;w&quot;&gt;[Promtail / Alloy]&lt;/span&gt; → &lt;span class=&quot;w&quot;&gt;[write: Distributor]&lt;/span&gt; → &lt;span class=&quot;w&quot;&gt;[write: Ingester]&lt;/span&gt; → &lt;span class=&quot;s&quot;&gt;[Object Storage]&lt;/span&gt;
&lt;/div&gt;

&lt;h3&gt;Distributor&lt;/h3&gt;

&lt;p&gt;Promtail이 로그를 보내면 가장 먼저 만나는 컴포넌트입니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;들어오는 로그의 &lt;strong&gt;라벨 유효성 검사&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;스트림 해시 기반으로 &lt;strong&gt;어느 Ingester로 보낼지 결정&lt;/strong&gt; (일관된 해시링)&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;ingestion_rate_limit&lt;/code&gt; 초과 시 429 응답 반환&lt;/li&gt;
  &lt;li&gt;자체 상태 없음 — stateless하게 동작&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Ingester&lt;/h3&gt;

&lt;p&gt;Distributor에게 받은 로그를 메모리에 청크로 버퍼링했다가 Object Storage로 플러시합니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;로그 스트림을 메모리 청크에 압축 저장&lt;/li&gt;
  &lt;li&gt;일정 조건(시간 또는 크기)이 되면 S3 등으로 플러시&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;WAL(Write-Ahead Log)&lt;/strong&gt;을 디스크에 기록해서 파드 재시작 시 복구 가능&lt;/li&gt;
  &lt;li&gt;Stateful하기 때문에 StatefulSet으로 배포됨&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code&gt;# Helm values.yaml — write 스케일링
write:
  replicas: 3
  resources:
    requests:
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;
    limits:
      cpu: &quot;1000m&quot;
      memory: &quot;1Gi&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;div class=&quot;callout tip&quot;&gt;
  &lt;strong&gt;언제 늘리나:&lt;/strong&gt; 로그 수집 지연이 발생하거나 Distributor에서 rate limit 에러가 많이 나면 &lt;code&gt;write&lt;/code&gt; 레플리카를 늘립니다.
&lt;/div&gt;

&lt;h2&gt;read — 쿼리를 받아서 조회&lt;/h2&gt;

&lt;div class=&quot;flow-box&quot;&gt;
&lt;span class=&quot;r&quot;&gt;[Grafana]&lt;/span&gt; → &lt;span class=&quot;r&quot;&gt;[read: Query Frontend]&lt;/span&gt; → &lt;span class=&quot;r&quot;&gt;[read: Querier]&lt;/span&gt; → &lt;span class=&quot;w&quot;&gt;[Ingester]&lt;/span&gt; + &lt;span class=&quot;s&quot;&gt;[Object Storage]&lt;/span&gt;
&lt;/div&gt;

&lt;h3&gt;Query Frontend&lt;/h3&gt;

&lt;p&gt;Grafana가 LogQL 쿼리를 보내면 가장 먼저 받는 컴포넌트입니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;쿼리를 &lt;strong&gt;시간 단위로 잘게 분할&lt;/strong&gt;해서 여러 Querier에게 병렬 처리 위임&lt;/li&gt;
  &lt;li&gt;결과를 &lt;strong&gt;캐시&lt;/strong&gt;해서 동일 쿼리 반복 시 빠르게 반환 (memcached 연동 가능)&lt;/li&gt;
  &lt;li&gt;쿼리 우선순위 큐 관리&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Querier&lt;/h3&gt;

&lt;p&gt;실제로 데이터를 가져오는 컴포넌트입니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Ingester와 Object Storage 양쪽에서 동시에 조회&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Ingester: 아직 플러시되지 않은 최신 로그 (수 분 이내)&lt;/li&gt;
      &lt;li&gt;Object Storage: 플러시된 과거 로그&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;두 결과를 합쳐서 중복 제거 후 반환&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code&gt;# Helm values.yaml — read 스케일링
read:
  replicas: 2
  resources:
    requests:
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;
    limits:
      cpu: &quot;2000m&quot;
      memory: &quot;2Gi&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;div class=&quot;callout tip&quot;&gt;
  &lt;strong&gt;언제 늘리나:&lt;/strong&gt; Grafana에서 로그 조회가 느리거나 타임아웃이 발생하면 &lt;code&gt;read&lt;/code&gt; 레플리카를 늘립니다.
&lt;/div&gt;

&lt;h2&gt;backend — 백그라운드 작업&lt;/h2&gt;

&lt;div class=&quot;flow-box&quot;&gt;
&lt;span class=&quot;b&quot;&gt;[backend: Compactor]&lt;/span&gt; → 인덱스 압축 + 오래된 로그 삭제
&lt;span class=&quot;b&quot;&gt;[backend: Ruler]&lt;/span&gt;     → LogQL 알림 규칙 평가 → AlertManager
&lt;span class=&quot;b&quot;&gt;[backend: Index Gateway]&lt;/span&gt; → Querier 인덱스 캐시 중계
&lt;/div&gt;

&lt;h3&gt;Compactor&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;작은 인덱스 파일들을 &lt;strong&gt;하나로 압축&lt;/strong&gt;해서 조회 성능 향상&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;retention_period&lt;/code&gt; 설정에 따라 &lt;strong&gt;오래된 로그 삭제&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;한 번에 하나만 실행 — 레플리카는 1개가 원칙&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Ruler&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;LogQL 표현식으로 정의한 &lt;strong&gt;알림 규칙을 주기적으로 평가&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;조건 충족 시 AlertManager로 알림 전송&lt;/li&gt;
  &lt;li&gt;Prometheus Ruler와 동일한 개념&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Index Gateway&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Querier가 매번 Object Storage에서 인덱스를 읽는 대신 &lt;strong&gt;캐시된 인덱스 제공&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;대규모 환경에서 Object Storage 요청 수 감소 → 비용 절감&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code&gt;# Helm values.yaml — backend 설정
backend:
  replicas: 1  # 대부분 1개로 충분
  resources:
    requests:
      cpu: &quot;100m&quot;
      memory: &quot;256Mi&quot;
    limits:
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;한눈에 정리&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;StatefulSet&lt;/th&gt;&lt;th&gt;내부 컴포넌트&lt;/th&gt;&lt;th&gt;역할&lt;/th&gt;&lt;th&gt;스케일링 기준&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;write&lt;/code&gt; (StatefulSet)&lt;/td&gt;&lt;td&gt;Distributor, Ingester&lt;/td&gt;&lt;td&gt;로그 수집 + 버퍼링 + 저장&lt;/td&gt;&lt;td&gt;수집 지연 / rate limit 에러&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;read&lt;/code&gt; (Deployment)&lt;/td&gt;&lt;td&gt;Query Frontend, Querier&lt;/td&gt;&lt;td&gt;쿼리 처리 + 데이터 조회&lt;/td&gt;&lt;td&gt;조회 느림 / 타임아웃&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;code&gt;backend&lt;/code&gt; (StatefulSet)&lt;/td&gt;&lt;td&gt;Compactor, Ruler, Index Gateway&lt;/td&gt;&lt;td&gt;압축 / 알림 / 인덱스 캐시&lt;/td&gt;&lt;td&gt;거의 건드릴 일 없음&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;자주 하는 실수&lt;/h2&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;replication_factor를 1로 설정하고 write를 3개 띄우는 경우&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  &lt;code&gt;replication_factor: 1&lt;/code&gt;이면 하나의 Ingester에만 저장되므로, 그 파드가 재시작될 때 미플러시 로그가 유실될 수 있습니다. write 레플리카 수와 replication_factor를 맞춰야 합니다.
&lt;/div&gt;

&lt;pre&gt;&lt;code&gt;loki:
  commonConfig:
    replication_factor: 3  # write replicas와 동일하게
write:
  replicas: 3&lt;/code&gt;&lt;/pre&gt;

&lt;div class=&quot;callout warn&quot;&gt;
  &lt;strong&gt;Object Storage 없이 SimpleScalable 모드를 쓰는 경우&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  Simple Scalable 모드는 반드시 외부 Object Storage가 필요합니다. &lt;code&gt;filesystem&lt;/code&gt; 스토리지는 Monolithic 모드에서만 동작합니다. 로컬 환경에서 테스트하려면 MinIO를 함께 띄우세요.
&lt;/div&gt;

&lt;h2&gt;실무 설정 예시&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;# custom-values.yaml

deploymentMode: SimpleScalable

loki:
  storage:
    type: s3
    s3:
      region: ap-northeast-2
      bucketnames: my-loki-chunks
  commonConfig:
    replication_factor: 3

write:
  replicas: 3
  resources:
    requests:
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;
    limits:
      cpu: &quot;1&quot;
      memory: &quot;1Gi&quot;

read:
  replicas: 2
  resources:
    requests:
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;
    limits:
      cpu: &quot;2&quot;
      memory: &quot;2Gi&quot;

backend:
  replicas: 1
  resources:
    requests:
      cpu: &quot;100m&quot;
      memory: &quot;256Mi&quot;
    limits:
      cpu: &quot;500m&quot;
      memory: &quot;512Mi&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;마치며&lt;/h2&gt;

&lt;p&gt;Loki의 Simple Scalable 모드는 쓰기/읽기 부하를 독립적으로 조절할 수 있어서 운영 환경에서 효율적입니다. 실제로 로그 수집량이 많아지면 &lt;code&gt;write&lt;/code&gt;만 늘리고, Grafana 조회가 느려지면 &lt;code&gt;read&lt;/code&gt;만 늘리면 되기 때문에 리소스 낭비가 없습니다.&lt;/p&gt;



&lt;/div&gt;</description>
      <category>Kubernetes</category>
      <category>lgtm</category>
      <category>Loki</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/96</guid>
      <comments>https://binarynum.tistory.com/96#entry96comment</comments>
      <pubDate>Sat, 16 May 2026 16:10:14 +0900</pubDate>
    </item>
    <item>
      <title>1주차 스터디 실습 정리</title>
      <link>https://binarynum.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;a href=&quot;https://www.linkedin.com/in/gasida99/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;서종호(가시다)&lt;/a&gt;님의 AEWS 4기 스터디 내용을 기반으로 참고하여 학습 목적으로 작성하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1주차) EKS Cluster Endpoint Access 소개&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Mac 로컬 환경 세팅 (awscli, k8s tools, teraform)&lt;/h4&gt;
&lt;pre id=&quot;code_1773835167887&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Install aws cli
brew install awscli
aws --version

# iam (주체) 자격 증명 설정
aws configure
AWS Access Key ID : &amp;lt;액세스 키 입력&amp;gt;
AWS Secret Access Key : &amp;lt;시크릿 키 입력&amp;gt;
Default region name : ap-northeast-2

# 확인
aws sts get-caller-identity

# 기본 Key Pair 생성 (pem 파일 생성)
aws ec2 create-key-pair \
  --key-name my-keypair \
  --query 'KeyMaterial' \
  --output text &amp;gt; my-keypair.pem

# 권한 설정
chmod 400 my-keypair.pem

# 확인
aws ec2 describe-key-pairs --key-names my-keypair

# Install kubectl
brew install kubernetes-cli
kubectl version --client=true

# Install krew
brew install krew

# Install k9s
brew install k9s

# Install kube-ps1
brew install kube-ps1

# Install kubectx
brew install kubectx


# kubectl 출력 시 하이라이트 처리
brew install kubecolor
echo &quot;alias k=kubectl&quot; &amp;gt;&amp;gt; ~/.zshrc
echo &quot;alias kubectl=kubecolor&quot; &amp;gt;&amp;gt; ~/.zshrc
echo &quot;compdef kubecolor=kubectl&quot; &amp;gt;&amp;gt; ~/.zshrc

# k8s krew path : ~/.zshrc 아래 추가
export PATH=&quot;${KREW_ROOT:-$HOME/.krew}/bin:$PATH&quot;

# Install Helm
brew install helm
helm version

# tfenv 설치
brew install tfenv

# 설치 가능 버전 리스트 확인
tfenv list-remote

# 테라폼 특정 버전 설치
tfenv install 1.14.6

# 테라폼 특정 버전 사용 설정 
tfenv use 1.14.6

# tfenv로 설치한 버전 확인
tfenv list

# 테라폼 버전 정보 확인
terraform version

# 자동완성
terraform -install-autocomplete

## 참고 .zshrc 에 아래 추가됨
cat ~/.zshrc
autoload -U +X bashcompinit &amp;amp;&amp;amp; bashcompinit
complete -o nospace -C /usr/local/bin/terraform terraform&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;EKS Public Endpoint Cluster 실습&lt;/h4&gt;
&lt;pre id=&quot;code_1773920973604&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 코드 다운로드
git clone https://github.com/gasida/aews.git
cd aews
tree aews

# 작업 디렉터리 이동
cd 1w

# 변수 지정
aws ec2 describe-key-pairs --query &quot;KeyPairs[].KeyName&quot; --output text
export TF_VAR_KeyName=$(aws ec2 describe-key-pairs --query &quot;KeyPairs[].KeyName&quot; --output text)
export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32
echo $TF_VAR_KeyName $TF_VAR_ssh_access_cidr

# 배포 : 12분 정도 소요
terraform init
terraform plan
nohup sh -c &quot;terraform apply -auto-approve&quot; &amp;gt; create.log 2&amp;gt;&amp;amp;1 &amp;amp;
tail -f create.log


# 자격증명 설정
aws eks update-kubeconfig --region ap-northeast-2 --name myeks

# k8s config 확인 및 rename context
cat ~/.kube/config
cat ~/.kube/config | grep current-context | awk '{print $2}'
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') myeks
cat ~/.kube/config | grep current-context&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;EKS Public&amp;amp;Private Endpoint Cluster 실습&lt;/h4&gt;
&lt;pre id=&quot;code_1773920978072&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 코드수정
#  endpoint_public_access = true
#  endpoint_private_access = true
#  endpoint_public_access_cidrs = [
#    var.ssh_access_cidr
#  ]
terraform apply -auto-approve&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;EKS Fully Private Cluster 실습&lt;/h4&gt;
&lt;pre id=&quot;code_1773920984838&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#EKS Fully Private Cluster 배포
# 실습 디렉터리 진입
cd eks-private

# 변수 지정
aws ec2 describe-key-pairs --query &quot;KeyPairs[].KeyName&quot; --output text
export TF_VAR_KeyName=$(aws ec2 describe-key-pairs --query &quot;KeyPairs[].KeyName&quot; --output text)
export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32
echo $TF_VAR_KeyName $TF_VAR_ssh_access_cidr

# 초기화
terraform init
terraform plan

# vpc 배포 : 2분 소요 -&amp;gt; 이후 aws vpc 정보 확인!
terraform apply -target=&quot;module.vpc&quot; -auto-approve

#bastion EC2 접속 후 확인
# bastion ec2 접속
ssh -o StrictHostKeyChecking=no ubuntu@$(terraform output -raw bastion_ec2-public_ip)

# admin IAM User 자격 증명 설정
aws configure

# 자격증명 설정
aws eks --region ap-northeast-2 update-kubeconfig --name eks-private
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') eks-private

# eks access endpoint 확인
APIDNS=$(aws eks describe-cluster --name eks-private | jq -r .cluster.endpoint | cut -d '/' -f 3)
echo $APIDNS
4924E16AD07400AA0CA66718E179E887.gr7.ap-northeast-2.eks.amazonaws.com

dig +short $APIDNS
10.0.14.147
10.0.28.171

# kubectl 조회 시도 : DNS Lookup resolved&quot; host=&quot;4924E16AD07400AA0CA66718E179E887.gr7.ap-northeast-2.eks.amazonaws.com&quot; address=[{&quot;IP&quot;:&quot;10.0.28.171&quot;,&quot;Zone&quot;:&quot;&quot;},{&quot;IP&quot;:&quot;10.0.14.147&quot;,&quot;Zone&quot;:&quot;&quot;}]
kubectl cluster-info
kubectl get node -v=9
...
	curl -v -XGET  -H &quot;Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json&quot; -H &quot;User-Agent: kubectl/v1.34.2 (linux/amd64) kubernetes/0ea4984&quot; 'https://4924E16AD07400AA0CA66718E179E887.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500'
 &amp;gt;
I0312 18:36:06.092396    2708 round_trippers.go:547] &quot;HTTP Trace: DNS Lookup resolved&quot; host=&quot;4924E16AD07400AA0CA66718E179E887.gr7.ap-northeast-2.eks.amazonaws.com&quot; address=[{&quot;IP&quot;:&quot;10.0.28.171&quot;,&quot;Zone&quot;:&quot;&quot;},{&quot;IP&quot;:&quot;10.0.14.147&quot;,&quot;Zone&quot;:&quot;&quot;}]

# eks 등 배포 : 14분 소요
terraform apply -auto-approve

# 자격증명 설정
aws eks --region ap-northeast-2 update-kubeconfig --name eks-private
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') eks-private

# kubectl 조회 시도
kubectl get node -v=7
...
I0312 18:01:51.102015   28018 round_trippers.go:527] &quot;Request&quot; verb=&quot;GET&quot; url=&quot;https://4924E16AD07400AA0CA66718E179E887.gr7.ap-northeast-2.eks.amazonaws.com/api?timeout=32s&quot; headers=&amp;lt;
        Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json
        User-Agent: kubectl/v1.35.2 (darwin/arm64) kubernetes/fdc9d74
 E0312 18:02:21.493542   28018 memcache.go:265] &quot;Unhandled Error&quot; err=&quot;couldn't get current server API group list: Get \&quot;https://4924E16AD07400AA0CA66718E179E887.gr7.ap-northeast-2.eks.amazonaws.com/api?timeout=32s\&quot;: dial tcp 10.0.14.147:443: i/o timeout&quot;

# 자격증명 삭제
rm -rf ~/.kube/config&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결과화면&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[VPC]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nbWF4/dJMcagrgp6C/v3wPt0d76vk1WQ2pGwhB31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nbWF4/dJMcagrgp6C/v3wPt0d76vk1WQ2pGwhB31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nbWF4/dJMcagrgp6C/v3wPt0d76vk1WQ2pGwhB31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnbWF4%2FdJMcagrgp6C%2Fv3wPt0d76vk1WQ2pGwhB31%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;1192&quot; height=&quot;139&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;139&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;[EKS]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elhOgs/dJMcabcpi7K/1k7cPoRgFjG8kQPPw7BS80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elhOgs/dJMcabcpi7K/1k7cPoRgFjG8kQPPw7BS80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elhOgs/dJMcabcpi7K/1k7cPoRgFjG8kQPPw7BS80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FelhOgs%2FdJMcabcpi7K%2F1k7cPoRgFjG8kQPPw7BS80%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;1152&quot; height=&quot;324&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Public Endpoint]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DjRiC/dJMcacoTjn0/KI57O1Qnq0OO2gkNlh7e6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DjRiC/dJMcacoTjn0/KI57O1Qnq0OO2gkNlh7e6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DjRiC/dJMcacoTjn0/KI57O1Qnq0OO2gkNlh7e6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDjRiC%2FdJMcacoTjn0%2FKI57O1Qnq0OO2gkNlh7e6k%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;250&quot; height=&quot;146&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ye6qr/dJMcabpZrbT/gQKvAdnHRdxHxWoOJ5cmS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ye6qr/dJMcabpZrbT/gQKvAdnHRdxHxWoOJ5cmS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ye6qr/dJMcabpZrbT/gQKvAdnHRdxHxWoOJ5cmS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fye6qr%2FdJMcabpZrbT%2FgQKvAdnHRdxHxWoOJ5cmS0%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;567&quot; height=&quot;72&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTcHZq/dJMcai3DrVG/8z10Wk1P6EXNFIzOZIymy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTcHZq/dJMcai3DrVG/8z10Wk1P6EXNFIzOZIymy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTcHZq/dJMcai3DrVG/8z10Wk1P6EXNFIzOZIymy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTcHZq%2FdJMcai3DrVG%2F8z10Wk1P6EXNFIzOZIymy0%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;524&quot; height=&quot;109&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Public &amp;amp; Private Endpoint]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBk87L/dJMcaa5Gi2C/zHRH66LxB041KVkdVyS6b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBk87L/dJMcaa5Gi2C/zHRH66LxB041KVkdVyS6b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBk87L/dJMcaa5Gi2C/zHRH66LxB041KVkdVyS6b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBk87L%2FdJMcaa5Gi2C%2FzHRH66LxB041KVkdVyS6b1%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;1583&quot; height=&quot;172&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9HFT/dJMcaiJmMtD/K7Hxk2uMXcqLzp8Rt4XgOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9HFT/dJMcaiJmMtD/K7Hxk2uMXcqLzp8Rt4XgOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9HFT/dJMcaiJmMtD/K7Hxk2uMXcqLzp8Rt4XgOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9HFT%2FdJMcaiJmMtD%2FK7Hxk2uMXcqLzp8Rt4XgOK%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;419&quot; height=&quot;195&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;71&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v9pmT/dJMcagY6V33/2LLrZdir3dCZ4FzmAD9uAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v9pmT/dJMcagY6V33/2LLrZdir3dCZ4FzmAD9uAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v9pmT/dJMcagY6V33/2LLrZdir3dCZ4FzmAD9uAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv9pmT%2FdJMcagY6V33%2F2LLrZdir3dCZ4FzmAD9uAK%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;294&quot; height=&quot;71&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;71&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Private Endpoint]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA50WW/dJMcadacAGS/VolLK3RqvaIm8fGgNjkPiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA50WW/dJMcadacAGS/VolLK3RqvaIm8fGgNjkPiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA50WW/dJMcadacAGS/VolLK3RqvaIm8fGgNjkPiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA50WW%2FdJMcadacAGS%2FVolLK3RqvaIm8fGgNjkPiK%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;566&quot; height=&quot;310&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;후기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에 도움이 되는 도구 - 사람 친화적인 설정&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 130px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;k9s&lt;/td&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;GUI처럼 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;kubectx/kubens&lt;/td&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;빠른 환경 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;kube-ps1&lt;/td&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;사고 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;kubecolor&lt;/td&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;가독성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;alias&lt;/td&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;krew&lt;/td&gt;
&lt;td style=&quot;height: 20px; text-align: center;&quot;&gt;확장성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;새로 알게된 내용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENI owned ENI 확인 - 소유주와 인스턴스 소유주가 다른 것 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL8GKg/dJMcacvFk9W/JRRyeKVMfVjzP2cLPEAySk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL8GKg/dJMcacvFk9W/JRRyeKVMfVjzP2cLPEAySk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL8GKg/dJMcacvFk9W/JRRyeKVMfVjzP2cLPEAySk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL8GKg%2FdJMcacvFk9W%2FJRRyeKVMfVjzP2cLPEAySk%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;869&quot; height=&quot;684&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;684&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;</description>
      <category>Kubernetes/AEWS 4기</category>
      <category>AEWS</category>
      <category>EKS</category>
      <category>study</category>
      <author>BinaryNumber</author>
      <guid isPermaLink="true">https://binarynum.tistory.com/95</guid>
      <comments>https://binarynum.tistory.com/95#entry95comment</comments>
      <pubDate>Wed, 18 Mar 2026 21:14:53 +0900</pubDate>
    </item>
  </channel>
</rss>