<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>/home/temp/log</title>
        <link>https://velog.io/</link>
        <description>멍총이 엔지니어</description>
        <lastBuildDate>Tue, 24 Jun 2025 05:48:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>/home/temp/log</title>
            <url>https://velog.velcdn.com/images/useradd_temp/profile/1efb2e98-43eb-472c-a55a-5af330a75d19/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. /home/temp/log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/useradd_temp" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[모니터링, 이제 알람 뿐만 아니라 분석도 자동화 된]]></title>
            <link>https://velog.io/@useradd_temp/%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%9D%B4%EC%A0%9C-%EC%95%8C%EB%9E%8C-%EB%BF%90%EB%A7%8C-%EC%95%84%EB%8B%88%EB%9D%BC-%EB%B6%84%EC%84%9D%EB%8F%84-%EC%9E%90%EB%8F%99%ED%99%94-%EB%90%9C</link>
            <guid>https://velog.io/@useradd_temp/%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%9D%B4%EC%A0%9C-%EC%95%8C%EB%9E%8C-%EB%BF%90%EB%A7%8C-%EC%95%84%EB%8B%88%EB%9D%BC-%EB%B6%84%EC%84%9D%EB%8F%84-%EC%9E%90%EB%8F%99%ED%99%94-%EB%90%9C</guid>
            <pubDate>Tue, 24 Jun 2025 05:48:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>식사중에 warning 알람이 발생했습니다. 식사를 멈추고 업무에 복귀해야 할까요?</p>
</blockquote>
<p>어플리케이션 또는 시스템 문제가 발생했을때, 알람을 받게 하는것은 쉽습니다.</p>
<p>하지만 실제 그 알람의 본질이 어떤것인지, 얼마나 중요성이 높은것인지 판단하는 것은 훨씬 어렵습니다.</p>
<p>특히 당장 분석을 위한 노트북을 가지고 있지 않은 환경이라면 더욱 그렇죠.</p>
<p>즉, 외부에 있을때에는 알람이 발생해도 문제의 원인과 중요성, 얼마나 빠르게 대응해야 하는지 등을 파악하기가 더욱 쉽지 않습니다.</p>
<h1 id="">:&lt;</h1>
<p>그래서 저희는 만들었습니다. 삥뽕이를!
<img src="https://velog.velcdn.com/images/useradd_temp/post/58bca1a2-8166-4732-a12c-b40d9a9f61fc/image.png" alt="삥뽕"></p>
<p>삥뽕이의 목표는 단순합니다</p>
<p>slack에서 알람이 발생하면 
-&gt; 정확한 알람 내용을 분석하고 
-&gt; 요약해서 
-&gt; 중요성 해결방안을 제안</p>
<h1 id="삥뽕-구성도">삥뽕 구성도</h1>
<p>삥뽕이의 구성은 간단합니다</p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/11c4f347-acf3-4702-bcf5-05c0e19031a4/image.png" alt="architecture"></p>
<ol start="0">
<li><p>모든 어플리케이션에 동일하게 사용되는 알람은 datadog crd를 이용해서 application workload와 함께 같이 차트로 구성합니다.</p>
</li>
<li><p>알람은 prompt와 함께 삥뽕이를 호출합니다. 
<img src="https://velog.velcdn.com/images/useradd_temp/post/9b634005-fd23-4a05-ae73-8cddc080dd27/image.png" alt="alarm"></p>
</li>
<li><p>slack event subscription을 이용해 lambda를 해당 스레드 내용을 sqs에 발행합니다</p>
</li>
<li><p>sqs 메시지에 기록된 정보를 바탕으로 lambda는 bedrock을 invoke 합니다</p>
</li>
</ol>
<h2 id="aws-bedrock">aws bedrock</h2>
<p>이제 삥뽕의 본질 bedrock 입니다</p>
<p>삥뽕의 bedrock agent는 크게 세개의 에이전트로 구성되어 있습니다.</p>
<h3 id="1-advisor">1. advisor</h3>
<p>문제 원인 파악을 위해 어떤 데이터가 필요할까요? 이 대답을 주는 에이전트가 advisor 입니다</p>
<p>advisor는 문제 분석을 위해 필요한 지표, 분석 기간, 분석 조건을 반환합니다.</p>
<p>예를 들어 </p>
<p><code>CPU 사용량이 높아요! 최근 1시간 데이터를 기반으로 원인을 분석</code>해야 한다면</p>
<ul>
<li><p>총 분석 기간 1시간 중에서 CPU가 가장 높은 시간 전후 5분간 메트릭</p>
</li>
<li><p>총 분석 기간 1시간 중에서 CPU가 가장 낮은 시간 전후 5분간 메트릭</p>
</li>
<li><p>총 분석 기간 1시간 중에서 CPU가 가장 높은 시간 전으로 10분간 스케쥴링 예약된 태스크</p>
</li>
</ul>
<p>이렇게 여러 지표를 각각 적절한 시점에서의 데이터를 추출하여 분석하는것이 유리합니다.</p>
<p>advisor는 이렇게 분석할 지표의 scope를 낮춰주는 knowledge base와 시점을 판단하기 위해 도움을 주는 action group으로 구성되어 있습니다.</p>
<h3 id="2-collector">2. collector</h3>
<p>실제 데이터를 수집하는것은 collector 입니다.</p>
<p>collector는 advisor가 반환한 데이터 수집 쿼리를 이용해 적절한 메트릭을 수집하고, 필요하다면 lambda 또는 다른 bedrock agent를 이용해서 데이터를 압축/요약합니다.</p>
<p>메트릭 데이터는 15초 간격으로 10분간 수집한다면, 메트릭 하나당 40개의 지표가 수집됩니다.</p>
<p>CPU, Memory, Disk, Network, Requests, Profile, Trace/Span 등 수집하고자 하는 지표가 많아질수록 데이터를 압축/요약해서 분석가에게 건내줘야 합니다.</p>
<p>너무 불필요하게 많은 데이터는 분석가를 멍청하게 만들기 때문이죠.</p>
<h3 id="3-supervisor">3. supervisor</h3>
<p>수집한 데이터를 분석하고 중요성을 판단하며 사용자가 읽기 쉽게 처리하는것은 supervisor의 역할입니다.</p>
<p>supervisor는 요약된 정보를 분석하여 진짜 원인을 분석하고, 
사용자가 읽기 쉽게 time format을 변환하며, 
slack에서의 가시성을 높이기 위한 slack toolkit 형태의 json으로 데이터를 변환합니다.</p>
<p>특히 이 중에 가시성을 높이기 위한 response format을 변경하는 작업은 실제 분석과 무관한 작업으로, 이런 불필요한 작업은 post-processing에서 처리하도록 하였습니다.</p>
<h3 id="response">response</h3>
<p>그 결과 </p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/0bf44e5a-dda5-4d7d-a172-1c70048f9788/image.png" alt="events"></p>
<p>동일 알람 스레드에 댓글을 남기면서, 해당 알람 처리자에게 알람을 주면서 분석 결과를 알려주게 구성되어 있습니다.</p>
<p>결국 원하던 목표인 외부에서도 문제를 빠르게 판단할 수 있도록 알람을 자동 분석하는 AI agent 삥뽕이를 완성하였습니다</p>
<h2 id="-1">+++</h2>
<p>내용을 간단히 적었기에 추가로 설명이 필요한 부분들 입니다</p>
<h3 id="1-lambda의-payload-사이즈-제한">1. lambda의 payload 사이즈 제한</h3>
<p>action group에서 반환받을 수 있는 데이터의 사이즈 제한이 있습니다. 이 부분은 일반적인 API를 개발하듯이 페이지네이션 처리할 수 있습니다.</p>
<pre><code class="language-text">## Pagination 기능이 적용된 툴을 이용해 전체 데이터를 조회하는 방법
  1. `cursor` parameter를 비워둔 채 요청 수행
  2. 응답에 `has_next` 값이 true일 경우 다음 페이지가 존재 한다는 의미
  3. 응답에 `cursor` 값을 동일한 요청에서 `cursor` 값으로 사용 (이전 요청의 다음 페이징 처리를 의미 함)
  4. [1]~[3]의 행동을 반복 최대 10회까지 반복 (데이터가 충분하다고 판단되어도 스스로 반복을 임의 종료하지 말 것)</code></pre>
<p>이런 가이드를 줘서 페이지네이션을 자동으로 처리하게 구성하였습니다</p>
<h3 id="2-시간-처리-능력">2. 시간 처리 능력</h3>
<p>(경험상) bedrock agent는 현재 시간을 제대로 알지를 못합니다... 1시간전은 2023년이며, 현재 시간은 미래입니다.</p>
<p>데이터 수집과 분석에서 발생 시간과 분석 기간은 아주 엄격하게 관리되야할 대상입니다.</p>
<p>LLM 스스로 포맷변환 또는 추론하는것을 막기 위해</p>
<pre><code class="language-text">  4. 상대적 시간 표현 처리:
    분석 기간이 &quot;어제 1시부터&quot; 같은 상대적인 표현일 경우
    1. GET::current_datetime::getCurrentDate 툴을 먼저 사용하여 현재 시간의 timestamp 확인
    2. GET::convert_timstamp::convertTimeFormat 툴을 사용하여 현재 시간의 iso 8601 포맷 확인
    3. &quot;어제&quot;와 같은 상대적 시간 표현은 [2]에서 확인한 날짜를 사용 (현재 날짜가 2025년 1월 3일 이였을 경우, 질의하고자 하는 분석 기간은 2025년 1월 2일을 사용)
    4. 추가로 주어진 시간 표현, 표현식 처리를 결합하여 iso8601 포맷으로 작성 (사용자는 항상 UTC+9 타임존을 사용)
      -예: &quot;어제 1시&quot;로 질의되고, [3]에서 확인한 날짜가 2025-01-02인 경우 분석 시간은 &quot;2025-01-02 01시&quot;이며, 여기에 추가적으로 누락된 시간 표현인 &quot;분&quot;, &quot;초&quot;는 가이드라인 규칙 2에 따라 0분, 0초를 사용. 즉 결과는 &quot;2025-01-02T01:00:00+09:00&quot;
    5. [4]에서 확인한 시간을 UTC+9 타임존이 반영된 결과라고 생각하고 GET::convert_datetime::convertTimeFormat 툴을 사용하여 반환된 timestamp를 분석 기간에 사용</code></pre>
<p>이런 종류의 가이드를 여러개 사용하고 있습니다. (하나로는 해결이 잘 안되더라고요)</p>
<h3 id="3-왜-안-mcp">3. 왜 안 MCP?</h3>
<p>편합니다. 단순히 툴링으로써는 MCP가 더 편하겠습니다만, 적당히 복잡하면서 편하려면 bedrock agent도 충분히 좋습니다.</p>
<h3 id="4-datadog-profile-추출">4. datadog profile 추출</h3>
<p>저희는 datadog을 사용중입니다. profile을 분석하고 싶었지만, datadog의 profile api가 존재하지 않습니다. </p>
<p>우선은 feature request를 남겨놨지만, 언제 만들어 줄지는 모르는 일입니다. (아니면 영원히 안만들어 줄지도)</p>
<p>다행히, <a href="https://github.com/DataDog/datadog-pgo">datadog-pgo 프로젝트</a>를 보면 profile을 직접 다운받아서 분석할 수 있는 api를 확인할 수 있습니다. <em>(불법이 아닙니다. datadog에 물어봤습니다)</em></p>
<p>간단하게 작성한거지만, 대략 이런 형태로 profile을 다운받을 수 있습니다.</p>
<pre><code class="language-python">import io
import zipfile
from datetime import datetime, timedelta, UTC

import requests

api_key = &quot;KEY&quot;
app_key = &quot;KEY&quot;
download_path = &#39;PATH&#39;
headers = {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    &quot;User-Agent&quot;: &quot;datadog-client/1.0&quot;,
    &quot;DD-APPLICATION-KEY&quot;: app_key,
    &quot;DD-API-KEY&quot;: api_key
}
payload = {
    &#39;filter&#39;: {
        &#39;from&#39;: (datetime.now(UTC) - timedelta(minutes=10)).strftime(&quot;%Y-%m-%dT%H:%M:%S.%fZ&quot;),
        &#39;to&#39;: datetime.now(UTC).strftime(&quot;%Y-%m-%dT%H:%M:%S.%fZ&quot;),
        &#39;query&#39;: &#39;service:SERVICE&#39;},
    &#39;sort&#39;: {
        &#39;order&#39;: &#39;desc&#39;,
        &#39;field&#39;: &#39;timestamp&#39;},
    &#39;limit&#39;: 10,
}

response = requests.post(f&quot;https://app.datadoghq.com/api/unstable/profiles/list&quot;, headers=headers, json=payload)

for item in response.json().get(&quot;data&quot;):
    profile = {
        &quot;EventID&quot;: item[&quot;id&quot;],
        &quot;ProfileID&quot;: item[&quot;attributes&quot;][&quot;id&quot;],
        &quot;Service&quot;: item[&quot;attributes&quot;][&quot;service&quot;],
        &quot;CPUCores&quot;: item[&quot;attributes&quot;][&quot;custom&quot;][&quot;metrics&quot;].get(&quot;core_cpu_cores&quot;, 0),
        &quot;Timestamp&quot;: datetime.fromisoformat(item[&quot;attributes&quot;][&quot;timestamp&quot;].replace(&quot;Z&quot;, &quot;+00:00&quot;)),
        &quot;Duration&quot;: timedelta(seconds=item[&quot;attributes&quot;][&quot;duration_nanos&quot;] / 1e9)
    }
    zip_buffer = io.BytesIO()
    zip_buffer.seek(0)
    response = requests.get(f&quot;https://app.datadoghq.com/api/ui/profiling/profiles/{profile.get(&#39;ProfileID&#39;)}/download?eventId={profile.get(&#39;EventID&#39;)}&quot;,
        headers=headers, stream=True, )

    for chunk in response.iter_content(chunk_size=8192):
        zip_buffer.write(chunk)

    with zipfile.ZipFile(zip_buffer) as zf:
        zf.extractall(download_path)
</code></pre>
<p>하지만 api endpoint가 <code>/api/unstable</code> 라는걸 보면...</p>
<h3 id="5-왜-삥뽕이-인가">5. 왜 삥뽕이 인가</h3>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/8936d13b-fd11-451e-a989-c188b932555d/image.png" alt="삥뽕">
서버가 고장났다 = 서버가 삥뽕하다</p>
<p>제가 자주 쓰는 표현입니다</p>
<h2 id="끗">끗</h2>
<p>이 글은 AI에 대한 내용을 말하고자 하는건 아닙니다. </p>
<p>개발자가 더 쉽고 편하게 운영 모니터링을 하기 위한 DevOps 도구로서의 AI에 촛점을 맞추고 있습니다.</p>
<p>다른 아이디어, 질문, 재미있는것들이 있으면 언제나 환영하고 있습니다.</p>
<h3 id="마지막으로">마지막으로</h3>
<p>아마 곧 이런 기능이 datadog에서 나올것 같아서 혹시 제 수고가 헛수고가 된건 아닐까 무섭긴 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Lambda의 global variable과 provisioned concurrency]]></title>
            <link>https://velog.io/@useradd_temp/AWS-Lambda%EC%9D%98-BlueGreen</link>
            <guid>https://velog.io/@useradd_temp/AWS-Lambda%EC%9D%98-BlueGreen</guid>
            <pubDate>Tue, 27 Aug 2024 07:53:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>AWS Lambda의 provisioned concurrency에 대해 생길 수 있는 오해</p>
</blockquote>
<h3 id="lambda의-cold-start">Lambda의 Cold Start</h3>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/dbc02988-bce3-4aee-a653-c2bfabbd813d/image.png" alt=""></p>
<p>Lambda를 통해 workload를 운영할때 겪는 가장 첫번째 문제는 <code>Cold Start</code>입니다. </p>
<p>Lambda는 배포된 이후 처음 요청이 들어온 경우 </p>
<ul>
<li>Download Code</li>
<li>Start environment</li>
<li>Execute Init Code</li>
</ul>
<p>총 3단계의 초기화 단계를 거치며 이 과정이 끝난 이후에 handler 코드를 실행합니다.</p>
<p>이중에 수초 이상 걸리는 앞의 두 단계를 cold start 라고 부르며 초기 요청에 대한 latency를 느리게 만드는 요소로 작동합니다.</p>
<p>물론 한번 생성된 lambda 환경은 일반적으로 수분이내에 새로운 요청이 올 경우 기존의 환경은 앞의 두 단계를 재반복 할 필요가 없으며 cold start에 비해 빠르게 반환합니다. 이때의 lambda를 warm start라고 부릅니다</p>
<p>하지만 일반적으로 모든 lambda에 대해서 멈추지 않고 수분이내 계속해서 요청이 오는 경우는 흔치 않습니다. 요청 빈도가 적을때 흔히 사용하는것이 lambda의 특징이기도 하고요.</p>
<h3 id="functions-warmer">Functions Warmer</h3>
<p>cold start를 방지하기 위한 하나의 방법은 기존의 warm 환경이 삭제되는것을 방지하기 위해 계속에서 lambda를 호출하는 것입니다. 이때 lambda를 호출하는 대상을 functions warmer라고 부릅니다. </p>
<p>아쉽지만 function warmer도 문제점이 있습니다. lambda는 env sticky하지 않습니다. 즉 주기적으로 람다를 호출한다고 해도 해당 호출은 동일한 환경에 대한 호출을 보장하지 않습니다. </p>
<p>이러한 특징으로 인해 warmer는 특정 warm lambda를 계속 호출하여 cold start를 방지할 수 있다고 보장할 수 없습니다.</p>
<p>그리고 이미 요청이 있을 경우 warmer로 인해 또 다른 환경이 생성되는 불필요한 동시성 유발됩니다.</p>
<h3 id="provisioned-concurrency">Provisioned Concurrency</h3>
<p>functions warmer의 문제를 극복하기 위한 도구로 provisioned concurrency가 있습니다. 
<img src="https://velog.velcdn.com/images/useradd_temp/post/74a68009-dfda-4129-8ea4-4b0c2cc155c7/image.png" alt=""></p>
<p>provisioned concurrency를 활용하면 lambda의 cold start를 예방할 수 있는것은 물론, init code하는 부분도 invocation단계에서 걷어내서 더욱 빠른 latency를 유지할 수 있게 됩니다.</p>
<h3 id="하지만">하지만</h3>
<p>재밌는 테스트를 위해 lambda의 provisioned concurrency를 1로 유지한채 5초 단위로 lambda를 호출하도록 코드를 구성해 보겠습니다</p>
<p>lambda의 코드는 간단하게 python으로 작성하였습니다</p>
<pre><code class="language-python">### --- init block ---
import json
import datetime
import time
print(datetime.datetime.now())
print(&quot;Lambda Started&quot;)
time.sleep(8)
### --- init block ---

def lambda_handler(event, context):

    return {
        &#39;statusCode&#39;: 200,
        &#39;body&#39;: json.dumps(&#39;Hello from Lambda!&#39;)
    }</code></pre>
<p>code init 단계를 확실히 확인할 수 있도록 8초 동안 sleep 하도록 구성하였습니다 (lambda의 초기화 timeout은 10초이기 때문에 이보다 짧아야 합니다)</p>
<p>lambda는 function URL을 통해서 간단하게 5초 단위로 무제한 호출하였습니다</p>
<pre><code class="language-python">import time
import requests

while True:
  response = requests.get(&quot;LAMBDA FUNCTION URL!&quot;)
  print(response.text)
  time.sleep(5)</code></pre>
<p>Lambda의 cloudwatch logstream은 하나의 lambda 환경에 해당하는데, 초기에는 당연하게도 A환경에 대해서만 START, END, REPORT 로그가 반복해서 출력됩니다. </p>
<pre><code class="language-text"># 편의상 logstream 이름을 맨 앞에 붙여넣었습니다
A | START RequestId: ...
A | END RequestId: ...
A | REPORT RequestId: ...
...
...
...
A | START RequestId: ...
A | END RequestId: ...
A | REPORT RequestId: ...
...
...
...</code></pre>
<p>재밌는점은 2시간정도가 지나면 확인할 수 있습니다.</p>
<pre><code class="language-text"># 편의상 logstream 이름을 맨 앞에 붙여넣었습니다
A | START RequestId: ...
A | END RequestId: ...
A | REPORT RequestId: ...
B | INIT_START Runtime Version ...
B | 현재 시간
B | Lambda Started
A | START RequestId: ...
A | END RequestId: ...
A | REPORT RequestId: ...
</code></pre>
<p>갑자기 중간에 B Logstream이 생겨납니다.</p>
<pre><code class="language-text">A | START RequestId: ...
A | END RequestId: ...
A | REPORT RequestId: ...
B | START RequestId: ...
B | END RequestId: ...
B | REPORT RequestId: ...
...</code></pre>
<p>그러더니 갑자기 요청이 A환경에서 B환경으로 넘어갑니다. lambda 환경이 blue/green 방식으로 교체되는 것입니다</p>
<p>물론 이 코드에서 lambda 환경이 백그라운드 상에서 교체되는것 자체는 문제가 없습니다. </p>
<p>하지만 문제를 유발하는 코드를 고의적으로 작성해보겠습니다.</p>
<pre><code class="language-python">import json
import time

im_slow = None

def slow_generator():
  time.sleep(10)
  return &quot;foo&quot;

def lambda_handler(event, context):
    global im_slow
    if im_slow is None:
        im_slow = slow_generator()
    return {
        &#39;statusCode&#39;: 200,
        &#39;body&#39;: json.dumps(im_slow)
    }</code></pre>
<p>lambda의 handler 블럭안에 있는 코드가 lazy하게 global variable인 <code>im_slow</code>를 생성하고 재활용하는 코드입니다.</p>
<p>이러한 코드는 init code 단계에서는 함수 선언만 하기 때문에 <code>im_slow</code>는 <code>None</code>로 초기화 된 상태입니다.</p>
<p>그리고 실제로 INIT_START가 끝난 이후 첫 요청에 대해서 <code>im_slow</code>를 생성하므로 (lazy initilalize) 10초의 시간이 걸리게 됩니다.</p>
<h3 id="conclusion">Conclusion</h3>
<p>lambda의 handler 코드가 global varialbe을 handler 코드에서 lazy initilalize하는 경우 예상치 느린 latency가 발생하는것을 확인할 수 있습니다.</p>
<p>그리고 이 문제는 lambda를 concurrency를 설정한 경우 lambda의 blue/green update로 인해 더 잦게(경험적으로 2시간내외) 발생하는것을 확인할 수 있습니다.</p>
<p>이러한 람다의 실행 구조를 이해하고 lambda 함수를 작성하면 운영상 생길 수 있는 문제점을 예방할 수 있을것입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[당신의 istio-envoy, xff spoofing 안전한가요?]]></title>
            <link>https://velog.io/@useradd_temp/%EB%8B%B9%EC%8B%A0%EC%9D%98-istio-envoy-xff-spoofing-%EC%95%88%EC%A0%84%ED%95%9C%EA%B0%80%EC%9A%94</link>
            <guid>https://velog.io/@useradd_temp/%EB%8B%B9%EC%8B%A0%EC%9D%98-istio-envoy-xff-spoofing-%EC%95%88%EC%A0%84%ED%95%9C%EA%B0%80%EC%9A%94</guid>
            <pubDate>Thu, 29 Feb 2024 06:15:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/useradd_temp/post/76a1b464-ee7f-458b-b9a4-21261a5b60d0/image.png" alt=""></p>
<h3 id="tldr">TL;DR</h3>
<p>istio는 디코딩 불가능한 문자열을 포함한 xff spoofing으로 한번에 박살낼 수 있습니다</p>
<h3 id="경고">경고</h3>
<p>당연히 공격하라는 의도가 아니라 대비하라는 의도입니다. <strong>왜냐면 제가 당했거든요 🥲</strong></p>
<h3 id="준비-또는-전제-조건">준비 또는 전제 조건</h3>
<p>istio Operator 기준으로 작성하였습니다. operator가 아닌 다른 방식으로도 동일한 설정값을 통해 동일한 현상을 유도할 수 있습니다.</p>
<p>테스트에 사용한 alpha 버전이며, alpha 계열에서만 발생하는것으로 확인 되어집니다.</p>
<p><em>본 문제는 istio group에 공유된 상황입니다</em></p>
<p>필요한 설정은 딱 2개입니다</p>
<pre><code class="language-yaml">apiVersion: install.istio.io/v1alpha1  
kind: IstioOperator  
metadata:  
  name: istio  
spec:  
  meshConfig:  
    accessLogEncoding: JSON  
    accessLogFile: /dev/stdout</code></pre>
<p>다른 불필요한 설정은 제거하였으며 (spec.profile 등) 필요한 조건은 access log의 출력 관련된 단 2개의 설정 <code>accessLogEncoding</code>과 <code>accessLogFile</code> 뿐입니다</p>
<h3 id="공격">공격</h3>
<p>앞서 말씀드린것처럼 디코딩 불가능한 적당한 문자열로 공격해볼 수 있습니다. python으로 가볍게 스크립트를 짜면 이렇습니다</p>
<pre><code class="language-python">import requests  

url = &quot;TEST_URL&quot;
headers = {&quot;X-Forwarded-For&quot;: &quot;\xec&quot;}   

response = requests.get(url=url, headers=headers)  </code></pre>
<p>적당히 한글을 인코딩한 코드에서 일부만 뜯어내서 디코딩 할 수 없도록 테스트 문자역을 사용했습니다.</p>
<p>GET, POST 등 모든것과 무관하게 디코딩 불가능한 xff만 포함된다면 어떤 요청이건 동일하게 반응합니다.</p>
<h3 id="로그">로그</h3>
<p>이때 반환되는 로그를 적당히 가시성을 고려해서 만지작 거리면</p>
<pre><code>critical    envoy main external/envoy/source/exe/terminate_handler.cc:33    std::terminate called! Uncaught unknown exception, see trace.    thread=26
critical    envoy backtrace external/envoy/source/server/backtrace.h:91    Backtrace (use tools/stack_decode.py to get line numbers):    thread=26
critical    envoy backtrace external/envoy/source/server/backtrace.h:92    Envoy version: 9d6c288b111627edf6d7cb380e4895e1224d5da1/1.30.0-dev/Clean/RELEASE/BoringSSL    thread=26
critical    envoy backtrace external/envoy/source/server/backtrace.h:96    #0: Envoy::TerminateHandler::logOnTerminate()::$_0::__invoke() [0x559f2906b4bf]    thread=26

... ... ...

critical    envoy backtrace external/envoy/source/server/backtrace.h:98    #0: [0x7f2e43649520]    thread=26
ActiveStream 0x559f2c3bd500, stream_id_: 9614445000952796613&amp;filter_manager_:
  FilterManager 0x559f2c3bd5a8, state_.has_1xx_headers_: 0
  filter_manager_callbacks_.requestHeaders():
    &#39;:path&#39;, &#39;/&#39;
    &#39;:method&#39;, &#39;GET&#39;
    &#39;:scheme&#39;, &#39;http&#39;
    &#39;x-forwarded-for&#39;, &#39;�, x.x.x.x, y.y.y.y&#39;
    &#39;x-forwarded-proto&#39;, &#39;http&#39;
    &#39;x-forwarded-port&#39;, &#39;443&#39;
    &#39;user-agent&#39;, &#39;python-requests/2.31.0&#39;
    &#39;accept-encoding&#39;, &#39;gzip, deflate, br&#39;</code></pre><p>이런 로그가 출력됩니다. 눈여겨 봐야할 점은 시작 익셉션이 <code>Uncaught unknown exception</code>으로 시작되고 다행히 마지막에 문제가 된 요청을 출력해 주는점입니다.</p>
<p>출력된 요청의 xff 헤더를 보시면 디코딩이 실패하여 깨진 문자열 <code>�</code> 을 확인할 수 있습니다.</p>
<p>현재까지 확인된 바로는 다른 헤더, 다른 상황에서는 동일한 현상을 목격하지 못하였습니다.</p>
<p>위에 설명드린 케이스 이외의 다른 상황을 목격하셨다면 공유해주세요!</p>
<h3 id="결과">결과</h3>
<p>공격에 대한 결과는 ingress pod가 Exit Code 0로 종료되는것입니다.</p>
<p>아쉽게도 kubelet은 재시작 정책을 exponential backoff를 사용하고 있습니다.</p>
<p>고의적인 공격이라면 해당 restart가 점점 더 길어져서 결국엔 장기적인 서비스 중단이 유발됩니다.</p>
<p>ingress와 동일하게 envoy sidecar도 동일한 문제를 유발하는것이 가능합니다 (실제로 두 pod는 논리적으로 동일하므로 똑같은 문제가 똑같이 발생합니다)</p>
<h3 id="대응">대응</h3>
<p>디코딩 불가능한 xff 헤더를 차단하는것으로 쉽게 공격을 방어할 수 있습니다.</p>
<p>xff 헤더는 다행히 그 의미에서부터 가능한 문자열을 추릴 수 있습니다.</p>
<p>ipv4와 ipv6의 규칙으로 발생하는 <code>숫자</code>, <code>문자</code>, <code>.</code>, <code>:</code> 그리고 레이어가 많아짐으로 생기는 추가 ip 접합 문자인 <code>,</code> </p>
<p>xff를 디코드 했을때 이렇게 총 5개의 케이스를 제외한 다른 문자열이 포함된 요청을 차단할 경우 해당 문제를 대비할 수 있습니다.</p>
<h3 id="맺음말">맺음말</h3>
<ol>
<li><p>velog 이거 자꾸 글 쓸때 간혹 본문이 날라가버립니다... 처음에 잘 작성된 글이 날라가서 머릿속 내용으로 복구했는데 이게 정상적으로 작성된건지 자신이 없습니다.</p>
</li>
<li><p>저는 보안은 매우 약합니다 ㅜㅜ... 늘 보안보다 속도감이 중요한 조직에 있었고 실제 그런 작업을 더 선호하기도 했다보니깐요</p>
</li>
<li><p>이번 기회를 통해 제가 조금 더 보안문제를 잘 해결할 수 있는 능력이 늘었으면...하는 생각이 듭니다</p>
</li>
<li><p>악용하지 마세요. 철컹철컹 합니다</p>
</li>
</ol>
<h3 id="추가">추가</h3>
<ul>
<li><a href="https://github.com/envoyproxy/envoy/security/advisories/GHSA-g979-ph9j-5gg4">envoy security issue</a>로 등록되어 있습니다 (해결)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[딱 다섯줄 짜리 FOCP 합격 후기]]></title>
            <link>https://velog.io/@useradd_temp/%EB%94%B1-%EB%8B%A4%EC%84%AF%EC%A4%84-%EC%A7%9C%EB%A6%AC-FOCP-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@useradd_temp/%EB%94%B1-%EB%8B%A4%EC%84%AF%EC%A4%84-%EC%A7%9C%EB%A6%AC-FOCP-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 13 Feb 2024 02:28:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/useradd_temp/post/7ebf1fbf-cdbc-42f3-a69f-3e76fb37fe8c/image.png" alt=""></p>
<ol>
<li>FOCP는 FinOps Certified Practitioner의 줄임말입니다</li>
<li>시험 난이도는 쉬우면서도 어렵습니다</li>
<li>300달러라는 어마어마한 비용이지만, 할인은 없으니 당장 그냥 시도하세요</li>
<li>60분 50문제라는, 보기에는 다소 빠듯한 시간같아 보이지만 용어가 익숙하다면 절반의 시간이면 충분합니다.</li>
<li>&quot;새로운 무언가를 배우고 싶을때 가장 쉬운 방법은 해당 자격증을 공부하는것이다&quot;에 100% 공감합니다.</li>
</ol>
<hr>
<p>더 궁금한점이 있다면 댓글을 남겨주세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CoreDNS on Fargate, Datadog으로 모니터링하기]]></title>
            <link>https://velog.io/@useradd_temp/EKS-on-Fargate-Datadog%EC%9C%BC%EB%A1%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@useradd_temp/EKS-on-Fargate-Datadog%EC%9C%BC%EB%A1%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 30 Jan 2024 08:53:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>EKS, Fargate, Datadog에 대한 기본적인 설명은 없습니다</p>
</blockquote>
<p>논리적인 스토리 전개를 위해 실제 히스토리를 작성한것은 아닙니다. 일부 망상과 각색이 존재합니다.</p>
<h3 id="사건의-발단">사건의 발단</h3>
<p>Karpenter를 사용하면 Node의 Lifecycle이 굉장히 짧아질 수 있습니다. 그래서 가능하면 크리티컬한 워크로드의 경우 karpenter가 제어하는 노드가 아닌 managed node 또는 fargate에서 실행되는 편이 마음이 편안합니다. 예를 들어 coredns, metric-server 등이 여기에 속합니다.
그리고 스스로가 스스로를 죽이는 행위를 방지하기 위해 karpenter 자체도 fargate에 올라가는것이 관리하기 좋습니다.</p>
<h3 id="슉샥슉샥">슉샥슉샥</h3>
<p>데이터독을 이용하여 모니터링 하기 위한 구성은 간단합니다. datadog을 sidecar로 붙여주면 끝입니다. 하지만 fargate로 올라가는 워크로드가 얼마나 늘지, 줄지 그리고 누가 어떻게 생성할지는 알 수 없습니다.</p>
<p>Kyverno를 활용하도록 합니다! </p>
<p>Kyverno를 활용하면 admission controller를 활용하여 fargate에 생성되는 pod에 sidecar를 붙여넣는 행동을 할 수 있습니다. 여타 다른 custom admission controller와 같은 방식입니다. (datadog을 쓰신다면 datadog의 apm admission controller도 이와 같은 방식입니다)</p>
<p>우선 해당 datadog agent가 sidecar로 붙을 pod를 지정합시다. 우선 기본 대상인 coredns를 확인해봅니다.</p>
<pre><code class="language-yaml">        labels:
          eks.amazonaws.com/component: coredns
          k8s-app: kube-dns</code></pre>
<p>이제 적당히 datadog agent를 sidecar로 붙이려면 이정도 느낌의 policy면 될것 같습니다.</p>
<pre><code class="language-yaml">apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-datadog-sidecar
spec:
  rules:
    - name: fargate-pods
      match:
        any:
          - resources:
              kinds:
                - Pod
              selector:
                matchLabels:
                  eks.amazonaws.com/component: coredns
      mutate:
        patchStrategicMerge:
          metadata:
            annotations:
              datadog-inject: &quot;true&quot;
          spec:
            shareProcessNamespace: true
            containers:
              - name: datadog-agent
                image: datadog/agent
                env:
                  - name: DD_API_KEY
                    value: &quot;XXX&quot;
                  - name: DD_SITE
                    value: &quot;datadoghq.com&quot;
                  - name: DD_EKS_FARGATE
                    value: &quot;true&quot;
                  - name: DD_CLUSTER_NAME
                    value: &quot;CLUSTER_NAME&quot;
                  - name: DD_CLUSTER_AGENT_ENABLED
                    value: &quot;true&quot;
                  - name: DD_CLUSTER_AGENT_AUTH_TOKEN
                    value: &quot;AUTH_TOKEN&quot;
                  - name: DD_ORCHESTRATOR_EXPLORER_ENABLED
                    value: &quot;true&quot;
                  - name: DD_KUBERNETES_KUBELET_NODENAME
                    valueFrom:
                      fieldRef:
                        apiVersion: v1
                        fieldPath: spec.nodeName
                resources:
                  requests:
                    memory: &quot;256Mi&quot;
                    cpu: &quot;200m&quot;
                  limits:
                    memory: &quot;256Mi&quot;
                    cpu: &quot;200m&quot;</code></pre>
<p>적당히 필요한 환경변수와 토큰, API 등을 입력해줍니다. 이제 <code>rollout/restart</code> 를 통해 pod를 재배치 하면 잘 해결될것 같지만...</p>
<pre><code>2024-07-07 07:77:77 UTC | PROCESS | ERROR | (pkg/workloadmeta/collectors/internal/kubemetadata/kubemetadata.go:75 in Start) | Could not initialise the communication with the cluster agent: temporary failure in clusterAgentClient, will retry later: cannot get a cluster agent endpoint for kubernetes service DATADOG_CLUSTER_AGENT, env DATADOG_CLUSTER_AGENT_SERVICE_HOST is empty</code></pre><p>중간에 뭐 이런 요상한 에러가 뜨고, datadog에서 확인해봐도 뭔가 클러스터와 pod가 연결되는것 같지 않습니다.</p>
<p>sidecar로 붙은 datadog agent는 일부 데이터를 cluster agent로 보내서 처리합니다. 그런데 cluster agent의 URL은 어떻게 될까요?</p>
<p>별다른 설정이 없다면 <code>datadog-cluster-agent.datadog</code> 같은 형태일겁니다. 하지만 문제는 coredns입니다. 이러한 내부 도메인의 DNS를 처리하는 역할은 coredns가 담당하고 있습니다. </p>
<p>그리고 스스로가 스스로의 네임서버가 될 순 없으니 coredns의  <code>dnspolicy</code>는 <code>Default</code> 입니다. 이로 인해 coredns의 sidecar는 내부 도메인을 사용하여 datadog cluster agent를 사용할 수 없습니다.</p>
<p>그렇다면 datadog cluster agent의 clusterIP로 호출하면 됩니다!</p>
<p>하지만 모든 클러스터의  datadog cluster agent의  cluster ip가 동일하다는 보장은 없습니다. 심지어 datadog을 operator pattern 또는 helm으로 설치한다면 svc의 clusterIP를 지정할수도 없습니다. </p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: {{ template &quot;datadog.fullname&quot; . }}-cluster-agent
  namespace: {{ .Release.Namespace }}
  labels:
{{ include &quot;datadog.labels&quot; . | indent 4 }}
spec:
  type: ClusterIP
  selector:
    app: {{ template &quot;datadog.fullname&quot; . }}-cluster-agent
  ports:
  - port: 5005
    name: agentport
    protocol: TCP</code></pre>
 <center>datadog service의 template는 이런 형태입니다. 수정할 여지를 안줍니다.</center>

<p>그렇다면 매 클러스터마다 자동생성되는, 그리고 고정되지도 않는 cluster IP를 매번 자동화하여 policy를 업데이트 하도록 구성해야 할까요?</p>
<p>만약 이렇게 생각하셨다면 helm 같은 패키지 관리자에 너무 익숙해져 버린겁니다.</p>
<p>우리는 종종 편안함에 익숙해져 커스터마이징 하는것을 종종 잊곤 합니다. helm에서 만들어준 svc를 그대로 쓸 필요 없습니다! 그냥 하나 더 만들면 되죠</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: manually
  name: self-managed-datadog-cluster-agent
  namespace: datadog
spec:
  clusterIP: 172.20.177.177
  internalTrafficPolicy: Cluster
  ipFamilyPolicy: SingleStack
  ports:
    - port: 5005
      protocol: TCP
      targetPort: 5005
  selector:
    agent.datadoghq.com/component: cluster-agent
    agent.datadoghq.com/name: datadog
  type: ClusterIP</code></pre>
<p>요런 느낌으로 자체 관리형 데이터독 서비스를 하나 더 만듭시다. 이러면 이 clusterIP는 거의 왠만하면 괜찮습니다. (이것마저도 100% 완전히 괜찮게 할 수 있겠지만 여기서는 다루지 않습니다)</p>
<p>이제 policy를 수정하여 datadog cluster agent의 endpoint를 명시해줍시다.</p>
<pre><code class="language-yaml">apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-datadog-sidecar
spec:
  rules:
    - name: fargate-pods
      match:
        any:
          - resources:
              kinds:
                - Pod
              selector:
                matchLabels:
                  eks.amazonaws.com/component: coredns
      mutate:
        patchStrategicMerge:
          metadata:
            annotations:
              datadog-inject: &quot;true&quot;
          spec:
            shareProcessNamespace: true
            containers:
              - name: datadog-agent
                image: datadog/agent
                env:
                  - name: DD_API_KEY
                    value: &quot;XXX&quot;
                  - name: DD_SITE
                    value: &quot;datadoghq.com&quot;
                  - name: DD_EKS_FARGATE
                    value: &quot;true&quot;
                  - name: DD_CLUSTER_NAME
                    value: &quot;CLUSTER_NAME&quot;
                  - name: DD_CLUSTER_AGENT_ENABLED
                    value: &quot;true&quot;
                  - name: DD_CLUSTER_AGENT_AUTH_TOKEN
                    value: &quot;AUTH_TOKEN&quot;
                  - name: DD_ORCHESTRATOR_EXPLORER_ENABLED
                    value: &quot;true&quot;
#! 요놈을 추가합니다 ------------------------------------------
                  - name: DD_CLUSTER_AGENT_URL
                    value: https://172.20.177.177:5005
# ---------------------------------------------------------
                  - name: DD_KUBERNETES_KUBELET_NODENAME
                    valueFrom:
                      fieldRef:
                        apiVersion: v1
                        fieldPath: spec.nodeName
                resources:
                  requests:
                    memory: &quot;256Mi&quot;
                    cpu: &quot;200m&quot;
                  limits:
                    memory: &quot;256Mi&quot;
                    cpu: &quot;200m&quot;</code></pre>
<p>이러면 적어도 클러스터를 새로 만들때마다 datadog cluster IP를 확인하고 policy를 생성하고 sidecar inject를 하는 수동 작업을 할 필요는 없을겁니다.</p>
<p>이제 datadog을 들어가서 확인해보면 원하는대로 클러스터와 컨테이너가 잘 결합한 모습이 보입니다.
<img src="https://velog.velcdn.com/images/useradd_temp/post/2e166b98-1122-4823-a893-2b66c280d845/image.png" alt="chart"></p>
<center> 17:4x 기준으로 해당 수정을 변경한 결과</center>

<p>[참고]
cluster agent와의 연결이 영향을 미치는 datadog상의 기능은</p>
<ul>
<li><a href="https://docs.datadoghq.com/containers/kubernetes/#event-collection">events collection</a></li>
<li><a href="https://docs.datadoghq.com/infrastructure/containers/?tab=docker#kubernetes-resources-view">Kubernetes resources view </a></li>
<li><a href="https://docs.datadoghq.com/containers/cluster_agent/clusterchecks/?tab=helm#overview">cluster checks</a></li>
</ul>
<p>입니다</p>
<h3 id="사실">사실...</h3>
<p>사실 하고싶은 말은 fargate에 kyverno를 사용하여 sidecar injection을 구현하는것은 아닙니다. 이 부분은 다른 여러 방식으로 mutating webhook을 만들어서 구성할수도 있습니다. 오히려 kyverno로 sidecar inject를 하는것은 kyverno의 원래 역할을 벗어난다고 느끼실 수도 있습니다.</p>
<p>종종 잘 만들어진 패키지를 그대로 사용하는것에 익숙해져서 다른 생각을 하지 못하는 경우가 있습니다. </p>
<p>예를 들어 helm에 묶여있는 서비스가 아닌 별도 관리형의 고전된 clusterIP를 가지는 서비스를 만드는것이 아닌,
kyverno의 자동화에 사로잡혀 datadog svc의 admission event를 받아서 kyverno policy를 수정하고 sidecar를 수정하는 방식은 다소 경직된 사고라고 생각합니다.</p>
<p>이번 포스팅을 통해서 뇌가 조금이라도 말랑말랑 해지셨으면 성공한 포스팅이 된것 같네요!</p>
<h3 id="누락된-부분">누락된 부분</h3>
<ol>
<li>datadog agent를  sidecar로 쓰려면 추가적인 clusterrole이 필요합니다.
```yaml</li>
</ol>
<ul>
<li>apiGroups:<ul>
<li>&quot;&quot;
resources:</li>
<li>nodes</li>
<li>namespaces</li>
<li>endpoints
verbs:</li>
<li>get</li>
<li>list</li>
</ul>
</li>
<li>apiGroups:<ul>
<li>&quot;&quot;
resources:</li>
<li>nodes/metrics</li>
<li>nodes/spec</li>
<li>nodes/stats</li>
<li>nodes/proxy</li>
<li>nodes/pods</li>
<li>nodes/healthz
verbs:</li>
<li>get         <pre><code></code></pre></li>
</ul>
</li>
</ul>
<p>대충 이런 느낌인데, 이것도 kyverno로 슉슉샥 할 수 있겠죠?</p>
<ol start="2">
<li>eks addon으로 설치된 coredns에 pod label을 설정할 수 있습니다.</li>
</ol>
<pre><code class="language-json">{&quot;computeType&quot;:&quot;Fargate&quot;,&quot;podLabels&quot;:{&quot;compute-type&quot;:&quot;Fargate&quot;}}</code></pre>
<p>이런식으로 configuration values를 수정하여 pod label 기준으로 통일된 fargate selector를 구성할 수 있겠습니다.</p>
<h2 id="추가-내용-2024년-언젠가">추가 내용 (2024년 언젠가)</h2>
<p>현재는 datadog에서 apm agent를 admission controller로 inject하는것과 유사하게, 
fargate (사실 특정 조건을 만족하는 pod)에 datadog agent sidecar를 inject 하는 기능을 지원하기 시작했습니다.</p>
<p>dns policy문제로 인해 coredns는 별도의 env를 오버라이드 해줘야 하긴 하지만 이 글이 작성되던 시점보다는 훨씬 간단하게 (kyverno 없이) 구성하는것이 가능합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[pod requests/limits 똑똑하게 관리하기]]></title>
            <link>https://velog.io/@useradd_temp/pod-requestslimits-%EB%98%91%EB%98%91%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@useradd_temp/pod-requestslimits-%EB%98%91%EB%98%91%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 24 Jan 2024 03:28:36 GMT</pubDate>
            <description><![CDATA[<p>이 전 <a href="https://velog.io/@useradd_temp/Karpenter-QOS">포스팅</a>의 연장입니다.</p>
<hr>
<h3 id="무엇이-문제인가">무엇이 문제인가</h3>
<p>대부분의 어플리케이션 Pod들은 초기화 단계에서 높은 CPU를 요구하는데 반해, 그 이후 과정에서는 낮은 CPU를 요구합니다.</p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/4c1c54ac-34f3-4920-b285-a5c035a95ebb/image.png" alt=""></p>
<center>이런 컨테이너들은 정말 많습니다</center>


<p>그렇다면 Pod의 requests를 어떻게 설정할 수 있을까요?</p>
<ol>
<li><p>초기 높은 CPU를 기준으로 삼으면
 -&gt; over provisioning에 대한 효과로 불필요한 비용이 지출됩니다.</p>
</li>
<li><p>운영 기준의 낮은 CPU를 기준으로 삼으면
 -&gt; 초기화 단계에서 너무 오랜 시간이 소요되고 워크로드에 따라 아예 실행되지 못하는 상황도 발생 가능합니다.</p>
</li>
<li><p>burstable QOS를 설정하여 운영기준으로 request, 초기화 기준으로 limit를 설정하면
 -&gt; node에 request 기준으로 꽉차게 pod가 스케쥴링 된 경우 guaranteed 같은 설정이 되어서 오히려 예상치 못한 (2)의 상황으로 전환되어 더욱 복잡해 질 수 있습니다.</p>
</li>
</ol>
<hr>
<h3 id="심플-아이디어">심플 아이디어</h3>
<p>간단한 해결법은 pod의 request를 스케쥴링 단계 및 초기화 단계 그리고 트래픽을 받기 전까지의 request는 높게, 그 이후에는 낮게 설정할 수 있다면 해결할 수 있겠습니다.</p>
<p>그런데 pod의 request는 수정이 불가능한 값입니다. </p>
<p><strong>쿠버네티스 1.27버전 전까지는요!</strong></p>
<p>이제 새로운 기능을 맛봅시다!</p>
<hr>
<h3 id="pod-resize-policy">Pod resize policy</h3>
<p>kubernetes pod의 resize policy는 1.27버전에서 알파로 추가된 기능입니다. </p>
<p>pod의 requests, limits를 재시작없이 또는 재시작을 동반한 업데이트를 할 수 있도록 지원합니다.</p>
<p>현재 기준으로 아직 alpha 단계의 feature이므로 feature gate를 활성화 해줍니다. feature명은 <code>InPlacePodVerticalScaling</code> 으로 등록되어 있습니다</p>
<pre><code class="language-yaml">apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
apiServer:
  extraArgs:
    feature-gates: &quot;InPlacePodVerticalScaling=true&quot;</code></pre>
<p>이제 테스트용 nginx pod를 하나 실행하겠습니다. </p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: resizable-nginx
spec:
  containers:
  - name: nginx
    image: nginx
    resizePolicy:
    - resourceName: cpu
      restartPolicy: NotRequired
    - resourceName: memory
      restartPolicy: RestartContainer
    resources:
      limits:
        memory: &quot;200Mi&quot;
        cpu: &quot;700m&quot;
      requests:
        memory: &quot;200Mi&quot;
        cpu: &quot;700m&quot;</code></pre>
<p>pod 상태를 확인해보면 </p>
<pre><code class="language-shell">$ k get po resizable-nginx -o jsonpath=&#39;{.spec.containers[0].resources}&#39; | jq

{
  &quot;limits&quot;: {
    &quot;cpu&quot;: &quot;700m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  },
  &quot;requests&quot;: {
    &quot;cpu&quot;: &quot;700m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  }
}</code></pre>
<pre><code class="language-shell">$ k describe po resizable-nginx

Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  7m9s  default-scheduler  Successfully assigned default/resizable-nginx to worker1
  Normal  Pulling    7m9s  kubelet            Pulling image &quot;nginx&quot;
  Normal  Pulled     7m6s  kubelet            Successfully pulled image &quot;nginx&quot; in 2.149202326s (2.149223074s including waiting)
  Normal  Created    7m6s  kubelet            Created container nginx
  Normal  Started    7m6s  kubelet            Started container nginx
</code></pre>
<p>pod에서 설정한대로 원하는 수치로 생성된것이 확인 됩니다.</p>
<p>이제 patch를 통해 cpu를 조정해보겠습니다. </p>
<pre><code class="language-shell">$ k patch pod resizable-nginx --patch &#39;{&quot;spec&quot;:{&quot;containers&quot;:[{&quot;name&quot;:&quot;nginx&quot;, &quot;resources&quot;:{&quot;requests&quot;:{&quot;cpu&quot;:&quot;800m&quot;}, &quot;limits&quot;:{&quot;cpu&quot;:&quot;800m&quot;}}}]}}&#39;</code></pre>
<pre><code class="language-shell">$ k get po resizable-nginx -o jsonpath=&#39;{.spec.containers[0].resources}&#39; | jq
{
  &quot;limits&quot;: {
    &quot;cpu&quot;: &quot;800m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  },
  &quot;requests&quot;: {
    &quot;cpu&quot;: &quot;800m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  }
}</code></pre>
<pre><code class="language-shell">$ k describe po resizable-nginx

Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  9m19s  default-scheduler  Successfully assigned default/resizable-nginx to worker1
  Normal  Pulling    9m19s  kubelet            Pulling image &quot;nginx&quot;
  Normal  Pulled     9m16s  kubelet            Successfully pulled image &quot;nginx&quot; in 2.149202326s (2.149223074s including waiting)
  Normal  Created    9m16s  kubelet            Created container nginx
  Normal  Started    9m16s  kubelet            Started container nginx</code></pre>
<p>cpu의 <code>restartPolicy</code>는 <code>NotRequired</code>이기 때문에 재시작 없이 resource의 requests와 limits가 변경되었습니다!</p>
<p>이제 <code>restartPolicy</code>가 <code>RestartContainer</code>로 지정된 memory를 patch 해보겠습니다.</p>
<pre><code class="language-shell">$ k patch pod resizable-nginx --patch &#39;{&quot;spec&quot;:{&quot;containers&quot;:[{&quot;name&quot;:&quot;nginx&quot;, &quot;resources&quot;:{&quot;requests&quot;:{&quot;memory&quot;:&quot;400Mi&quot;}, &quot;limits&quot;:{&quot;memory&quot;:&quot;400Mi&quot;}}}]}}&#39;</code></pre>
<pre><code class="language-shell">$ k get po resizable-nginx -o jsonpath=&#39;{.spec.containers[0].resources}&#39; | jq
{
  &quot;limits&quot;: {
    &quot;cpu&quot;: &quot;800m&quot;,
    &quot;memory&quot;: &quot;400Mi&quot;
  },
  &quot;requests&quot;: {
    &quot;cpu&quot;: &quot;800m&quot;,
    &quot;memory&quot;: &quot;400Mi&quot;
  }
}</code></pre>
<pre><code>Events:
  Type    Reason     Age                From               Message
  ----    ------     ----               ----               -------
  Normal  Scheduled  10m                default-scheduler  Successfully assigned default/resizable-nginx to worker1
  Normal  Pulled     10m                kubelet            Successfully pulled image &quot;nginx&quot; in 2.149202326s (2.149223074s including waiting)
  Normal  Pulling    31s (x2 over 10m)  kubelet            Pulling image &quot;nginx&quot;
  Normal  Killing    31s                kubelet            Container nginx resize requires restart
  Normal  Created    29s (x2 over 10m)  kubelet            Created container nginx</code></pre><p>cpu와 동일하게 patch는 되지만 restart event가 발생하는것을 확인할 수 있습니다.</p>
<hr>
<h3 id="kyverno">kyverno</h3>
<p>resize policy를 이용하여 pod의 resource를 재시작 없이 수정할 수 있는것은 확인했습니다.</p>
<p>하지만 언제, 어떻게 수정하냐에 대한 문제가 남아있습니다.</p>
<p>직접 컨틀롤러, CRD 등을 개발해서 이 기능을 구현할 수도 있겠지만 더 쉬운 방법인 <code>kyverno</code>를 사용하겠습니다.</p>
<p>kyverno를 쿠버네티스 리소스의 유효성 검사, 변경, 생성 등을 제공하는 정책엔진 (policy engine)입니다. </p>
<p>(<em>이 포스팅에서는 분량상의 문제로 kyverno를 자세하게 다루지는 않습니다.</em>)</p>
<p>helm chart로 kyverno를 설치하겠습니다.</p>
<pre><code class="language-shell">helm repo add kyverno https://kyverno.github.io/kyverno
</code></pre>
<p>이때 사용할 기본 values에서 excludeGroups을 수정합니다.</p>
<pre><code class="language-yaml">config:
  excludeGroups: []</code></pre>
<p>기본값은 <code>system:nodes</code>를 제외하게 되어있습니다. 하지만 pod의 probe 등을 처리하는 컴포넌드는 node의 kubelet이고 kubelet은 <code>system:nodes</code> Group을 사용하게 되어있습니다. 저희는 kubelet event를 처리해야하기 때문에 <code>excludeGroups</code> 에서 <code>system:nodes</code>를 제외합니다.</p>
<p>이제 kyverno 설치를 마무리 합니다.</p>
<pre><code class="language-shell">helm upgrade -install kyverno kyverno/kyverno -n kyverno --create-namespace -f values.yaml</code></pre>
<p>kyverno가 pod를 직접 수정하기 때문에 적절한 clusterrole이 필요합니다. 
하지만 기본으로 생성된 clusterrole을 수정하는것은 이벤트 손실을 유발할 수 있으므로, 새롭게 clusterrole을 생성하는것이 권장됩니다.</p>
<p>pod resource를 수정하기 위한 권한을 추가합니다.</p>
<pre><code class="language-yaml">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/component: background-controller
    app.kubernetes.io/instance: kyverno
    app.kubernetes.io/part-of: kyverno
  name: kyverno:resize-pods
rules:
- apiGroups:
  - &#39;&#39;
  resources:
  - pods
  verbs:
  - patch
  - update</code></pre>
<p>resize policy는 resource를 수정할 수 있게 하는 feature입니다.  </p>
<p>문제는 QOS는 resource와는 약간 별개의 값이기 때문에 <code>guaranteed</code>로 pod를 구성하였다면 request를 낮춰서 <code>burstable</code>로 바꾸는 행위는 할 수 없습니다. </p>
<p>이점에 유의하여 위에서 작성한 nginx pod manifest를 일부 수정합니다. 
추가적으로 pod가 ready된 이후에 리소스 변경이 일어나는지 확인하기 위해 probe를 추가하였습니다.</p>
<pre><code class="language-yaml">    resources:
      limits:
        memory: &quot;200Mi&quot;
        cpu: &quot;700m&quot;
      requests:
        memory: &quot;200Mi&quot;
        cpu: &quot;500m&quot;
    readinessProbe: 
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 60</code></pre>
<p>그리고 pod가 ready상태가 되면 container의 cpu requests를 200m로 수정하도록 kyverno의 ClusterPolicy를 구성합니다.</p>
<pre><code class="language-yaml">apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: resize-pods
spec:
  rules:
    - name: resize-pods
      match:
        any:
          - resources:
              kinds:
                - Pod/status
      preconditions: 
        all:
          - key: &quot;{{request.object.status.containerStatuses[0].ready}}&quot;
            operator: Equals
            value: true
      mutate:
        targets:
          - apiVersion: v1
            kind: Pod
            name: &quot;{{request.object.metadata.name}}&quot;
        patchStrategicMerge:
          spec:
            containers:
              - (name): nginx
                resources:
                  requests:
                    cpu: &quot;200m&quot;</code></pre>
<p>pod를 생성한 직후 초기화 단계에서의 상태입니다.</p>
<pre><code class="language-shell">$ k get po resizable-nginx -o jsonpath=&#39;{.spec.containers[0].resources}&#39; | jq

{
  &quot;limits&quot;: {
    &quot;cpu&quot;: &quot;700m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  },
  &quot;requests&quot;: {
    &quot;cpu&quot;: &quot;500m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  }
}</code></pre>
<p>잠시 시간이 지난 이후 pod가 ready 상태가 된 이후 동일한 명령어를 입력해보면, pod의 리소스가 변한것을 확인할 수 있습니다.</p>
<pre><code class="language-shell">$ k get po resizable-nginx -o jsonpath=&#39;{.spec.containers[0].resources}&#39; | jq

{
  &quot;limits&quot;: {
    &quot;cpu&quot;: &quot;700m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  },
  &quot;requests&quot;: {
    &quot;cpu&quot;: &quot;200m&quot;,
    &quot;memory&quot;: &quot;200Mi&quot;
  }
}</code></pre>
<p>이제 이 메커니즘을 이용해서 저희는 초기화 단계에 필요로 하는 높은 cpu를 기준으로 하는 pod를 구성하고, 초기화 단계 이후 cpu를 resizing 해서 pod 운영을 최적화 하는 워크로드를 구성할 수 있습니다!</p>
<hr>
<h3 id="한계">한계</h3>
<p>꽤나 우아해 보이는 이러한 구성도 한계가 있습니다.</p>
<ol>
<li>EKS는 alpha 버전의 feature를 지원하지 않습니다.</li>
</ol>
<ul>
<li>Which Kubernetes features are supported by Amazon EKS?
  Amazon EKS supports all generally available (GA) features of the Kubernetes API. Starting with Kubernetes version 1.24, new beta APIs aren&#39;t enabled in clusters by default. However, previously existing beta APIs and new versions of existing beta APIs continue to be enabled by default. <strong>Alpha features aren&#39;t supported.</strong></li>
</ul>
<p>이로 인해 적어도 EKS에서는 당장 구현하기 힘듭니다. 시간이....필요합니다....</p>
<ol start="2">
<li>karpenter의 이상 현상</li>
</ol>
<p>karpenter의 consolidation policy를 <code>WhenUnderutilized</code>를 사용할 경우 이상현상이 발생할 수 있습니다. (테스트 할 수 없는 상황이기에 정확하진 않습니다)</p>
<p>아래 그림처럼 4core 노드 2대에 각각 3core와 1core를 사용하고 있는 상황이라고 생각해 봅시다.
이 경우 <code>WhenUnderutilized</code> policy를 사용하고 있을 경우 karpenter는 아래에 있는 노드를 버리고 윗쪽의 노드 하나만을 사용하고자 할껍니다.
<img src="https://velog.velcdn.com/images/useradd_temp/post/f587a4df-546d-4146-9d86-f6d7d57572fc/image.png" alt="p"></p>
<center>초기 단계</center>

<p>하지만 아랫쪽에 있던 1core pod가 사실 resize 되기 전에는 2core pod였다면 무슨 현상이 발생할까요? 아마 새로운 노드를 다시 요청하게 될것입니다. 그런데 또다시 안정화 단계에서 CPU 사용량이 낮아지고... 이 무한 반복이 발생할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/36771238-7653-41b8-a9c2-1f0656765e3d/image.png" alt="s"></p>
<center>???????</center>

<p>그래도 아직 추측하건데 이 현상은 <code>WhenUnderutilized</code>가 아닌 <code>WhenEmpty</code>를 사용하면 발생하지 않지 않을까...하는 기대는 하고 있습니다.</p>
<hr>
<h3 id="🥸">🥸</h3>
<p>우선 이 내용의 힌트는 karpenter contributor인 Jason Deal에게서 얻었습니다.
이 전 포스팅의 문제를 해결하기 위해 일부 예약된 자원을 통해 node를 필요한 스펙보다 더 큰 요청을 하게 하는 방식을 고려했고 그 기능을 Feature Request로 올렸습니다. <a href="https://github.com/kubernetes-sigs/karpenter/issues/945#event-11557529185">[참고]</a></p>
<p>여기서 resizable resources에 대한 힌트를 얻었고 그 아이디어로 여기까지 발전할 수 있었습니다.</p>
<p>그리고 재밌는것은 k8s blog의 InPlacePodVerticalScaling에 대한 블로그에 example use cases로 <code>Java processes initialization CPU requirements</code> 를 직접적으로 언급하기도 합니다 <a href="https://kubernetes.io/blog/2023/05/12/in-place-pod-resize-alpha/#java-processes-initialization-cpu-requirements">[참고]</a></p>
<p>물론 위에 작성한 내용이 이런 현상을 모두 해결해주는 방법은 절대 아닙니다. 하지만 이런 선택지와 아이디어를 통해 더 나은 쿠버네티스 운영을 고려하는 기회가 되시길 바랍니다.</p>
<p>감삼당</p>
<hr>
<h3 id="추가">추가</h3>
<p>구글이 <a href="https://github.com/google/kube-startup-cpu-boost">https://github.com/google/kube-startup-cpu-boost</a> 요런걸 만드는 중입니다.</p>
<p>기본적으로 유사한 메커니즘입니다. 그러기에 당연히 <code>InPlacePodVerticalScaling</code> 를 필요로 하는것도 동일합니다. </p>
<hr>
<h3 id="reference">reference</h3>
<ol>
<li><a href="https://kubernetes.io/blog/2023/05/12/in-place-pod-resize-alpha/">https://kubernetes.io/blog/2023/05/12/in-place-pod-resize-alpha/</a></li>
<li><a href="https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/">https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/</a></li>
<li><a href="https://piotrminkowski.com/2023/08/22/resize-cpu-limit-to-speed-up-java-startup-on-kubernetes">https://piotrminkowski.com/2023/08/22/resize-cpu-limit-to-speed-up-java-startup-on-kubernetes</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Karpenter - QOS]]></title>
            <link>https://velog.io/@useradd_temp/Karpenter-QOS</link>
            <guid>https://velog.io/@useradd_temp/Karpenter-QOS</guid>
            <pubDate>Mon, 22 Jan 2024 15:18:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 글을 적고싶지 않았습니다. 이런 일을 겪고 싶지 않았습니다</p>
</blockquote>
<p>이 글에서는 고통을 겪은 히스토리만 나옵니다. 해결법에 대한 아이디어는 다음 <a href="https://velog.io/@useradd_temp/pod-requestslimits-%EB%98%91%EB%98%91%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0">포스팅</a>에 작성되어 있습니다.</p>
<h3 id="문제의-시작">문제의 시작</h3>
<p>Karpenter를 도입한 이후 어느날 이벤트가 발생했습니다. 개발자들에게 별도로 공지는 없는 작은 이벤트였고 자연스럽게 컴퓨팅 리소스들은 스케일아웃되고 자동으로 대응되고 있었습니다.</p>
<p>그런데 배치성 업무를 담당하는 어플리케이션이 갑자기 실행되지 못하는것을 발견했습니다.</p>
<p>이벤트가 종료된 이후에도 마찬가지였고 문제 발생 전후 짧은 기간동안 바뀐 구성은 karpenter의 사용 유무 뿐이였습니다.</p>
<p>다행히 다른 replica는 정상적으로 실행되어 서비스가 중단되는 현상까지는 발생하지 않았습니다.</p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/dd4ef448-fe12-494c-ae0d-8477b7bacbbd/image.png" alt="restarts"></p>
<center>100회 이상 재시작된 슬픈 상황</center>





<h3 id="문제-분석">문제 분석</h3>
<p>해당 컨테이너는 spring boot로 구성되어 있습니다. 실행시 DB에 접속을 시도하며 DB 접속을 실패하기 때문에 서비스 실행 자체가 안되서 restart 하고 있었습니다.</p>
<p>이런식의 에러입니다</p>
<pre><code>The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
...
...
Caused by: software.aws.rds.jdbc.mysql.shading.com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
...
...
Caused by: java.net.SocketException: Connection attempt exceeded defined timeout.</code></pre><p>우선 확실해 보이는건 어떤 종류의 네트워크...뭔가 뭔가 라고 생각했습니다.</p>
<h3 id="문제-재현">문제 재현</h3>
<p>재현을 하고자 했으나... 뭔가 이상합니다. 아래에 있는 설정을 켰다가 껏다가 했을때 모두 재현이 되다가 안되다가 멋대로입니다</p>
<ul>
<li><p>노드 타입 변경 (c5a &lt;-&gt; c5n ...)</p>
</li>
<li><p>노드 유형 변경 (karpenter node가 아닌 managed node)</p>
</li>
<li><p>coredns를 fargate로 분리</p>
<ul>
<li>fargate에서 실행되는  coredns를 datadog으로 모니터링 하는 방법은 나중에 게시하겠습니다</li>
</ul>
</li>
<li><p>동일한 노드의 여러 파드중에서도 일부 파드에서만 문제 발생</p>
</li>
<li><p>동일한 노드에서도 발생 하기도하고 안하기도 하고...</p>
</li>
<li><p>클러스터 변경 (운영에서는 발생, 다른곳에서는 발생하지 않음[?])</p>
</li>
<li><p>특정 데몬셋(datadog agent)를 제외</p>
</li>
<li><p>istio 등 잡다한 네트워크 레이어 제거</p>
</li>
</ul>
<p>문제는 이 모든것들이 어떨때는 재현이 되고, 어떨때는 안되는 재현이 안되는... 재현 신뢰성이 낮은 상황이 발생했습니다.</p>
<p>이 때문에 정확하게 예측가능한 재현이 가능한 상황을 찾아내기 위해 거의 2주(!) 에 가까운 시간을 소모했습니다.</p>
<p>사실 결과를 알고보면 왜 재현이 됬다가 안됬다가... 전혀 상관 없는 다른 pod 때문에도 왜 됬다가 안됬다가 하는지 알 수 있습니다 8_8</p>
<h3 id="문제의-이유">문제의 이유</h3>
<p>해당 서비스의 QOS는 <code>burstable</code>입니다.  cpu limit가 cpu request가 높은 상황으로 구성되어 있습니다.</p>
<p>대부분의 어플리케이션은 실행시 필요한 리소스의 양이 실행 후 안정된 리소스의 요구치보다 아주 높습니다.</p>
<p>라이브러리를 로드하고, 쓰레드를 생성하고, 초기 DB 연결을 구성하거나 하는 초기화 작업이 꽤 높은 리소스를 요구하기 때문입니다.</p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/33f04798-f2d0-4d3e-86bc-cfe89253512c/image.png" alt="cpu usage"></p>
<center>초기화 작업에 엄청난 CPU를 요구합니다! (limit까지 사용한 상황)</center>

<p>서비스의 QOS를 <code>burstable</code>로 할것이냐 <code>guaranteed</code>로 할것이냐는 늘 논쟁 가능한 부분입니다.</p>
<p>하지만 저런 CPU 사용량을 보인다면 burstable로 구성하고 싶어집니다. 그리고 저희 문제가 되는 그 워크로드가 바로 burstable 이였습니다.</p>
<p>이러한 설정에는 전혀 문제가 없습니다. <b>하지만 karpenter는 bin-packing을 잘 계산해줍니다.</b></p>
<p>karpenter는 필요한 리소스에 맞는 가장 가격 합리적인 노드를 선택합니다. 그러다보니 이 선택이 가장 빈패킹 잘하는, 즉 노드의 사용량을 100%로 맞추는 행위를 유발하기도 합니다.</p>
<p>이러면 Pod들은 burstable이라고 하더라도 남는 리소스가 없기 때문에 guaranteed 처럼 작동합니다.</p>
<p>이로 인해 예상보다 낮은 CPU 할당과 그로 유발되는 throttling이 발생했습니다.</p>
<p>결국 네트워크 문제가 아니라 성능 문제였던 것입니다. 그러면 위에 있었던 재현 의문증이 모두 해소됩니다.</p>
<hr>
<ul>
<li><p>해당 노드에 실행된 워크로드의 cpu limit 비율에 따라</p>
</li>
<li><p>데몬셋 등을 삭제하면 노드 자체의 여유 CPU가 생김</p>
</li>
</ul>
<hr>
<p>문제가 해소되는지 테스트하기 위해 DB connection timeout을 10초로 늘리고 다시 재현되는지 테스트 해봤는데... 재현되지 않습니다!</p>
<p>문제를 해결했습니다. </p>
<p>burstable 이였던 pod의 CPU를 높여서 guaranteed로 변경하였고 모든 문제는 해결되었습니다.</p>
<h3 id="왜-이렇게-문제-해결이-오래걸렸는가에-대한-변론">왜 이렇게 문제 해결이 오래걸렸는가에 대한 변론</h3>
<p>데브옵스 엔지니어로서 저도 지표는 미친듯이 봅니다.</p>
<p>온갖 네트워크 지표, 어플리케이션 지표, 노드 및 파드 지표 등등 별의 별 지표는 다 봤습니다.</p>
<p>하지만 슬프게도 초기화 작업에서 커넥션이 몇초나 걸리는지는 정확하게 볼 수 있는 지표가 없었습니다. </p>
<p>쓰로틀링도 원래 초기에 약간 발생하는 상황이였습니다.</p>
<p>어플리케이션의 connection timeout에 설정된 시간보다 살짝 빠르게 connection이 맺어지는 아슬아슬한 상황으로 서비스가 지속되고 있었던겁니다.</p>
<p>그 아슬아슬한 상황에서 약간의 CPU 마저 빼앗아 가버렸으니 실행이 성공하기도/실패하기도 하는거였습니다.</p>
<h3 id="그렇다면">그렇다면</h3>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/48944cf6-0d12-400c-9eda-3962ba05c214/image.png" alt="cat"></p>
<p>이런 상황을 방지할 순 없을까요?</p>
<p>고양이와 박스처럼, 미배치된 파드들이 딱 맞는 노드에 배치됨으로 생기는 burstable이 guaranteed 처럼 작동하는 현상 그리고 그로 인해 생기는 성능저하는 막을 수 없을까요?</p>
<p>request를 limit만큼 올려서 비용을 일부 포기하고 워크로드의 안정성을 갖는것만이 방법일까요?</p>
<p>효과적인 방법을 <a href="https://velog.io/@useradd_temp/pod-requestslimits-%EB%98%91%EB%98%91%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0">다음글</a>에서 소개드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Karpenter의 나쁜 문서]]></title>
            <link>https://velog.io/@useradd_temp/Karpenter%EC%9D%98-%EB%82%98%EC%81%9C-%EB%AC%B8%EC%84%9C</link>
            <guid>https://velog.io/@useradd_temp/Karpenter%EC%9D%98-%EB%82%98%EC%81%9C-%EB%AC%B8%EC%84%9C</guid>
            <pubDate>Fri, 29 Dec 2023 07:41:28 GMT</pubDate>
            <description><![CDATA[<p>Karpenter의 docs에는 잘못된 부분들이 있습니다. 
일부 유저에게는 크리티컬할 수도 있는 부분인데, 어떤 일이 있었는지 공유합니다.</p>
<p>*<em><em>[주의] 이 글은 Karpenter의 설치 방법과 작동 방식 등을 설명하는 내용은 전혀 아닙니다.</em>
*</em></p>
<h3 id="karpenter-install">Karpenter Install</h3>
<p>우선 EKS에 Karpenter를 설치하고 있었습니다. </p>
<p>이미 여러 방식으로 말려져 있는 karpenter 모듈/스택 등을 사용한다면 karpenter controller가 필요로 하는 여러 권한을 직접 입력할 필요는 없습니다.</p>
<p>하지만 저는 따끈따끈하게 나온 EKS Pod Identity를 쓰고 싶었고 karpenter가 필요로 하는 권한들을 직접 입력하였습니다.</p>
<p>karpenter controller가 필요로 하는 권한은 여러 문서에 적혀있습니다.</p>
<ol>
<li><a href="https://karpenter.sh/docs/reference/cloudformation/">karpenter 공식 문서의 reference</a> </li>
<li><a href="https://github.com/terraform-aws-modules/terraform-aws-eks/blob/master/modules/karpenter/main.tf">terraform karpenter module</a></li>
<li><a href="https://github.com/aws/karpenter-provider-aws/blob/main/website/content/en/docs/getting-started/getting-started-with-karpenter/cloudformation.yaml">karpenter github에 작성된 cloudformation.yaml</a><ul>
<li>공식 문서의 getting start에서는 docs가 아니라 <a href="https://github.com/aws/karpenter-provider-aws/blob/main/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml">preview</a>를 참조하고 있습니다</li>
</ul>
</li>
</ol>
<p>작업하기 귀찮음과 설명이 포함되어 있다는것 등의 여러 이유가 있기 때문에 하필 1번을 선택했습니다.</p>
<h2 id="문제-발생">문제 발생</h2>
<p>공식문서의 권한을 그대로 부여할 경우 두가지 문제가 발생합니다.</p>
<h3 id="node가-join-되지-않는다">node가 join 되지 않는다</h3>
<p>아직 정확한 karpenter에 대한 이해도가 부족할때였지만 문제는 원인은 직관적으로 찾을 수 있었습니다.</p>
<p>아래 권한은 해당 문제가 수정되기 전 권한입니다</p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/1684168a-caf9-41f4-8c91-3af95df65698/image.png" alt="before"></p>
<p>이 권한은 <code>karpenter.sh/cluster/클러스터명</code> 태그의 값이 <code>owned</code>인 EC2 인스턴스에 한해서 karpenter controller가 <code>karpenter.sh/nodeclaim</code>과 <code>Name</code> 태그를 추가할 수 있도록 하는 권한입니다.</p>
<p>EKS를 많이 써보신 분은 눈에 익숙하지 않은 부분을 바로 찾을 수 있습니다</p>
<p>바로 <code>karpenter.sh/cluster/클러스터명</code> 입니다.  EKS는 원래 <code>kubernetes.io/cluster/클러스터명</code> 태그를 사용합니다. </p>
<p>이제 바로 karpenter의 cloudformation.yaml 코드를 확인해봤습니다.</p>
<pre><code>            {
              &quot;Sid&quot;: &quot;AllowScopedResourceTagging&quot;,
              &quot;Effect&quot;: &quot;Allow&quot;,
              &quot;Resource&quot;: &quot;arn:${AWS::Partition}:ec2:${AWS::Region}:*:instance/*&quot;,
              &quot;Action&quot;: &quot;ec2:CreateTags&quot;,
              &quot;Condition&quot;: {
                &quot;StringEquals&quot;: {
                  &quot;aws:ResourceTag/kubernetes.io/cluster/${ClusterName}&quot;: &quot;owned&quot;
                },
                &quot;StringLike&quot;: {
                  &quot;aws:ResourceTag/karpenter.sh/nodepool&quot;: &quot;*&quot;
                },
                &quot;ForAllValues:StringEquals&quot;: {
                  &quot;aws:TagKeys&quot;: [
                    &quot;karpenter.sh/nodeclaim&quot;,
                    &quot;Name&quot;
                  ]
                }
              }
</code></pre><p>역시나 <code>kubernetres.io/cluster</code>를 사용하고 있었습니다. 다행히 소스코드를 뜯어보기 전에 발견할 수 있었습니다.</p>
<p>바로 태그를 수정했고 노드는 정상적으로 join 됬으며 이 문제는 PR을 거쳐서 현재 karpenter docs에 정상적으로 반영되어 있습니다. <a href="https://github.com/aws/karpenter-provider-aws/pull/5392">(관련 PR)</a></p>
<h3 id="spot이-실행되지-않는다">Spot이 실행되지 않는다</h3>
<p>이것저것 만지다 보니 또다른 문제를 찾았습니다. 바로 Spot Instance가 실행되지 않는것이였는데,</p>
<p>로그를 확인해보니 Spot을 실행할 권한이 없다고 나옵니다. 그래서 또 권한을 확인했습니다.</p>
<p>아래는 현재 작성되어 있는 karpenter의 공식 문서입니다.</p>
<p><img src="https://velog.velcdn.com/images/useradd_temp/post/0716a0f3-09bf-407c-8b2b-bc3798a28a2d/image.png" alt=""></p>
<p>진짜 spot-instance가 없습니다. (첨부하진 않았지만 tag를 추가하는 권한도 없습니다)</p>
<p>또다시 <code>cloudformation.yaml</code> 문서를 확인했습니다.</p>
<pre><code>            {
              &quot;Sid&quot;: &quot;AllowScopedEC2InstanceActionsWithTags&quot;,
              &quot;Effect&quot;: &quot;Allow&quot;,
              &quot;Resource&quot;: [
                &quot;arn:${AWS::Partition}:ec2:${AWS::Region}:*:fleet/*&quot;,
                &quot;arn:${AWS::Partition}:ec2:${AWS::Region}:*:instance/*&quot;,
                &quot;arn:${AWS::Partition}:ec2:${AWS::Region}:*:volume/*&quot;,
                &quot;arn:${AWS::Partition}:ec2:${AWS::Region}:*:network-interface/*&quot;,
                &quot;arn:${AWS::Partition}:ec2:${AWS::Region}:*:launch-template/*&quot;,
                &quot;arn:${AWS::Partition}:ec2:${AWS::Region}:*:spot-instances-request/*&quot;
              ],
              &quot;Action&quot;: [
                &quot;ec2:RunInstances&quot;,
                &quot;ec2:CreateFleet&quot;,
                &quot;ec2:CreateLaunchTemplate&quot;
              ],
              &quot;Condition&quot;: {
                &quot;StringEquals&quot;: {
                  &quot;aws:RequestTag/kubernetes.io/cluster/${ClusterName}&quot;: &quot;owned&quot;
                },
                &quot;StringLike&quot;: {
                  &quot;aws:RequestTag/karpenter.sh/nodepool&quot;: &quot;*&quot;
                }
              }
            },</code></pre><p>역시나 이번에도 누락된것이 맞습니다 (CreateTag도 누락되어 있습니다).</p>
<p>이번에도 권한을 추가하니 정상적으로 Spot을 실행합니다. (<a href="https://github.com/aws/karpenter-provider-aws/pull/5402">관련 PR</a>)</p>
<p>(이번 수정은 아직 PR이 merge되지 않았습니다. 하지만 간단한 문서 수정이니 문제없이 하루 이틀 사이에 merge 될겁니다)</p>
<h3 id="">:(</h3>
<p>이 과정을 통해 Karpenter 문서의 문제점들을 찾았습니다. 따로 작성하진 않았지만 슬프게도 문서상에 또다른 문제들이 존재하는것처럼 보이기도 합니다. </p>
<p>이러한 문제를 해결하는 과정은 나름 저를 설레게 합니다. </p>
<p>아마 다음에는 문서의 오류가 아닌 다른 종류로 karpenter 프로젝트에 기여할 수 있지 않을까 하는 기대를 가지고 있습니다.</p>
<p>아마도요??</p>
]]></description>
        </item>
    </channel>
</rss>