<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Welcome my World</title>
        <link>https://velog.io/</link>
        <description>develop myself</description>
        <lastBuildDate>Sat, 23 May 2026 08:57:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Welcome my World</title>
            <url>https://velog.velcdn.com/images/mason_dev/profile/2a043454-1252-4d48-be8c-3ad929289ae6/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Welcome my World. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mason_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[로봇 1,000대 센서 데이터 파이프라인 구축기 4편 — Troubleshooting 및 회고]]></title>
            <link>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-4%ED%8E%B8-Troubleshooting</link>
            <guid>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-4%ED%8E%B8-Troubleshooting</guid>
            <pubDate>Sat, 23 May 2026 08:57:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 프로젝트에서 가장 많이 배운 건 코드를 짤 때가 아니라 뭔가 터졌을 때였다.
4편은 운영 중에 실제로 발생한 사고들을 시간 순서 없이, 기억나는 것들 위주로 정리한 글이다.</p>
</blockquote>
<hr>
<h2 id="사고-1-kds-재생성-후-slack-알림이-30분-동안-안-왔다">사고 1. KDS 재생성 후 Slack 알림이 30분 동안 안 왔다</h2>
<p>비용 절감을 위해 인프라를 내렸다가 다시 올리는 작업을 했다. KDS를 삭제하고 재생성하면 Firehose 연결이 자동으로 붙을 줄 알았다.</p>
<p>아니었다.</p>
<p>Firehose는 KDS가 삭제되는 순간 <code>SUSPENDED</code> 상태로 전환되고, 이후 새 KDS가 생겨도 <strong>자동으로 reconnect되지 않는다.</strong> 직접 다시 연결해줘야 한다. 이걸 몰라서 Bronze 적재가 0 rows인 채로 한참 돌았다.</p>
<p>Lambda도 같은 문제가 있었다. Alert KDS에 연결된 Lambda event source mapping이 <code>Disabled</code> 상태로 고착된다.</p>
<pre><code class="language-bash"># Disabled 항목 확인
aws lambda list-event-source-mappings \
    --event-source-arn &lt;alert-kds-arn&gt;

# 재활성화
aws lambda update-event-source-mapping \
    --uuid &lt;mapping-uuid&gt; \
    --enabled</code></pre>
<p>이걸 발견한 건 Flink가 Alert KDS에 17건을 보냈는데 Slack 알림이 하나도 안 왔을 때였다. 30분 동안 운영자 화면에서 아무 알림도 없었던 것. Lambda가 KDS를 읽지 않고 있었다.</p>
<p>지금은 <code>down.sh</code> / <code>up.sh</code> 스크립트에 이 재활성화 스텝을 명시적으로 포함했다. 까먹을 수 없게.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/7d90764e-814b-4a12-8f8a-5370e48b25d8/image.png" alt=""></p>
<hr>
<h2 id="사고-2-grafana에서-로봇이-1000대-중-480대만-보였다">사고 2. Grafana에서 로봇이 1,000대 중 480대만 보였다</h2>
<p>가장 오래 헤맨 사고다. 진단에만 1시간 넘게 걸렸다.</p>
<p>Grafana Anomaly Timeline 패널에서 <code>COUNT(DISTINCT robot_id)</code>를 5분 슬라이딩 윈도우로 집계하고 있었다. 어느 날 값이 갑자기 절반 수준으로 떨어졌다. 480, 490, 510... 1,000 근처가 아니라 딱 절반이었다.</p>
<p>처음엔 Generator가 절반만 데이터를 보내는 줄 알았다. CloudWatch KDS IncomingRecords를 보니 정상이었다.</p>
<p>그 다음엔 Athena 쿼리 문제인 줄 알았다. 직접 돌려보니 정상이었다.</p>
<p>실제 원인은 <strong>Firehose의 multi-shard flush stagger</strong>였다.</p>
<p>KDS 2개 Shard가 있을 때, 각 Shard의 Firehose flush 타이밍이 약 15초씩 어긋난다. Shard 1이 12:00:00에 flush하고, Shard 2가 12:00:15에 flush한다. PartitionKey(robot_id) 해시 기준으로 Shard가 나뉘기 때문에, 5분 슬라이딩 윈도우 안에 두 Shard의 데이터가 완전히 겹치지 않는 타이밍이 생긴다.</p>
<p>결국 5분 윈도우 안에 Shard 1 로봇만 잡히는 순간 → <code>COUNT(DISTINCT robot_id) ≈ 500</code>. 딱 절반이 나왔던 이유가 MD5 해시 분포가 Shard 경계와 거의 정확히 50:50으로 나뉘었기 때문이었다.</p>
<p>해결은 간단했다. 윈도우를 Firehose buffer 시간 + 여유 마진 이상으로 늘렸다.</p>
<pre><code>Firehose buffer: 300초 (5분)
Shard 수: 2
→ 최소 윈도우: 300 + (2 × 60) = 420초 (7분)
→ 실제 적용: 10분 윈도우</code></pre><p>근데 이걸 파악하기 전까지 &quot;Generator가 일부 로봇을 샘플링하는 게 아닐까?&quot;, &quot;S3 파티션이 일부만 쓰이는 건 아닐까?&quot; 등 전혀 다른 방향을 한참 들여다봤다. 결정적인 단서는 <strong>항상 딱 절반</strong>이 나온다는 거였다.</p>
<hr>
<h2 id="사고-3-slack-webhook-url이-코드에-박혔다">사고 3. Slack Webhook URL이 코드에 박혔다</h2>
<p>초반에 빠르게 테스트하려고 Slack Webhook URL을 코드에 직접 넣었다. GitHub에 올라갔다. Public repo였다.</p>
<p>Slack이 자동으로 탐지해서 Webhook을 즉시 폐기했다. 다행히 Slack 쪽에서 먼저 막아줬지만 찜찜했다.</p>
<p>이후 모든 민감값은 AWS Secrets Manager에 저장하고 Lambda에서 런타임 조회로만 쓴다. Terraform에서도 <code>TF_VAR_slack_webhook_url</code> 환경변수로 주입하던 방식을 버리고 Secrets Manager에서 직접 read하는 방식으로 바꿨다.</p>
<pre><code class="language-hcl">data &quot;aws_secretsmanager_secret_version&quot; &quot;slack_webhook&quot; {
  secret_id = &quot;/robot-telemetry/slack-webhook-url&quot;
}

resource &quot;aws_lambda_function&quot; &quot;alert_handler&quot; {
  environment {
    variables = {
      SLACK_WEBHOOK_URL = data.aws_secretsmanager_secret_version.slack_webhook.secret_string
    }
  }
}</code></pre>
<p>환경변수로 주입하면 <code>export</code>를 한 번만 빼먹어도 <code>CHANGEME</code> 같은 기본값이 prod에 조용히 들어간다. Secrets Manager에서 직접 read하면 이 패턴 자체가 없어진다.</p>
<hr>
<h2 id="사고-4-alb를-안-지우고-셧다운했다">사고 4. ALB를 안 지우고 셧다운했다</h2>
<p>비용 절감을 위해 EKS 워크로드를 전부 0으로 줄이는 셧다운 루틴을 만들었다. Pod을 전부 0으로 줄이면 노드가 없어지고, Karpenter가 EC2를 반납한다.</p>
<p>근데 ALB가 살아있었다.</p>
<p>Pod을 0으로 줄여도 <code>kubectl delete ingress</code>를 안 하면 ALB Controller가 ALB를 그대로 유지한다. AWS가 2024년 2월부터 public IPv4 주소를 attached/unattached 무관하게 과금하기 시작했다.</p>
<p>ALB 3개 × public IP 2개씩 × <code>$0.005/h</code> × 720h = <strong>월 $21.6 누수</strong>.</p>
<p>셧다운 루틴에 Ingress 삭제 + ALB 완전 삭제 확인을 추가했다.</p>
<pre><code class="language-bash"># Ingress 삭제
kubectl delete ingress --all -A

# ALB가 완전히 사라질 때까지 대기
until [ &quot;$(aws elbv2 describe-load-balancers \
    --query &#39;length(LoadBalancers)&#39; --output text)&quot; = &quot;0&quot; ]; do
    echo &quot;ALB 삭제 대기 중...&quot;
    sleep 30
done
echo &quot;ALB 완전 삭제 확인&quot;</code></pre>
<p>Pod만 0으로 줄이고 ALB가 살아있으면 노드는 사라지는데 public IP는 계속 과금되는 상황이 된다. 셧다운 루틴은 ALB까지 확인하고 끝내야 한다.</p>
<hr>
<h2 id="사고-5-dt--d-1-하드코딩">사고 5. <code>dt = D-1</code> 하드코딩</h2>
<p>API 서버에서 캐시를 갱신할 때 이렇게 썼었다.</p>
<pre><code class="language-python">yesterday = (datetime.now() - timedelta(days=1)).strftime(&#39;%Y-%m-%d&#39;)
query = f&quot;SELECT * FROM gold WHERE dt = &#39;{yesterday}&#39;&quot;</code></pre>
<p>평소엔 괜찮다. 근데 비용 절감을 위해 인프라를 내린 날 밤에 Airflow ETL이 돌지 않으면? <code>gold</code> 테이블에 어제 파티션이 없다. 캐시 갱신 시 빈 DataFrame이 올라가고, AI 채팅은 &quot;데이터를 찾을 수 없습니다&quot;만 반환하게 된다. <strong>자연 회복이 안 된다.</strong></p>
<p>다음날 인프라를 다시 올려도, 오늘 ETL이 돌기 전까지 어제 파티션은 없는 채로 유지된다.</p>
<pre><code class="language-python"># 수정: 최근 N일 안에서 MAX(dt) 사용
query = &quot;&quot;&quot;
    SELECT * FROM gold
    WHERE dt = (
        SELECT MAX(dt) FROM gold
        WHERE dt &gt;= DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -7 DAY), &#39;%Y-%m-%d&#39;)
    )
&quot;&quot;&quot;</code></pre>
<p>7일 안에서 가장 최신 파티션을 쓴다. ETL이 하루 빠져도 이틀 전 데이터라도 보여준다. 빈 응답보다 낫다.</p>
<hr>
<h2 id="사고-6-sagemaker-sdk-기본-버킷-문제">사고 6. SageMaker SDK 기본 버킷 문제</h2>
<p>Airflow에서 SageMaker 학습 job을 실행할 때 이런 에러가 났다.</p>
<pre><code>HeadBucket Error: An error occurred (403) when calling the HeadBucket operation
Bucket: sagemaker-eu-west-1-123456789012</code></pre><p>SageMaker SDK가 자동으로 <code>sagemaker-{region}-{account}</code> 버킷을 기본 버킷으로 쓰려 하는데, Airflow의 IRSA에는 그 버킷 접근 권한이 없었다.</p>
<pre><code class="language-python"># ❌ 기본 버킷 사용 (IRSA 권한 없음)
sagemaker.Session()

# ✅ 명시적 버킷 지정
sagemaker.Session(default_bucket=&quot;robot-telemetry-bucket&quot;)</code></pre>
<p>이것 말고도 <code>source_dir</code> 상대경로 문제도 있었다. Airflow worker pod의 현재 디렉토리가 <code>/opt/airflow</code>라서, DAG 파일 기준 상대경로가 맞지 않았다.</p>
<pre><code class="language-python"># ❌ 상대경로 (worker pod CWD 기준)
source_dir=&quot;src/ml&quot;

# ✅ 절대경로 (__file__ 기준 파생)
source_dir=str(Path(__file__).parent.parent / &quot;src&quot; / &quot;ml&quot;)</code></pre>
<p>SageMaker 관련 문제가 많았던 이유 중 하나가 Airflow worker pod에 SageMaker SDK를 전역으로 설치했던 거다. SageMaker SDK가 313MB라서, webserver/scheduler pod이 시작될 때마다 pip install이 돌았다. 콜드스타트 6분 → CrashLoop.</p>
<p>worker pod에만 격리해서 설치하는 방식으로 바꿨다.</p>
<pre><code class="language-yaml"># helm/airflow-values.yaml
workers:
  env:
    - name: _PIP_ADDITIONAL_REQUIREMENTS
      value: &quot;sagemaker boto3&quot;  # worker만

# webserver/scheduler에는 해당 없음</code></pre>
<hr>
<h2 id="비용-최적화--월-750-절감">비용 최적화 — 월 $750 절감</h2>
<p>이 프로젝트를 운영하면서 비용을 꽤 줄였다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>전</th>
<th>후</th>
<th>절감</th>
</tr>
</thead>
<tbody><tr>
<td>KDS Shard</td>
<td>4개 ($144)</td>
<td>2개 ($72)</td>
<td>$72</td>
</tr>
<tr>
<td>Athena 스캔</td>
<td>풀스캔</td>
<td>Partition Projection</td>
<td>~$280</td>
</tr>
<tr>
<td>EKS 노드</td>
<td>On-Demand</td>
<td>Spot (Karpenter)</td>
<td>~$150</td>
</tr>
<tr>
<td>S3 Bronze</td>
<td>90일 Standard</td>
<td>Glacier 전환</td>
<td>~$104</td>
</tr>
<tr>
<td><strong>합계</strong></td>
<td></td>
<td></td>
<td><strong>~$606/월</strong></td>
</tr>
</tbody></table>
<p>KDS Shard를 4개에서 2개로 줄인 건, 실제 트래픽을 측정해보니 1,000 rec/sec가 항상 나오는 게 아니었기 때문이다. 피크를 커버할 수 있는 최소한으로 줄였다. (나중에 1개까지 줄이는 실험도 했는데, 그게 위에 나온 Firehose flush stagger 사고로 이어졌다.)</p>
<p>Partition Projection이 가장 효과가 컸다. DDL에 파티션 범위를 수식으로 정의해두면 Athena가 S3 메타데이터 조회 없이 스캔할 파티션을 바로 계산한다.</p>
<pre><code class="language-sql">TBLPROPERTIES (
  &#39;projection.enabled&#39; = &#39;true&#39;,
  &#39;projection.dt.type&#39; = &#39;date&#39;,
  &#39;projection.dt.range&#39; = &#39;2026-01-01,NOW&#39;,
  &#39;projection.dt.format&#39; = &#39;yyyy-MM-dd&#39;,
  &#39;projection.dt.interval&#39; = &#39;1&#39;,
  &#39;projection.dt.interval.unit&#39; = &#39;DAYS&#39;
)</code></pre>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/b49aec7b-81f0-4874-bf81-9c213f4ef8a7/image.png" alt=""></p>
<hr>
<h2 id="이-프로젝트가-해커톤으로-이어진-것들">이 프로젝트가 해커톤으로 이어진 것들</h2>
<p>이 파이프라인을 만들면서 직접 써본 것들이 PRISM 해커톤에서 그대로 살아났다.</p>
<ul>
<li><strong>Bedrock 연동</strong> — 여기서 처음 써봤다. 해커톤에서 4 Agent + Supervisor 구조를 빠르게 잡을 수 있었던 건 이미 한 번 해봤기 때문이다.</li>
<li><strong>XGBoost 예측 모델</strong> — AI4I 2020 데이터셋으로 여기서 처음 학습시켰다. PRISM의 6-class 분류기의 베이스였다.</li>
<li><strong>결정론적 시연</strong> — 이 프로젝트에서 KDS 재생성 후 Lambda 고착 같은 사고를 경험하면서 &quot;시연 환경은 절대 외부 의존성을 최소화해야 한다&quot;는 걸 배웠다. PRISM의 LLM 캐시 replay 구조가 그 결과물이다.</li>
<li><strong>운영 가드레일</strong> — 여기서 터진 사고들이 그대로 CLAUDE.md 가드레일 19개가 됐다.</li>
</ul>
<p>학습 프로젝트로 시작했지만, 결국 해커톤 MVP의 기술적 토대가 됐다. 처음부터 대규모를 목표로 설계하지 않고, 작게 만들고 부딪히면서 키워나가는 방식이 맞았다고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로봇 1,000대 센서 데이터 파이프라인 구축기 3편 — 배치 ETL과 AI 서빙]]></title>
            <link>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-3%ED%8E%B8-%EB%B0%B0%EC%B9%98-ETL%EA%B3%BC-AI-%EC%84%9C%EB%B9%99</link>
            <guid>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-3%ED%8E%B8-%EB%B0%B0%EC%B9%98-ETL%EA%B3%BC-AI-%EC%84%9C%EB%B9%99</guid>
            <pubDate>Sat, 23 May 2026 08:54:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Flink가 실시간을 담당한다면, Airflow는 하루치 데이터를 정제하고 쌓는 역할이다.
그리고 그 위에 Bedrock AI 채팅과 SageMaker 예측 모델이 올라간다.</p>
</blockquote>
<hr>
<h2 id="medallion-architecture--bronze-silver-gold">Medallion Architecture — Bronze, Silver, Gold</h2>
<p>S3 Data Lake를 세 계층으로 나눴다. 처음엔 &quot;굳이 이렇게까지 해야 하나?&quot; 싶었는데, 운영하다 보니 이 구조가 없으면 나중에 엄청 골치 아파진다는 걸 알게 됐다.</p>
<pre><code>Bronze (원본)
  s3://bucket/bronze/year=2026/month=04/day=27/hour=12/
  → Firehose가 그대로 적재, 절대 수정 안 함
  → 90일 후 S3 Glacier로 이동

    ↓ Athena ETL (매일 자정)

Silver (정제)
  s3://bucket/silver/dt=2026-04-27/
  → 이상치 제거, 중복 제거, null 처리
  → 365일 보관

    ↓ Athena ETL (일 1회)

Gold (집계)
  s3://bucket/gold/dt=2026-04-27/
  → robot_id별 일일 집계값
  → 영구 보관 (분석 자산)</code></pre><p>Bronze를 절대 수정하지 않는 게 핵심이다. Silver/Gold 로직에 버그가 있어도 Bronze에서 다시 재처리하면 된다. 원본이 오염되면 복구가 불가능하다.</p>
<hr>
<h2 id="airflow--멱등성이-가장-중요한-설계-원칙">Airflow — 멱등성이 가장 중요한 설계 원칙</h2>
<p>Airflow DAG가 매일 자정에 돌면서 Bronze → Silver → Gold → Bedrock Report를 처리한다.</p>
<pre><code class="language-python">dag = DAG(
    &#39;robot_daily_etl&#39;,
    schedule_interval=&#39;0 0 * * *&#39;,
    catchup=False
)

quality_check &gt;&gt; bronze_to_silver &gt;&gt; silver_to_gold &gt;&gt; bedrock_report</code></pre>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/8a35d38e-c8ff-4029-84ab-bff4227cae99/image.png" alt=""></p>
<p>Airflow에서 가장 중요하게 생각했던 건 <strong>멱등성</strong>이었다. 같은 DAG를 두 번 실행해도 결과가 동일해야 한다.</p>
<p>초기에 이 쿼리를 썼다.</p>
<pre><code class="language-sql">-- ❌ 비멱등 (재실행하면 데이터 중복)
INSERT INTO TABLE silver_robot_telemetry
PARTITION (dt = &#39;2026-04-27&#39;)
SELECT ...</code></pre>
<p>재실행하면 같은 파티션에 데이터가 두 배로 쌓인다. 이걸 발견한 건 테스트 중에 DAG를 두 번 돌렸다가 집계값이 이상하게 나왔을 때였다.</p>
<pre><code class="language-sql">-- ✅ 멱등 (재실행해도 동일)
INSERT OVERWRITE TABLE silver_robot_telemetry
PARTITION (dt = &#39;2026-04-27&#39;)
SELECT ...</code></pre>
<p><code>OVERWRITE</code>를 쓰면 기존 파티션을 지우고 새로 쓴다. 몇 번을 실행해도 결과가 같다.</p>
<h3 id="bronze-→-silver-정제">Bronze → Silver 정제</h3>
<pre><code class="language-sql">INSERT OVERWRITE TABLE silver_robot_telemetry
PARTITION (dt = &#39;{{ ds }}&#39;)
SELECT
    robot_id, pos_x, pos_y, battery_level,
    motor_temp, current_load, timestamp
FROM bronze_robot_telemetry
WHERE year = YEAR(CAST(&#39;{{ ds }}&#39; AS DATE))
  AND month = MONTH(CAST(&#39;{{ ds }}&#39; AS DATE))
  AND day = DAY(CAST(&#39;{{ ds }}&#39; AS DATE))
  AND motor_temp &lt; 500          -- 이상치 제거
  AND battery_level BETWEEN 0 AND 100
  AND robot_id IS NOT NULL
QUALIFY ROW_NUMBER() OVER (
    PARTITION BY robot_id, timestamp ORDER BY robot_id
) = 1                           -- 중복 제거</code></pre>
<p>실제로 Bronze에는 같은 robot_id + timestamp 조합이 두 번 들어오는 경우가 있었다. Generator가 KDS 전송 실패 후 재시도할 때 생기는 중복이다. <code>ROW_NUMBER()</code>로 처음 것만 남긴다.</p>
<h3 id="silver-→-gold-집계">Silver → Gold 집계</h3>
<pre><code class="language-sql">INSERT OVERWRITE TABLE gold_robot_daily_stats
PARTITION (dt = &#39;{{ ds }}&#39;)
SELECT
    &#39;{{ ds }}&#39;                              AS dt,
    robot_id,
    AVG(motor_temp)                         AS avg_motor_temp,
    MAX(motor_temp)                         AS max_motor_temp,
    100 - MIN(battery_level)               AS battery_drain,
    COUNT(DISTINCT HOUR(timestamp))         AS active_hours
FROM silver_robot_telemetry
WHERE dt = &#39;{{ ds }}&#39;
GROUP BY dt, robot_id</code></pre>
<p>1,000대 × 86,400초 = 8,640만 레코드가 1,000행으로 압축된다. 이 Gold 테이블이 API 캐시와 AI 분석의 원천이다.</p>
<h3 id="great-expectations--데이터-품질-게이트">Great Expectations — 데이터 품질 게이트</h3>
<p>Bronze 데이터에 문제가 있으면 Silver/Gold까지 오염된다. ETL 시작 전에 검증 게이트를 뒀다.</p>
<pre><code class="language-python">검증 규칙:
- robot_id null 비율 &lt; 1%
- motor_temp: 0 ~ 500°C 범위
- battery_level: 0 ~ 100% 범위
- 레코드 수 &gt; 0</code></pre>
<p>검증 실패하면 DAG가 중단되고 Slack으로 알림이 간다. 처음에 이걸 귀찮아서 뺐다가, Generator 버그로 <code>motor_temp = 999</code> 데이터가 들어온 걸 Gold까지 올라간 뒤에야 발견했다. 그 이후로 절대 빼지 않는다.</p>
<hr>
<h2 id="fastapi-portal--in-memory-cache">FastAPI Portal + In-Memory Cache</h2>
<p>API 서버는 운영자가 쓰는 포털의 백엔드다. 가장 많이 쓰이는 기능이 <strong>AI 채팅</strong>이었다.</p>
<p>&quot;ROBOT-00042의 상태를 분석해줘&quot; 같은 질문을 보내면, 해당 로봇의 Gold 데이터를 가져와서 Bedrock Claude에게 전달하고 분석을 받아온다.</p>
<p>처음엔 채팅 요청이 올 때마다 Athena 쿼리를 날렸다.</p>
<pre><code>Athena 쿼리 흐름:
1. 쿼리 시작: ~1초
2. S3 스캔: 5~10초
3. 결과 직렬화: ~1초
→ 총 응답 시간: 10초 이상</code></pre><p>10초는 채팅 UX에서 너무 길다. Gold 데이터는 하루에 한 번만 갱신되는데, 매번 Athena를 쓰는 건 비효율적이었다.</p>
<p><strong>In-Memory Cache</strong>로 바꿨다.</p>
<pre><code class="language-python">_gold_cache: dict = {}      # {robot_id: {...stats}}
_cache_ready: bool = False

def refresh_cache():
    &quot;&quot;&quot;매일 01:00 KST에 Gold 데이터를 메모리에 로드&quot;&quot;&quot;
    global _gold_cache, _cache_ready
    _cache_ready = False

    df = athena_query(f&quot;SELECT * FROM gold WHERE dt = &#39;{yesterday}&#39;&quot;)
    _gold_cache = df.set_index(&#39;robot_id&#39;).to_dict(&#39;index&#39;)
    _cache_ready = True

# APScheduler로 매일 자동 갱신
scheduler.add_job(
    refresh_cache, &#39;cron&#39;,
    hour=1, minute=0,
    timezone=&#39;Asia/Seoul&#39;  # ← 타임존 명시 필수
)</code></pre>
<p>Gold 데이터 1,000행 × 6컬럼이 메모리에 올라가도 약 1MB다. 이걸 Dict로 들고 있으면 채팅 요청 때 캐시 조회가 1ms다.</p>
<p>타임존을 <code>Asia/Seoul</code>로 명시한 건 실수 한 번 하고 나서다. 처음엔 타임존 없이 <code>hour=1</code>로만 설정했더니 UTC 01:00(= KST 10:00)에 갱신되고 있었다. 운영자가 아침에 출근해서 &quot;왜 어제 데이터가 아직도 나와?&quot;라고 물어봤을 때 발견했다.</p>
<hr>
<h2 id="bedrock-ai-채팅">Bedrock AI 채팅</h2>
<pre><code class="language-python">@app.post(&quot;/api/chat&quot;)
async def chat(question: str):
    if not _cache_ready:
        raise HTTPException(503, &quot;캐시 로드 중입니다&quot;)

    # 캐시에서 즉시 조회
    robot_data = _gold_cache.get(robot_id_from_question(question))

    # Bedrock Claude 호출
    response = bedrock.invoke_model(
        modelId=&quot;anthropic.claude-3-haiku-20240307-v1:0&quot;,
        body=json.dumps({
            &quot;system&quot;: &quot;당신은 공장 로봇 정비팀의 기술 고문입니다. 로봇 ID는 반드시 [ROBOT-XXXXX] 형식으로 표기하세요.&quot;,
            &quot;messages&quot;: [{
                &quot;role&quot;: &quot;user&quot;,
                &quot;content&quot;: f&quot;데이터: {robot_data}\n질문: {question}&quot;
            }],
            &quot;max_tokens&quot;: 512
        })
    )

    answer = parse_response(response)

    # [ROBOT-XXXXX] 패턴 감지 → 딥링크 자동 생성
    links = extract_robot_links(answer)

    return {&quot;response&quot;: answer, &quot;links&quot;: links}</code></pre>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/320f528c-cf7b-4741-a8bb-3422b5a4f04e/image.png" alt=""></p>
<p>응답에서 <code>[ROBOT-00042]</code> 같은 패턴을 정규식으로 찾아내면, 해당 로봇의 Grafana 대시보드로 이동하는 딥링크 버튼을 자동으로 만든다. 운영자가 AI 답변을 보다가 특정 로봇이 언급되면 버튼 클릭 하나로 Grafana로 넘어간다.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/5f327ff1-660a-4085-a917-3b4fc92398bc/image.png" alt=""></p>
<p>XSS 방지를 위해 AI 응답을 HTML로 렌더링할 때 <code>DOMPurify</code>로 sanitize한다.</p>
<hr>
<h2 id="sagemaker-xgboost--예측-정비">SageMaker XGBoost — 예측 정비</h2>
<p>실시간 이상탐지는 지금 이상한 거 잡는 거고, SageMaker 모델은 <strong>미래에 고장날 것 같은 로봇을 미리 찾는</strong> 게 목적이다.</p>
<pre><code>입력: [avg_motor_temp, max_motor_temp, battery_drain, active_hours]
출력: 고장 확률 (0~1), 위험 등급 (low/medium/high)</code></pre><p>Gold 테이블 30일치 데이터를 학습 데이터로 쓰고, AI4I 2020 데이터셋의 <code>machine_failure</code> 레이블을 정답으로 사용한다. 주간 자동 재학습으로 모델이 최신 데이터를 반영하도록 했다.</p>
<pre><code class="language-python"># dags/weekly_ml_retrain.py (매주 월요일)
if execution_date.weekday() == 0:
    train_model(lookback_days=30)
    # 이전 모델보다 성능 높으면 배포, 아니면 유지</code></pre>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/66d997b7-e7ae-4402-9d20-2c51ecb716cb/image.png" alt=""></p>
<p>여기서 꽤 골치 아픈 문제들이 있었는데 — SageMaker SDK 버전, IRSA 권한, Airflow worker pod 설정 등 — 전부 4편에서 정리한다.</p>
<hr>
<h2 id="grafana-대시보드">Grafana 대시보드</h2>
<p>Grafana는 Athena와 CloudWatch를 데이터소스로 연결해서 세 가지 대시보드를 운영했다.</p>
<ol>
<li><strong>Fleet Status</strong> — 1,000대 전체 상태 (온도, 배터리, 가동 시간)</li>
<li><strong>Anomaly Timeline</strong> — 이상 이벤트 발생 패턴 시계열</li>
<li><strong>Pipeline Health</strong> — KDS throughput, Firehose 적재율, Airflow 성공률</li>
</ol>
<p>Anomaly Timeline 패널에서 처음에 짧은 시간 윈도우(5분)를 썼다가 데이터가 이상하게 보여서 한참 헤맸던 사고가 있다. Firehose의 multi-shard flush stagger 때문이었는데, 이것도 4편에서 자세히 다룬다. 진단에만 1시간 넘게 썼다.</p>
<hr>
<p>4편에서는 이 프로젝트를 운영하면서 실제로 터진 사고들을 정리한다. 코드보다 운영에서 배운 게 훨씬 많았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로봇 1,000대 센서 데이터 파이프라인 구축기 2편 — 수집과 실시간 이상탐지
]]></title>
            <link>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-2%ED%8E%B8-%EC%88%98%EC%A7%91%EA%B3%BC-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9D%B4%EC%83%81%ED%83%90%EC%A7%80</link>
            <guid>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-2%ED%8E%B8-%EC%88%98%EC%A7%91%EA%B3%BC-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9D%B4%EC%83%81%ED%83%90%EC%A7%80</guid>
            <pubDate>Sat, 23 May 2026 08:53:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>센서 데이터 1개가 Slack Alert이 되기까지 걸린 시간은 약 6초였다.
이 편에서는 Generator부터 Flink 이상탐지까지, 데이터가 실제로 흘러가는 경로를 따라간다.</p>
</blockquote>
<hr>
<h2 id="generator--1000개-코루틴">Generator — 1,000개 코루틴</h2>
<p>Generator는 가상 로봇 1,000대를 시뮬레이션하는 Python 프로세스다. K8s Deployment로 배포되고, 각 로봇마다 독립적인 asyncio 코루틴이 돌아간다.</p>
<pre><code class="language-python">async def robot_coroutine(robot_id: str):
    while True:
        record = {
            &quot;robot_id&quot;: robot_id,
            &quot;motor_temp&quot;: generate_temp(robot_id),
            &quot;battery_level&quot;: generate_battery(robot_id),
            &quot;current_load&quot;: generate_load(robot_id),
            &quot;timestamp&quot;: datetime.utcnow().isoformat()
        }
        await send_to_kds(record)
        await asyncio.sleep(1)  # 1초마다

# 1,000개 코루틴 동시 실행
tasks = [robot_coroutine(f&quot;ROBOT-{i:05d}&quot;) for i in range(1000)]
await asyncio.gather(*tasks)</code></pre>
<p>초당 1,000건이 KDS로 나간다. 실제로는 500건씩 묶어서 <code>put_records</code> 배치 호출을 한다. 500 × 2회/초 = 1,000 rec/sec.</p>
<p>여기서 중요한 설계 결정이 하나 있다. <strong>PartitionKey를 robot_id로 설정</strong>했다.</p>
<pre><code class="language-python">kinesis_client.put_records(
    StreamName=&quot;robot-telemetry-stream&quot;,
    Records=[{
        &quot;Data&quot;: json.dumps(record),
        &quot;PartitionKey&quot;: record[&quot;robot_id&quot;]  # ← 핵심
    }]
)</code></pre>
<p>같은 robot_id는 항상 같은 Shard로 라우팅된다. 덕분에 Flink에서 robot_id별 시계열 처리를 할 때 순서가 보장된다. 로봇 A의 데이터가 뒤섞여서 들어오면 5분 이동평균 계산이 틀어진다.</p>
<h3 id="glue-schema-registry">Glue Schema Registry</h3>
<p>KDS로 데이터를 보내기 전에 <strong>Glue Schema Registry</strong>로 스키마 검증을 한다. 필드 타입이 맞는지, 필수 필드가 빠지지 않았는지 확인한다.</p>
<p>처음엔 &quot;어차피 우리가 만드는 데이터인데 검증이 왜 필요해?&quot;라고 생각했다. 실제로 Generator 코드를 수정하다가 필드명을 <code>motorTemp</code>에서 <code>motor_temp</code>로 바꾼 적이 있었다. Schema Registry가 없었다면 Flink에서 조용히 null 처리되고 이상탐지가 통째로 멈췄을 거다.</p>
<hr>
<h2 id="kinesis-firehose--bronze-적재">Kinesis Firehose — Bronze 적재</h2>
<p>KDS에 들어온 데이터는 두 군데로 나간다.</p>
<ol>
<li><strong>Managed Flink</strong> — 실시간 이상탐지 (수백 ms)</li>
<li><strong>Kinesis Firehose</strong> — S3 Bronze 저장 (5분 배치)</li>
</ol>
<p>Firehose 설정에서 중요한 두 가지.</p>
<p><strong>Parquet 변환.</strong> JSON을 그대로 저장하면 S3 비용과 Athena 스캔 비용이 모두 올라간다. Firehose가 자동으로 JSON → Parquet(Snappy) 변환을 해준다. 실제로 같은 데이터 기준으로 JSON 대비 약 4분의 1 크기로 줄었다.</p>
<p><strong>Dynamic Partitioning.</strong> S3 경로를 자동으로 나눠준다.</p>
<pre><code>s3://bucket/bronze/
  year=2026/month=04/day=27/hour=12/
    &lt;UUID&gt;.parquet
  year=2026/month=04/day=27/hour=13/
    &lt;UUID&gt;.parquet</code></pre><p>나중에 Athena에서 쿼리할 때 <code>WHERE hour = 12</code> 조건을 주면 <code>hour=12</code> 폴더만 스캔한다. 이게 없으면 하루치 데이터 전부를 읽어야 한다.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/00048f70-114b-48da-a577-7806404395af/image.png" alt=""></p>
<hr>
<h2 id="managed-flink--실시간-이상탐지">Managed Flink — 실시간 이상탐지</h2>
<p>Flink 애플리케이션이 KDS에서 데이터를 읽어 두 가지 조건으로 이상을 탐지한다.</p>
<h3 id="조건-1-z-score-기반">조건 1: Z-Score 기반</h3>
<p>단순히 <code>motor_temp &gt; 90°C</code> 같은 고정 임계값을 쓰지 않은 이유가 있다.</p>
<p>로봇마다 정상 동작 온도가 다르다. 어떤 로봇은 평소에 80°C에서 안정적으로 돌고, 다른 로봇은 60°C가 기준이다. 80°C 로봇에게 &quot;90°C 넘으면 이상&quot;이라고 하면 진짜 이상과 정상 변동을 구분하기 어렵다.</p>
<p>그래서 <strong>robot_id별 5분 이동평균과 표준편차</strong>를 계산하고, 현재 값이 얼마나 벗어났는지를 Z-Score로 본다.</p>
<pre><code class="language-sql">-- Flink SQL: 5분 OVER Window로 통계 계산
SELECT
    robot_id,
    event_time,
    motor_temp,
    AVG(motor_temp) OVER (
        PARTITION BY robot_id
        ORDER BY event_time
        RANGE INTERVAL &#39;5&#39; MINUTE PRECEDING
    ) AS avg_temp,
    STDDEV(motor_temp) OVER (...) AS stddev_temp
FROM source_kds</code></pre>
<pre><code>Z = (현재값 - 5분평균) / 표준편차

ROBOT-00042 예시:
- 5분 평균: 75°C, 표준편차: 4.2°C
- 현재: 90°C
- Z = (90 - 75) / 4.2 = 3.57 → 이상 (Z &gt; 3.0)</code></pre><p><code>GREATEST(stddev_temp, 0.5)</code> 가드를 넣어둔 건, 표준편차가 0일 때(모든 값이 동일할 때) 나눗셈이 무한대가 되는 걸 막기 위해서다.</p>
<h3 id="조건-2-다변량-상관성-부하-대비-온도">조건 2: 다변량 상관성 (부하 대비 온도)</h3>
<p>Z-Score만으로는 잡지 못하는 케이스가 있다. 고부하 작업 중에 온도가 올라가는 건 정상이다. 그런데 <strong>부하가 낮은데 온도가 높은 건</strong> 모터 문제다.</p>
<pre><code class="language-sql">WHERE motor_temp &gt;= 85.0
  AND (motor_temp / GREATEST(current_load, 1.0)) &gt; 1.8</code></pre>
<p>두 조건 중 하나라도 참이면 이상으로 판정한다.</p>
<h3 id="watermark--late-data-처리">Watermark — Late Data 처리</h3>
<p>Event Time 기반 Window를 쓸 때 반드시 해결해야 하는 문제가 있다. <strong>Window가 언제 닫히는가?</strong></p>
<p>네트워크 지연으로 데이터가 늦게 도착할 수 있다. 12:05 Window가 닫혔는데 12:04:55 타임스탬프를 가진 데이터가 12:05:12에 도착하면 어떻게 해야 하는가?</p>
<pre><code class="language-sql">WATERMARK FOR event_time AS event_time - INTERVAL &#39;10&#39; SECOND</code></pre>
<p>&quot;10초 이상 늦은 데이터는 버린다&quot;고 선언하면, Window는 <code>window_end + 10초</code> 이후에 확정된다. 이게 없으면 Window가 영원히 열려 있어 메모리가 계속 쌓인다.</p>
<p>로봇 센서 데이터는 거의 실시간으로 오기 때문에 10초 허용이 적당했다.</p>
<h3 id="알람-집계--이중-sink">알람 집계 + 이중 Sink</h3>
<p>이상이 탐지되면 1분 Tumbling Window로 한 번 더 집계한다. 같은 로봇이 연속으로 이상 신호를 보내도 1분에 1번만 Alert가 나가게 하기 위해서다.</p>
<pre><code class="language-python"># Statement Set으로 두 Sink를 동일 트랜잭션에 실행
statement_set = t_env.create_statement_set()
statement_set.add_insert_into(&quot;sink_alert_kds&quot;, alert_events)  # Lambda 트리거
statement_set.add_insert_into(&quot;sink_s3_alerts&quot;, alert_events)  # 이력 보존
statement_set.execute()</code></pre>
<p>한쪽 Sink가 실패하면 둘 다 롤백된다. Exactly-Once 보장.</p>
<hr>
<h2 id="lambda-→-slack">Lambda → Slack</h2>
<p>Alert KDS에 데이터가 들어오면 Lambda가 자동으로 트리거된다.</p>
<pre><code class="language-python">def lambda_handler(event, context):
    for record in event[&#39;Records&#39;]:
        payload = json.loads(base64.b64decode(record[&#39;kinesis&#39;][&#39;data&#39;]))

        # SSM에서 포털 URL 런타임 조회 (배포 시점에 모름)
        portal_url = ssm.get_parameter(
            Name=&#39;/robot-telemetry/portal-url&#39;
        )[&#39;Parameter&#39;][&#39;Value&#39;]

        slack_message = f&quot;&quot;&quot;
⚠️ *이상 감지*
🤖 {payload[&#39;robot_id&#39;]}
🌡️ {payload[&#39;max_alert_temp&#39;]:.1f}°C
🔗 &lt;{portal_url}/?robot_id={payload[&#39;robot_id&#39;]}|포털에서 확인&gt;
        &quot;&quot;&quot;

        sns.publish(TopicArn=SNS_TOPIC_ARN, Message=slack_message)</code></pre>
<p>포털 URL을 코드에 하드코딩하지 않은 이유: ALB DNS는 EKS 배포 후에야 확정된다. 배포 시점에 모르는 값은 SSM Parameter Store에 런타임 조회로 처리하는 게 깔끔하다.</p>
<p>Slack Webhook URL도 <code>.env</code>가 아니라 AWS Secrets Manager에 저장해서 Lambda가 런타임에 읽어온다. 이걸 코드에 하드코딩했다가 한 번 사고가 났었는데, 그 이야기는 4편에서 한다.</p>
<hr>
<h2 id="전체-타임라인">전체 타임라인</h2>
<table>
<thead>
<tr>
<th>시각</th>
<th>주체</th>
<th>작업</th>
</tr>
</thead>
<tbody><tr>
<td>12:34:56</td>
<td>Generator</td>
<td>센서 데이터 생성</td>
</tr>
<tr>
<td>12:34:57</td>
<td>KDS</td>
<td>저장 완료</td>
</tr>
<tr>
<td>12:34:58</td>
<td>Flink</td>
<td>이상 탐지 (Z-Score 3.57)</td>
</tr>
<tr>
<td>12:35:00</td>
<td>Lambda</td>
<td>Alert KDS 소비, Slack 메시지 생성</td>
</tr>
<tr>
<td>12:35:02</td>
<td>Slack</td>
<td>알림 수신</td>
</tr>
</tbody></table>
<p><strong>총 6초.</strong> 센서에서 운영자 알림까지.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/6b5ab5fe-c2da-40a5-9001-6902a1899fe2/image.png" alt=""></p>
<hr>
<p>3편에서는 배치 ETL과 AI 서빙 레이어를 다룬다. Airflow로 Bronze → Gold를 만들고, Bedrock으로 AI 채팅을 붙이는 과정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로봇 1000대 센서 데이터 파이프라인 구축기 1편 — 설계와 기술 선택]]></title>
            <link>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-1%ED%8E%B8-%EC%84%A4%EA%B3%84%EC%99%80-%EA%B8%B0%EC%88%A0-%EC%84%A0%ED%83%9D</link>
            <guid>https://velog.io/@mason_dev/%EB%A1%9C%EB%B4%87-1000%EB%8C%80-%EC%84%BC%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0-1%ED%8E%B8-%EC%84%A4%EA%B3%84%EC%99%80-%EA%B8%B0%EC%88%A0-%EC%84%A0%ED%83%9D</guid>
            <pubDate>Sat, 23 May 2026 08:46:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 글에서 데이콘 스마트 제조 AI 해커톤 PRISM을 소개했는데, 사실 그 프로젝트의 뼈대는 몇 달 전부터 혼자 만들던 미니 프로젝트에서 왔다. PRISM에서 Bedrock AI 연동, XGBoost 예측 모델, Kinesis 스트리밍을 비교적 빠르게 붙일 수 있었던 건 이미 이 프로젝트에서 한 번씩 다 겪어봤기 때문이다.</p>
</blockquote>
<hr>
<h2 id="시작하게-된-이유">시작하게 된 이유</h2>
<p>데이터 엔지니어링을 공부하면서 &quot;실제로 데이터가 흘러가는 시스템&quot;을 한 번이라도 직접 만들어보고 싶었다. 강의에서 배운 Kafka, Spark 같은 개념들이 막상 AWS 환경에서 어떻게 연결되는지가 잘 그려지지 않았다.</p>
<p>그래서 선택한 소재가 <strong>가상 로봇 fleet의 센서 데이터</strong>였다. AI4I 2020 생산 설비 고장 데이터셋(CC BY 4.0)을 기반으로, 로봇 수천 대가 실시간으로 센서 데이터를 보내는 상황을 시뮬레이션하는 거다.</p>
<p>처음엔 &quot;로봇 100대 정도면 되겠지&quot; 했는데, 결국 스케일 아웃을 거치면서 <strong>1,000대</strong>까지 늘어났다.</p>
<hr>
<h2 id="전체-구조">전체 구조</h2>
<pre><code>[IaC]          Terraform → VPC · EKS · KDS · IAM (코드 한 줄로 전체 인프라 재현)
                                    ↓
[STREAMING]    Generator(K8s) → KDS(4 Shard) → Flink(이상탐지) → Alert KDS → Lambda → Slack
                                    ↓ Firehose
[LAKEHOUSE]    S3 Bronze → S3 Silver → S3 Gold   (Medallion, Parquet + Glue Catalog)
                                    ↓ Airflow ETL
[BATCH]        Bronze→Silver(정제) → Silver→Gold(집계) + 주간 SageMaker ML 재학습
                                    ↓
[ANALYTICS]    Athena(Partition Projection) · SageMaker 예측 · Bedrock AI 채팅 · Grafana</code></pre><p>레이어를 <strong>5개</strong>로 나눴다. 데이터 엔지니어링에서 자주 다루는 5가지 영역을 이 프로젝트에 하나씩 매핑한 구조다.</p>
<ol>
<li><strong>IaC</strong> — Terraform으로 전체 인프라 코드화. <code>terraform apply</code> 한 번으로 VPC부터 EKS, KDS, IAM까지 재현 가능하게</li>
<li><strong>Streaming</strong> — Kinesis &amp; Flink로 실시간 수집 + 윈도우 기반 이상 탐지. Watermark로 Late Data까지 처리</li>
<li><strong>Lakehouse</strong> — Medallion Architecture(Bronze/Silver/Gold)로 원본 보존과 정제·집계를 레이어별로 분리</li>
<li><strong>Batch</strong> — Airflow가 멱등성 보장하며 일 단위 ETL 스케줄링 + 주간 SageMaker 모델 재학습</li>
<li><strong>Analytics</strong> — Athena Partition Projection으로 Ad-hoc 쿼리 비용 70% 절감, SageMaker 예측, Bedrock AI 채팅, Grafana 대시보드</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/8d647f1c-372a-4cc8-a360-dd680d5311be/image.png" alt=""></p>
<hr>
<h2 id="기술-선택에서-고민했던-것들">기술 선택에서 고민했던 것들</h2>
<h3 id="kinesis-vs-kafka">Kinesis vs Kafka</h3>
<p>처음엔 Kafka를 쓸까 했다. 더 많이 쓰이고 레퍼런스도 풍부하다. 근데 이 프로젝트는 AWS 위에서 돌아가고 관리 오버헤드를 최소화하는 게 목표였다. MSK(Managed Kafka)를 쓰면 되긴 하지만 설정이 생각보다 복잡하고, 비용도 예상보다 높았다.</p>
<p><strong>Kinesis Data Streams</strong>를 선택한 건 단순한 이유였다. AWS 네이티브라 IAM IRSA 연동이 깔끔하고, Firehose와의 연결이 자연스러웠다. Shard 개수만 잘 계산하면 처리량은 충분했다.</p>
<p>Shard 계산은 이렇게 했다.</p>
<pre><code>로봇 1,000대 × 1 rec/sec = 1,000 rec/sec
레코드 크기: ~200 bytes

KDS 제한:
- 처리량: 1,000 rec/sec per shard
- 대역폭: 1 MB/sec per shard

→ 처리량 기준: 1,000 ÷ 1,000 = 1 shard
→ 안정 마진 고려: 4 Shard로 시작</code></pre><p>4 Shard로 시작했다. 나중에 비용 최적화 단계에서 2 Shard까지 줄이는 작업을 하게 되는데, 그 과정에서 꽤 재미있는 사고가 터진다. (4편에서 다룬다)</p>
<h3 id="flink-vs-spark-streaming">Flink vs Spark Streaming</h3>
<p>실시간 이상 탐지에 Flink를 선택한 이유는 <strong>Event Time 처리</strong> 때문이었다.</p>
<p>Spark Structured Streaming도 Event Time을 지원하지만, Flink가 Watermark 개념이 더 명확하게 구현되어 있다. 네트워크 지연으로 늦게 도착한 데이터(Late Data)를 어떻게 처리할지 — 이걸 Watermark로 명시적으로 선언할 수 있다는 게 마음에 들었다.</p>
<pre><code class="language-sql">WATERMARK FOR event_time AS event_time - INTERVAL &#39;10&#39; SECOND</code></pre>
<p>&quot;10초 이상 늦게 도착한 데이터는 버린다&quot;고 명시하면, Window가 언제 닫히는지 결정론적으로 계산된다. 이게 없으면 Window가 영원히 열려있어서 상태(State)가 계속 쌓인다.</p>
<h3 id="athena-vs-emr-vs-spark">Athena vs EMR vs Spark</h3>
<p>일일 배치 ETL에 Athena를 쓴 건 솔직히 가장 쉬운 선택을 한 것이었다.</p>
<p>EMR이나 Spark를 쓰면 더 빠르고 유연하다. 근데 일일 배치는 속도가 크게 중요하지 않았다. Serverless라서 서버 관리가 없고, S3 Parquet과 Glue Data Catalog를 그대로 쓸 수 있었다.</p>
<p>문제는 비용이었다. 처음에 Athena 비용을 계산해보니</p>
<pre><code>일일 데이터량: ~21.6 GB (Parquet 압축 후)
Bronze 스캔: 21.6 GB × $6.25/TB = $0.135
Silver 스캔: 21.6 GB × $6.25/TB = $0.135
→ 월 $280 이상</code></pre><p>이걸 줄이는 핵심이 <strong>Partition Projection</strong>이었다. 파티션 메타데이터를 S3에서 읽지 않고 DDL에 수식으로 정의해두면, Athena가 필요한 파티션만 정확히 스캔한다. 실제로 적용하고 나서 스캔량이 약 70% 줄었다.</p>
<hr>
<h2 id="인프라-기반--terraform--eks--karpenter">인프라 기반 — Terraform + EKS + Karpenter</h2>
<p>전체 인프라는 Terraform으로 코드화했다. <code>terraform apply</code> 한 번으로 VPC부터 EKS, Kinesis, IAM까지 생성된다.</p>
<p>EKS 노드 오토스케일링은 <strong>Karpenter</strong>를 썼다. 기존 Cluster Autoscaler는 Pod 수를 조정할 뿐, 노드 타입까지 최적화하지 않는다. Karpenter는 Pod의 리소스 요구사항에 맞는 가장 저렴한 인스턴스를 자동으로 선택한다. Spot 인스턴스까지 활용하면 On-Demand 대비 70% 절감이 가능하다.</p>
<p><strong>IRSA(IAM Roles for Service Accounts)</strong>도 처음 도입해봤다. Pod에 AWS 자격증명을 <code>.env</code>로 박는 게 아니라, Kubernetes ServiceAccount에 IAM Role을 연결해서 임시 자격증명을 자동 주입하는 방식이다. Generator Pod이 KDS에 데이터를 보낼 때, 별도의 <code>AWS_ACCESS_KEY</code> 없이 IRSA 권한만으로 동작한다.</p>
<p>CI/CD는 GitHub Actions + OIDC로 구성했다. Actions에서 AWS 자격증명을 시크릿으로 관리하는 대신, OpenID Connect 토큰을 발급받아 AWS STS에서 임시 자격증명을 받는다. access key를 어딘가에 저장할 필요가 없어진다.</p>
<hr>
<h2 id="스케일-아웃의-흐름">스케일 아웃의 흐름</h2>
<p>이 프로젝트는 처음부터 1,000대를 목표로 한 게 아니었다.</p>
<pre><code>초기:  로봇 100대,  KDS 1 Shard
↓
중간:  로봇 500대,  KDS 2 Shard
↓
최종:  로봇 1,000대, KDS 4 Shard</code></pre><p>각 단계마다 병목이 다른 곳에서 나타났다. 처음엔 Generator의 asyncio 코루틴 수가 문제였고, 그 다음엔 KDS Shard throttle이 문제였고, 마지막엔 Firehose buffer 설정이 문제였다.</p>
<p>이 과정에서 생긴 사고들이 나중에 해커톤 PRISM의 안정성 설계에 직접적인 영감이 됐다. &quot;실제로 터져봐야 안다&quot;는 걸 몸으로 배웠다.</p>
<hr>
<p>2편에서는 데이터 수집부터 실시간 이상 탐지까지 — 센서 데이터 1개가 Slack Alert이 되는 6초의 여정을 따라간다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인생 첫 해커톤 후기 — 스마트  공정  AI 운영 시스템 만들면서 느낀 것들]]></title>
            <link>https://velog.io/@mason_dev/%EC%9D%B8%EC%83%9D-%EC%B2%AB-%ED%95%B4%EC%BB%A4%ED%86%A4-%ED%9B%84%EA%B8%B0-%EC%8A%A4%EB%A7%88%ED%8A%B8-%EA%B3%B5%EC%A0%95-AI-%EC%9A%B4%EC%98%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EB%A9%B4%EC%84%9C-%EB%8A%90%EB%82%80-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@mason_dev/%EC%9D%B8%EC%83%9D-%EC%B2%AB-%ED%95%B4%EC%BB%A4%ED%86%A4-%ED%9B%84%EA%B8%B0-%EC%8A%A4%EB%A7%88%ED%8A%B8-%EA%B3%B5%EC%A0%95-AI-%EC%9A%B4%EC%98%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EB%A9%B4%EC%84%9C-%EB%8A%90%EB%82%80-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Sat, 23 May 2026 07:48:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>데이콘 스마트 제조 AI 해커톤 본선 (2025.05.22)</strong> 참가 후기
인과추론 + Multi-Agent로 제조 현장 문제를 풀어본 이야기</p>
</blockquote>
<hr>
<h2 id="시작-전에">시작 전에</h2>
<p>해커톤이 끝났다.</p>
<p>사실 제조 공정 쪽은 완전 문외한이었다. 기계가 어떻게 돌아가는지도 몰랐고, OEE가 뭔지도 몰랐고, CNC 머신이 뭔지도 해커톤 준비 전까지는 솔직히 관심도 없었다. 그런데 몇 주를 이 도메인에 파묻히고 나니 신기하게도 제조 공정에서 AI가 어떤 역할을 해야 하는지에 대해 내 나름의 시각이 생겼다.</p>
<p>이 글은 화려한 수상 후기가 아니다. 오히려 개발 과정에서 *&quot;이게 왜 이렇게 복잡해졌지?&quot;* 를 반복하면서 어떤 판단을 했고, 무엇이 진짜 어려웠는지를 기록하고 싶었다.</p>
<p>특히 인프라 파이프라인보다 <strong>AI 두뇌 부분</strong> — Multi-Agent 협상 구조, 인과추론, Closed-Loop 학습 — 에 집중해서 정리해보려 한다.</p>
<hr>
<h2 id="뭘-만들었나">뭘 만들었나</h2>
<p><strong>PRISM</strong> <em>(Process Reasoning &amp; Intelligent Supervision for Manufacturing)</em></p>
<p>핵심 문제 정의는 이거였다.</p>
<blockquote>
<p>메이커스페이스나 중소 제조업체에서 <strong>1인 운영자</strong>가 공정 이상이 생기면 원인 분석에 <strong>1~2시간</strong>을 쓴다.
대형 MES 시스템을 쓰면 해결되지만 <strong>연간 1,000만 원 이상</strong>이다.
우리는 <strong>노트북 한 대, 월 2만 원</strong>으로 그 격차를 좁히는 걸 목표로 했다.</p>
</blockquote>
<p>그 수단이 <strong>Closed-Loop AI</strong> 였다.</p>
<pre><code>센서 통합 → 인과 RCA → Multi-Agent 협상 → 학습 자산화
         ↑___________________________________|</code></pre><p>&quot;Closed-Loop&quot;이라는 단어를 쓴 건, 단순히 이상 탐지하고 끝나는 게 아니라, <strong>발생한 이상이 다시 모델 개선으로 이어지는 순환</strong>을 만들었기 때문이다.</p>
<hr>
<h2 id="두뇌-설계-가장-많이-고민한-부분">두뇌 설계: 가장 많이 고민한 부분</h2>
<p>인프라(DuckDB, Streamlit, Docker)는 비교적 선택이 명확했다. 진짜 고민은 <strong>AI가 어떻게 &quot;생각&quot;하게 만들지</strong> 였다.</p>
<h3 id="1-인과추론을-선택한-이유">1. 인과추론을 선택한 이유</h3>
<p>처음에는 XGBoost 예측 결과를 보여주면 되지 않을까 생각했다. 근데 곰곰이 생각해보니 문제가 있었다.</p>
<p>XGBoost가 *&quot;공구 마모(TWF) 가능성 62%&quot;* 라고 말해줘도 운영자는 <strong>&quot;그래서 뭘 어쩌라고?&quot;</strong> 에서 멈춘다.</p>
<p>상관관계와 인과관계의 차이다. spindle_rpm이 높을 때 불량이 많이 나온다고 해서 spindle_rpm을 낮추면 불량이 줄어드는 게 아닐 수 있다. 운영자가 실제로 필요한 건 <strong>&quot;내가 어떤 조치를 취하면 불량이 줄어드느냐&quot;</strong> 다.</p>
<p>그래서 <a href="https://py-why.github.io/dowhy/">DoWhy</a> 라이브러리로 <strong>6-Node 인과 DAG</strong>를 구축했다.</p>
<pre><code>tool_age ──→ vibration_xyz ──→ dimension_dev ──→ DEFECT
tool_age ──→ thermal_drift ──→ dimension_dev
spindle_rpm → vibration_xyz
spindle_rpm → coolant_temp</code></pre><p><img src="https://velog.velcdn.com/images/mason_dev/post/2e08f1ae-f00b-445e-bd31-ab810c1a5132/image.png" alt=""></p>
<p>여기서 <strong>do-calculus 개입(intervention)</strong> 을 쓴다. <code>do(tool_age=0)</code> — 즉 *&quot;공구를 지금 교체했다고 가정하면&quot;* — 불량률이 얼마나 달라지는지 ATE(Average Treatment Effect)로 추정해서 운영자에게 보여준다.</p>
<p>모델 신뢰도도 Wright(1991) partial R²를 기반으로 <strong>σ_max 임계값</strong>을 설정했다.</p>
<table>
<thead>
<tr>
<th>σ_max</th>
<th>상태</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>&lt; 0.5</td>
<td>✅ robust</td>
<td>숨겨진 변수에 강건</td>
</tr>
<tr>
<td>&lt; 1.0</td>
<td>⚠️ moderate</td>
<td>주의 필요</td>
</tr>
<tr>
<td>≥ 1.0</td>
<td>❌ fragile</td>
<td>결과 신뢰 불가</td>
</tr>
</tbody></table>
<p>이걸 UI에 바로 노출하지 않고 <strong>&quot;🔍 자세히&quot; expander</strong> 를 달아서, 심사위원이 물어볼 경우에 대비해 DoWhy 반박 추정(refutation) 원문을 열람할 수 있게 했다.
<img src="https://velog.velcdn.com/images/mason_dev/post/7a84cfa7-a523-4ce0-8453-4bb708faf08d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/26d0ab19-86ce-403c-a1e8-d92a0032eb86/image.png" alt=""></p>
<p>*&quot;우리 모델이 완벽하다&quot;고 포장하지 않고 &quot;이 정도 불확실성이 있다&quot;는 걸 투명하게 보여주는 게 더 정직한 접근이라고 생각했다.*</p>
<hr>
<h3 id="2-multi-agent-협상-구조--왜-에이전트-4개인가">2. Multi-Agent 협상 구조 — 왜 에이전트 4개인가</h3>
<p>이상이 탐지됐을 때 어떤 조치를 취해야 하는지는 <strong>한 관점만으로는 결정하기 어렵다.</strong></p>
<table>
<thead>
<tr>
<th>Agent</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>🔬 Quality</td>
<td>불량률이 얼마나 올라가고 있는가</td>
</tr>
<tr>
<td>🦺 Safety</td>
<td>계속 운영 시 물리적 위험은 없는가</td>
</tr>
<tr>
<td>🔧 Equipment</td>
<td>기계 수명에 얼마나 영향을 주는가</td>
</tr>
<tr>
<td>📦 Production</td>
<td>멈추면 납기에 얼마나 타격인가</td>
</tr>
</tbody></table>
<p>이 네 관점은 <strong>자주 충돌</strong>한다. 생산 에이전트는 &quot;계속 가자&quot;고 하고, 안전 에이전트는 &quot;당장 멈춰야 한다&quot;고 한다.
<img src="https://velog.velcdn.com/images/mason_dev/post/457445cc-5247-4840-b0d5-d4cb09b85906/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/fda527bc-842f-431d-a581-75121facdd4b/image.png" alt=""></p>
<p><strong>Claude Sonnet을 Supervisor로, Haiku를 Domain Agent 4개로</strong> 쓴 이유가 여기 있다. Haiku가 각 도메인에서 빠르게 분석을 내놓으면, Supervisor가 이걸 종합해서 <strong>Single-Scalar Net Value (KRW)</strong> 로 결정을 내린다.</p>
<pre><code class="language-python">net_value_KRW = throughput_gain
              - α × defect_loss
              - β × safety_loss    # β default=1.0, 기준값 1억 KRW
              - γ × rul_loss</code></pre>
<p>안전 위반이 <strong>1억 원 페널티</strong>인 건 의도적이다. Streamlit 슬라이더로 β를 1.0 → 2.0으로 올리면 Supervisor 결정이 바뀌는 걸 <strong>실시간</strong>으로 보여줬다.</p>
<blockquote>
<p>&quot;AI가 어떤 가중치를 쓰는지 운영자가 직접 조정할 수 있어야 한다&quot;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/8af9c03a-e3cd-4e0d-94df-dc377bb1e260/image.png" alt=""></p>
<p>Hard-block 규칙도 있다. <code>safety.estop_required = True</code> 이면 해당 선택지의 net_value는 자동으로 <code>-∞</code> 가 된다. 어떤 상황에서도 비상정지가 필요한 경우에는 경제적 계산 이전에 차단된다.</p>
<hr>
<h3 id="3-closed-loop-학습--가장-까다로웠던-부분">3. Closed-Loop 학습 — 가장 까다로웠던 부분</h3>
<p>불량 #47이 실제 발생한 후, 그 패턴을 XGBoost 모델에 추가 학습시켜 <strong>accuracy 0.81 → 0.97</strong> 로 올라가는 걸 라이브로 보여주는 게 목표였다.</p>
<p>문제는 <strong>&quot;라이브&quot;와 &quot;결정론적&quot;이 충돌</strong>한다는 거였다.</p>
<p>시연 중에 랜덤 요소가 끼어들면 어떤 날은 0.97이 나오고 어떤 날은 0.94가 나온다. 해커톤 당일 평가자 앞에서 수치가 달라지면 안 됐다.</p>
<p>그래서 <strong>Triple Insurance</strong> 를 만들었다.</p>
<pre><code>① 시드 고정
   PYTHONHASHSEED=2026
   random.Random(2026)
   np.random.seed(2026)

② LLM 캐시 재생
   SHA256 해시 키 → JSONL 저장
   PRISM_MODE=demo → Bedrock 호출 0회

③ 영상 자동 fallback
   네트워크 장애 / 타임아웃 →
   사전 녹화 mp4 자동 전환</code></pre><p><code>PRISM_MODE=demo</code> 환경변수 하나로 이 모드가 활성화된다. D-1 Verification Gate 결과: <code>cache_hit_rate = 1.000</code>, <code>bedrock_tokens = 0</code>. <strong>완벽히 오프라인 재현 가능한 상태였다.</strong></p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/fc3d5381-84a6-4600-87d1-9c1e4bd8ec79/image.png" alt=""></p>
<hr>
<h2 id="개발하면서-제일-골치-아팠던-것들">개발하면서 제일 골치 아팠던 것들</h2>
<p>솔직하게 적는다.</p>
<h3 id="dowhy--networkx-버전-충돌">DoWhy + networkx 버전 충돌</h3>
<p>가장 예상 못한 복병이었다. DoWhy 0.8이 <code>nx.algorithms.d_separated</code>를 참조하는데, networkx 3.4+에서 이게 <code>nx.algorithms.d_separation.is_d_separator</code>로 이름이 바뀌었다. 에러 메시지도 직관적이지 않아서 처음엔 뭐가 문제인지 한참 헤맸다.</p>
<p>결국 사이트 패키지를 건드리지 않고 프로젝트 레벨에서 monkey-patch로 해결했다.</p>
<pre><code class="language-python">if not hasattr(nx.algorithms, &quot;d_separated&quot;):
    nx.algorithms.d_separated = nx.algorithms.d_separation.is_d_separator</code></pre>
<p>오픈소스 버전 호환성 문제는 항상 이런 식으로 찾아온다는 걸 다시 배웠다.</p>
<h3 id="narrative-drift">Narrative Drift</h3>
<p>코드가 바뀌면 <strong>발표 대본, 운영자 가이드, README, 시연 스크립트</strong> — 이 네 곳을 동시에 갱신해야 한다.</p>
<p>개발 막바지에 UI에는 &quot;OEE <strong>+32%p</strong>&quot;라고 나와 있고 발표 슬라이드에는 &quot;<strong>+35%</strong>&quot;가 남아있는 걸 발견했다. 수치가 다른 걸 심사위원이 잡아내면 신뢰도가 한 번에 무너진다.</p>
<p>마지막 이틀은 코드보다 <strong>문서 정합 작업에 더 많은 시간</strong>을 쓴 것 같다.</p>
<h3 id="streamlit-실행-흐름">Streamlit 실행 흐름</h3>
<p>버튼 클릭마다 전체 스크립트가 재실행되는 구조라, 마커 전환 로직을 어디에 두느냐에 따라 &quot;다음 마커&quot; 버튼이 클릭 이벤트를 먹어버리는 문제가 생겼다. CNC 스트림 루프를 <code>main()</code> 끝으로 옮기고서야 해결됐다.</p>
<hr>
<h2 id="이번-해커톤으로-늘어난-도메인-지식">이번 해커톤으로 늘어난 도메인 지식</h2>
<p>제조 공정을 거의 모르고 시작했는데, 어느 순간 이런 개념들이 자연스러워졌다.</p>
<p><strong>OEE (Overall Equipment Effectiveness)</strong>
세계 평균 약 0.60인데, 우리 시연은 0.34 → 0.67 시나리오를 구성했다. Nakajima 기준 절대값이라서 단순한 &quot;32%p 향상&quot;이 아니라 맥락이 있는 수치다.</p>
<p><strong>공구 마모 유형</strong>
TWF(Tool Wear), HDF(Heat Diffusion), PWF(Power Failure), OSF(Overstrain)... XGBoost를 6-class 분류기로 학습시키면서 각 고장 유형이 어떤 센서 패턴과 연결되는지를 이해하게 됐다.</p>
<p><strong>RUL (Remaining Useful Life)</strong>
설비 잔여 수명. 공구를 지금 교체할 것인지 나중에 교체할 것인지의 경제적 트레이드오프 계산에서 핵심 변수가 된다. <code>rul_hour_cost = ₩25,000/h</code> 를 기준값으로 쓴 게 이 맥락이다.</p>
<p>제조 도메인을 모른 채 &quot;AI를 적용하면 좋겠다&quot;로만 접근했다면, 이 수치들이 의미하는 바를 제대로 설계할 수 없었을 것이다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/e2f9814c-3358-4f5c-ac1a-fc67fef671f7/image.png" alt=""></p>
<h2 id="마치며">마치며</h2>
<p>첫 해커톤이라 잘 모르는 게 너무 많았다. 기획서를 처음 봤을 때 어디서부터 시작해야 할지도 막막했고, 인과추론이나 Multi-Agent 같은 개념을 실제 제품에 녹여내는 경험도 처음이었다.</p>
<p>그럼에도 이번에 제일 잘한 결정은 하나였다.</p>
<blockquote>
<p><strong>&quot;파이프라인보다 두뇌를 먼저 설계하자&quot;</strong></p>
</blockquote>
<p>데이터를 어떻게 흘릴지보다, <strong>AI가 어떤 논리로 판단하는지를 먼저 정의</strong>하니까 나머지가 그 구조에 맞게 자리를 찾았다. Closed-Loop라는 개념이 그 중심을 잡아줬다.</p>
<p>결과가 어떻든, 공장이라는 물리 세계에서 인과추론이 어떤 역할을 할 수 있는지를 스스로 설계하고 구현해본 경험은 오래 남을 것 같다.</p>
<p>마지막으로 해커톤에서 찍은 사진 몇개 첨부하고 긴글 읽어줘서 감사하고 계속 블로그 업데이트해보도록 노력해보겠다.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/b37dfa4a-8d14-4c57-8a16-afd4f70dc4ec/image.jpeg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/cc98be06-8e0c-4f32-a12d-5b4f3cbfe9ac/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka를 활용한 로컬 데이터파이프라인을 구성해보자]]></title>
            <link>https://velog.io/@mason_dev/Kafka%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EC%BB%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-%EA%B5%AC%EC%84%B1%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@mason_dev/Kafka%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EC%BB%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-%EA%B5%AC%EC%84%B1%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Fri, 24 Apr 2026 08:13:49 GMT</pubDate>
            <description><![CDATA[<p>데이터 파이프라인을 설계할 때 가장 경계해야 할 것은 &#39;목적 없는 도구의 맹목적인 도입&#39;이다. 특히 로컬 환경에서 수집한 데이터를 클라우드로 전송하는 하이브리드 아키텍처에서는 각 컴포넌트의 리소스 효율성과 책임 분리(Separation of Concerns)가 시스템의 안정성을 결정한다.</p>
<p>본 포스팅에서는 로컬 환경에서 로봇 센서 데이터를 수집 및 가공하고, Apache Kafka를 거쳐 최적화된 포워더(Vector)를 통해 AWS S3로 적재하는 엔드투엔드(End-to-End) 데이터 파이프라인의 구축 과정과 기술적 의사결정을 정리한다.</p>
<ol>
<li>설계 목표 및 파이프라인 아키텍처
본 아키텍처의 핵심은 데이터 형식에 따른 처리 채널의 분리와 <strong>경량화된 클라우드 전송(Egress)</strong>이다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/2304bd34-457b-4b90-b868-b25712e900f7/image.png" alt=""></p>
<p>(캡처 권장: 파이썬 소스부터 Fluent Bit, Kafka, Vector를 거쳐 S3까지 이어지는 전체 구조도)</p>
<p>파이프라인은 다음과 같은 논리적 흐름을 갖는다.</p>
<p>Source: log_gen.py를 통한 두 가지 형태(JSON, Text)의 로그 파일 생성.</p>
<p>Ingestion: Fluent Bit를 활용한 파일 Tailing 및 라우팅.</p>
<p>Transform: Logstash를 거쳐 비정형 텍스트를 구조화(Grok 파싱).</p>
<p>Messaging: KRaft 모드 기반의 다중 토픽 Kafka 클러스터.</p>
<p>Egress &amp; Storage: Vector를 통한 Kinesis Data Firehose 데이터 포워딩 및 최종 Amazon S3(JSON 형태) 적재.</p>
<ol start="2">
<li>Apache Kafka의 본질과 KRaft의 도입
Kafka는 단순한 메시지 큐가 아니라 데이터를 지연 없이 흘려보내는 이벤트 스트리밍 플랫폼이다. 데이터를 생산하는 프로듀서(Producer)와 이를 소비하는 컨슈머(Consumer) 간의 의존성을 완벽히 분리한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/2717552f-c107-4a9f-a958-d8e350846d71/image.png" alt=""></p>
<p>(캡처 권장: factory-json-topic과 factory-text-topic, 그리고 내부 상태 관리용 토픽들이 보이는 화면)</p>
<p>이번 아키텍처에서는 기존 Zookeeper 기반의 구성을 탈피하고 KRaft(Kafka Raft) 모드를 도입했다.
과거 수백만 개의 파티션 환경에서 컨트롤러 병목을 유발하던 Zookeeper 의존성을 제거함으로써, 메타데이터를 내부 로컬 로그로 관리하여 아키텍처를 단순화하고 로컬 컨테이너 리소스 점유율을 최적화했다.</p>
<ol start="3">
<li>수집 및 가공: Channel 분리와 Logstash Filter의 역할
모든 데이터를 동일한 파이프라인으로 처리하는 것은 비효율적이다. 본 설계에서는 원본 데이터의 구조화 여부에 따라 라우팅을 이원화했다.</li>
</ol>
<pre><code>[ LAYER ]          [ COMPONENT ]          [ PROTOCOL / ACTION ]            [ DATA FORMAT ]
====================================================================================================
SOURCE       :     Python (log_gen.py)  ----( Write to File )----&gt;  [ /sensor_logs/*.log ]
               |                                                                   |
               |                                                            ( Shared Volume )
               v                                                                   |
INGESTION    :     Fluent Bit (Agent)   &lt;---( Tailing File  )----------------------+
               |          |
               |          | [ CHANNEL A: DIRECT BYPASS(json) ]
               |          +--------------------------------------------( Produce )--+
               |          |                                                         |
               |          | [ CHANNEL B: PROXY ROUTE(text) ]                        |
               |          +----( Forward )----&gt; [ Logstash ] ----( Produce )--+     |
               |                                     |                        |     |
               v                                     v                        v     v
TRANSFORM    : (A: Bypass)  +--[ Grok    : Pattern Matching &amp; Extract Fields   ]--+ |
               |            +--[ Mutate  : Data Type Conversion (Str-&gt;Float)   ]--+ |
               |            +--[ Tagging : Add Metadata for Conditional Alerts ]--+ |
               |                                                              |     |
MESSAGING    :     Kafka / MSK (Broker) &lt;-------------------------------------+-----+
               |          |             ( Merge Structured &amp; Processed Data )
               |          |
               |          +----( Topic A: factory-json-topic ) &lt;--- [Channel A Result]
               |          +----( Topic B: factory-text-topic ) &lt;--- [Channel B Result]
               v                                                               |
               v                                                               |
                                                                        kafka connect / ui
                                                                                |
                                                                                v               
VISUALIZE    :     OpenSearch (DB)      &lt;---( Indexing &amp; Search )----&gt;  DASHBOARD (Kibana)
=================================================================================================
</code></pre><p>Channel A (직결): 이미 구조화된 JSON 데이터는 가공 레이어를 생략하고 즉시 factory-json-topic으로 전송하여 Latency를 최소화한다.</p>
<p>Channel B (가공): 비정형 Text 데이터는 Fluent Bit에서 HTTP(포트 5044)를 통해 Logstash로 전달된다.</p>
<p>이 과정에서 HTTP 통신 프로토콜 특성상 막대한 양의 네트워크 메타데이터(IP, 헤더, 버전 등)가 노이즈처럼 결합된다.
예시)</p>
<pre><code>{
    &quot;event&quot;:{
        &quot;original&quot;:&quot;[{\&quot;date\&quot;:1777005819.42045,\&quot;log\&quot;:\&quot;\\\&quot;[2026-04-24 13:43:38] ID=AI-FACTORY-001 |   TEMP:87.5 |   HUMI:42.1 |   STAT:RUNNING\\\&quot;\&quot;}]&quot;},
        &quot;host&quot;:{&quot;ip&quot;:&quot;172.19.0.3&quot;},
        &quot;user_agent&quot;:{&quot;original&quot;:&quot;Fluent-Bit&quot;},
        &quot;date&quot;:1.77700581942045E9,
        &quot;url&quot;:{
            &quot;path&quot;:&quot;/&quot;,
            &quot;domain&quot;:&quot;logstash&quot;,
            &quot;port&quot;:5044
        },
        &quot;http&quot;:{
            &quot;version&quot;:&quot;HTTP/1.1&quot;,
            &quot;method&quot;:&quot;POST&quot;,
            &quot;request&quot;:{
                &quot;body&quot;:{
                    &quot;bytes&quot;:&quot;124&quot;
                },
                &quot;mime_type&quot;:&quot;application/json&quot;
            }
        },
        &quot;log&quot;:&quot;\&quot;[2026-04-24 13:43:38] ID=AI-FACTORY-001 |   TEMP:87.5 |   HUMI:42.1 |   STAT:RUNNING\&quot;&quot;,&quot;@version&quot;:&quot;1&quot;,
        &quot;@timestamp&quot;:&quot;2026-04-24T04:43:40.412003029Z&quot;
    }</code></pre><p>이러한 오염된 데이터를 그대로 Kafka에 적재하면 하위 스토리지 비용 증가 및 쿼리 복잡도를 유발한다. 따라서 Logstash의 filter 블록 내에서 grok 패턴([%{TIMESTAMP_ISO8601}] ID=%{DATA}...)을 엄격하게 적용하여, 필요한 센서 지표만 추출한 정제된 구조화 데이터로 변환 후 factory-text-topic으로 발행했다.</p>
<ol start="4">
<li>클라우드 전송 아키텍처: 왜 Kafka Connect 대신 Vector인가?
로컬 Kafka에 모인 데이터를 AWS 클라우드(Firehose -&gt; S3)로 보내기 위해 초기에는 Kafka Connect를 고려했으나, 엔지니어링 관점에서 명확한 한계가 존재했다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/0c72e8c9-d2a4-43dc-a32e-fd52827a3e68/image.png" alt=""></p>
<p>Java 의존성이 높은 Kafka Connect는 무겁고, 플러그인 관리 및 API 기반 설정이 번거롭다. 이를 대체하기 위해 Datadog에서 개발한 Vector를 도입했다.</p>
<p>압도적인 경량화: Rust 기반으로 작성되어 메모리 사용량이 극히 적다. 로컬 개발 환경에서 매우 유리하다.</p>
<p>운영 복잡도 감소: 복잡한 커넥터 설치 없이, 단일 vector.yaml 파일 하나로 파이프라인(Kafka -&gt; Vector -&gt; Firehose) 구성이 완료된다.</p>
<ol start="5">
<li>데이터 적재 확인 및 파이프라인 검증
Vector를 통해 포워딩된 데이터는 AWS Kinesis Data Firehose의 버퍼링(크기/시간 조건)을 거쳐 최종적으로 S3 버킷에 JSON 형태로 안전하게 적재된다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/47ca5b32-3ce7-40f6-86d9-47b39f492d80/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/915f4c75-cd74-45be-bcc4-b63a4c4e1cd7/image.png" alt=""></p>
<p>로컬 디렉토리에서 시작된 단 한 줄의 텍스트 로그가 경량 에이전트, 스트리밍 허브, 포워더를 거쳐 클라우드 스토리지에 무손실 적재되는 전체 라이프사이클을 검증했다.</p>
<ol start="6">
<li>마치며
로컬과 클라우드를 잇는 하이브리드 스트리밍 아키텍처가 안정적으로 구축되었다. 불필요한 시스템(Zookeeper, Kafka Connect)을 도려내고, 각 계층에 가장 효율적인 도구(KRaft, Vector)를 배치하여 파이프라인의 체급을 낮추면서도 처리량은 극대화했다.</li>
</ol>
<p>S3에 적재된 JSON 데이터는 향후 Amazon Athena를 활용한 서버리스 SQL 쿼리 분석의 기반이 될 것이며, 실시간 탐지가 필요한 구간에는 Flink를 추가 배치하여 파이프라인을 확장해 나갈 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Troubleshooting] Athena & Glue: 메달리온 아키텍처 구축 시 데이터가 조회되지 않는 문제]]></title>
            <link>https://velog.io/@mason_dev/Troubleshooting-Athena-Glue-%EB%A9%94%EB%8B%AC%EB%A6%AC%EC%98%A8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B5%AC%EC%B6%95-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A1%B0%ED%9A%8C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@mason_dev/Troubleshooting-Athena-Glue-%EB%A9%94%EB%8B%AC%EB%A6%AC%EC%98%A8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B5%AC%EC%B6%95-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A1%B0%ED%9A%8C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Tue, 21 Apr 2026 08:03:01 GMT</pubDate>
            <description><![CDATA[<h2 id="1-배경">1. 배경</h2>
<p>데이터 레이크하우스 아키텍처인 <strong>메달리온 아키텍처(Medallion Architecture)</strong>를 구현하며, S3에 저장된 Raw 데이터를 <strong>Bronze 계층(Athena/Glue)</strong>으로 구조화하는 작업을 진행했습니다.</p>
<ul>
<li><strong>저장 포맷:</strong> Parquet (Snappy Compressed)</li>
<li><strong>S3 경로 구조:</strong> <code>s3://.../bronze/year=2026/month=04/day=21/hour=15/</code></li>
<li><strong>작업 내용:</strong> Glue Data Catalog에 테이블 스키마를 미리 생성한 후, Athena에서 <code>MSCK REPAIR TABLE</code> 명령어를 통해 S3 데이터를 로드하려 함.</li>
</ul>
<h2 id="2-문제-발생">2. 문제 발생</h2>
<p>S3에는 분명히 파티셔닝된 데이터가 존재하고, 테이블의 <code>LOCATION</code>도 올바르게 설정했음에도 불구하고 <strong>Athena에서 조회 시 결과가 빈 값(Empty Result)</strong>으로 나오는 현상이 발생했습니다.</p>
<h3 id="발견된-증상">발견된 증상:</h3>
<ol>
<li><code>MSCK REPAIR TABLE</code> 명령어를 실행해도 새로운 파티션이 추가되었다는 메시지가 뜨지 않음.</li>
<li>Glue 테이블 설정의 &#39;Partitions&#39; 탭이 비어 있음.</li>
<li>스키마에는 파티션 키(year, month, day, hour)가 설정되어 있으나, 실제 데이터와 연결되지 않음.</li>
</ol>
<h2 id="3-원인-분석">3. 원인 분석</h2>
<p>문제의 핵심은 <strong>&quot;파티션 키의 정의 순서와 S3 디렉토리 계층 구조의 불일치&quot;</strong>에 있었습니다.</p>
<h3 id="athena의-파티션-인식-원리">Athena의 파티션 인식 원리</h3>
<p>Athena의 <code>MSCK REPAIR TABLE</code> 명령어는 S3의 폴더 구조를 <strong>정해진 순서대로(Hierarchy)</strong> 타고 내려가며 탐색합니다.</p>
<ul>
<li><strong>실제 S3 구조:</strong> <code>year</code> -&gt; <code>month</code> -&gt; <code>day</code> -&gt; <code>hour</code></li>
<li><strong>문제의 기존 테이블 설정:</strong> <code>hour</code> (Partition 0) -&gt; <code>year</code> (Partition 1) -&gt; ...</li>
<li><strong>충돌 발생:</strong> Athena는 S3의 최상위 폴더가 <code>hour=XX</code>일 것이라고 예상했지만, 실제로는 <code>year=2026</code> 폴더가 먼저 나타나자 탐색을 중단하고 파티션을 인식하지 못한 것입니다.</li>
</ul>
<h2 id="4-해결-방법">4. 해결 방법</h2>
<p>파티션 키의 순서를 S3의 물리적 경로 순서와 일치하도록 수정했습니다.</p>
<h3 id="step-1-테이블-스키마-수정">[Step 1] 테이블 스키마 수정</h3>
<p>Glue Catalog에서 파티션 번호(Index)를 조정하여 S3 계층 구조와 일치시킵니다.</p>
<ul>
<li><strong>수정 전:</strong> <code>hour(0), year(1), month(2), day(3)</code></li>
<li><strong>수정 후:</strong> <code>year(0), month(1), day(2), hour(3)</code></li>
</ul>
<h3 id="step-2-athena-메타데이터-복구">[Step 2] Athena 메타데이터 복구</h3>
<p>순서가 교정된 상태에서 파티션을 다시 로드합니다.</p>
<pre><code class="language-sql">-- 1. 테이블의 위치 재설정 (확인용)
ALTER TABLE de_06_ai_ma_bronze_db.bronze_tbl 
SET LOCATION &#39;s3://de-ai-30-827913617635-ap-northeast-2-an/medallion/bronze/&#39;;

-- 2. 파티션 구조 재스캔 및 등록
MSCK REPAIR TABLE de_06_ai_ma_bronze_db.bronze_tbl;</code></pre>
<p><strong>결과 메시지:</strong></p>
<blockquote>
<p><code>Repair: Added partition to metastore bronze_tbl:year=2026/month=04/day=21/hour=15</code>
(성공적으로 메타데이터에 등록됨)
<img src="https://velog.velcdn.com/images/mason_dev/post/94962971-ff1a-4100-afe8-0afe85a87a4d/image.png" alt=""></p>
</blockquote>
<h2 id="5-요약-및-교훈">5. 요약 및 교훈</h2>
<ol>
<li><strong>계층 구조의 중요성:</strong> Athena/Glue에서 파티션 키를 설정할 때는 반드시 <strong>S3의 물리적 폴더 깊이 순서대로</strong> 정의해야 한다.</li>
<li><strong>MSCK REPAIR의 한계:</strong> 단순히 키 이름이 같다고 인식하는 것이 아니라, 순서가 틀리면 &quot;탐색 실패&quot;로 간주한다.</li>
<li><strong>데이터 포맷 명시:</strong> <code>STORED AS PARQUET</code>와 같은 포맷 설정이 실제 파일과 일치해야 하며, <code>SET LOCATION</code>은 메타데이터 주소만 바꿀 뿐 기존 파티션의 상세 경로까지 자동으로 갱신해주지 않으므로 주의해야 한다.</li>
</ol>
<h3 id="마무리">마무리</h3>
<p>메달리온 아키텍처의 첫 단추인 Bronze 계층 구축에서 흔히 하는 실수였지만, 이번 트러블슈팅을 통해 <strong>Glue 메타데이터와 S3 물리 저장소 간의 매핑 원리</strong>를 깊이 있게 이해할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Troubleshooting] AWS Athena HIVE_BAD_DATA: invalid magic number 오류 원인과 해결]]></title>
            <link>https://velog.io/@mason_dev/Troubleshooting-AWS-Athena-HIVEBADDATA-invalid-magic-number-%EC%98%A4%EB%A5%98-%EC%9B%90%EC%9D%B8%EA%B3%BC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@mason_dev/Troubleshooting-AWS-Athena-HIVEBADDATA-invalid-magic-number-%EC%98%A4%EB%A5%98-%EC%9B%90%EC%9D%B8%EA%B3%BC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 20 Apr 2026 02:03:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/mason_dev/post/d2349547-a18b-49c8-8ab9-89cd2b6ef696/image.png" alt=""></p>
<h3 id="-1-문제-상황-problem">### 1. 문제 상황 (Problem)</h3>
<p>ASAC 2기 데이터 엔지니어링 실습으로 S3를 Data Lake로 삼고 AWS Athena를 연동하는 기본 파이프라인을 구축하던 중 오류가 발생했습니다.</p>
<ul>
<li><strong>데이터 원본:</strong> S3 S3 <code>data/basic</code> 경로에 다중 JSON(dict 형태)이 나열된 <code>a.txt</code> 파일 업로드.</li>
<li><strong>테이블 설정:</strong> Athena 쿼리 편집기에서 S3 데이터를 기반으로 외부 테이블(<code>dummy_test_tbl</code>)을 생성.</li>
<li><strong>발생 에러:</strong> 테이블 생성 후 쿼리 실행 시 아래와 같은 치명적 오류(Fatal Error) 발생.</li>
</ul>
<blockquote>
<p>HIVE_BAD_DATA: Error reading field &#39;event_id&#39; at position X. <strong>invalid magic number</strong></p>
</blockquote>
<h3 id="-2-팩트-기반-원인-분석-root-cause">### 2. 팩트 기반 원인 분석 (Root Cause)</h3>
<p>이 에러는 S3에 적재된 실제 데이터의 포맷과 Athena(Glue Data Catalog)에 정의된 테이블 포맷 설정이 불일치할 때 발생합니다. </p>
<ul>
<li><strong>잘못된 가정:</strong> 테이블을 생성할 때, &#39;분석 효율을 위해 Parquet 포맷을 선택하면 Athena가 알아서 데이터를 Parquet 포맷으로 읽거나 변환해 줄 것&#39;이라는 착각.</li>
<li><strong>기술적 팩트 (Magic Number란?):</strong> 실제 S3에 있는 파일은 평문 텍스트 형식의 <code>JSON</code>입니다. 그러나 테이블 속성을 <code>PARQUET</code>로 지정하면, Athena의 쿼리 엔진은 파일을 읽을 때 파일의 맨 앞 4바이트에서 Parquet 포맷의 고유 식별자인 매직 넘버(<code>PAR1</code>)를 찾습니다.
  텍스트 파일인 JSON에는 이 바이너리 시그니처가 존재하지 않으므로 엔진이 데이터를 해석하지 못하고 즉시 <code>invalid magic number</code> 파싱 에러를 뱉어내는 것입니다. Athena는 쿼리 엔진일 뿐, 테이블 포맷을 바꾼다고 S3 S3 원본 데이터를 변환해주지 않습니다.</li>
</ul>
<h3 id="-3-논리적-해결-방법-solution">### 3. 논리적 해결 방법 (Solution)</h3>
<p>데이터의 수집(Raw)과 가공(Refined) 단계를 철저히 분리하여 접근해야 합니다.</p>
<p><strong>Step 1: Raw 데이터 테이블은 원본 포맷과 100% 일치하게 재생성</strong></p>
<ul>
<li>테이블의 파일 포맷을 <code>Parquet</code>가 아닌 원본 데이터와 동일한 <code>JSON</code>으로 정확히 지정하여 테이블을 다시 생성합니다. (내부적으로 OpenX JSON SerDe가 사용됩니다.)</li>
<li>이 테이블(<code>dummy_test_tbl_json</code>)을 쿼리하면 정상적으로 S3의 JSON 텍스트를 읽어옵니다.</li>
</ul>
<p><strong>Step 2: 분석을 위한 Parquet 테이블은 CTAS로 별도 생성 (ETL 처리)</strong></p>
<ul>
<li>JSON 포맷은 스캔 비용이 비싸고 성능이 떨어지므로, Parquet 포맷의 데이터 마트가 필요하다면 <strong>CTAS (Create Table As Select)</strong> 구문을 이용해 물리적 포맷 변환 작업을 수행해야 합니다.
```sql</li>
<li><ul>
<li>JSON 테이블의 데이터를 읽어 Parquet 포맷으로 S3 새 경로에 저장하는 ETL 로직
CREATE TABLE dummy_test_tbl_parquet
WITH (
format = &#39;PARQUET&#39;, 
external_location = &#39;s3://your-bucket/data/refined/&#39;
) AS
SELECT *
FROM dummy_test_tbl_json;<pre><code>![](https://velog.velcdn.com/images/mason_dev/post/23349b38-6b10-4107-aa00-f774c06cea84/image.png)

</code></pre></li>
</ul>
</li>
</ul>
<h3 id="-4-마치며">### 4. 마치며</h3>
<ul>
<li><strong>스키마 온 리드(Schema-on-Read):</strong> Athena는 데이터를 읽어들이는 시점에 스키마와 포맷을 적용합니다. 물리적인 Storage Format과 논리적인 DDL 포맷 정의가 어긋나면 즉각적인 <code>HIVE_BAD_DATA</code> 장애로 이어집니다.</li>
<li>설정 창 클릭 한 번으로 포맷이 바뀌는 것이 아닙니다. 파일의 구조와 엔진의 직렬화/역직렬화(SerDe) 과정에 대한 팩트를 정확히 이해하고 데이터 파이프라인을 설계해야 합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Troubleshooting] PyFlink 1.18 + AWS Managed Flink + KDS 연동 ]]></title>
            <link>https://velog.io/@mason_dev/Troubleshooting-PyFlink-1.18-AWS-Managed-Flink-KDS-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@mason_dev/Troubleshooting-PyFlink-1.18-AWS-Managed-Flink-KDS-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Thu, 16 Apr 2026 08:44:33 GMT</pubDate>
            <description><![CDATA[<h2 id="1-개요">1. 개요</h2>
<p>AWS Managed Service for Apache Flink(구 Kinesis Data Analytics) 환경에서 PyFlink를 활용해 Kinesis Data Streams(KDS)의 실시간 데이터를 집계하는 파이프라인을 구축하던 중 발생한 일련의 오류와 해결 과정을 기록한다.</p>
<hr>
<h2 id="2-문제-상황-1-python-버전과-라이브러리-호환성">2. 문제 상황 1: Python 버전과 라이브러리 호환성</h2>
<p>로컬 환경(Python 3.11)에서 <code>apache-flink==1.15.0</code> 설치 시도 중 <code>subprocess-exited-with-error</code> 발생.</p>
<h3 id="원인-분석"><strong>원인 분석</strong></h3>
<ul>
<li><strong>팩트:</strong> Apache Flink 1.15는 Python 3.10 이상의 환경을 공식 지원하지 않음. 특히 <code>apache-beam</code> 등 의존성 라이브러리가 Python 3.11의 빌드 구조와 충돌함.</li>
<li><strong>해결:</strong> Python 3.11 환경을 유지하기 위해 Flink 버전을 <strong>1.18.0</strong>으로 업그레이드하여 설치 진행.</li>
</ul>
<h2 id="3-문제-상황-2-모듈-참조-및-객체-생성-오류">3. 문제 상황 2: 모듈 참조 및 객체 생성 오류</h2>
<p>코드 실행 시 <code>ModuleNotFoundError: No module named &#39;flink&#39;</code> 및 생성자 호출 에러 발생.</p>
<h3 id="원인-분석-1"><strong>원인 분석</strong></h3>
<ul>
<li><strong>Import 경로:</strong> 패키지명은 <code>apache-flink</code>이지만, 실제 코드 내 임포트 경로는 <code>pyflink</code>임. <code>from flink.table</code>은 존재하지 않는 경로.</li>
<li><strong>객체 생성:</strong> <code>TableEnvironment</code>는 생성자를 직접 호출하지 않고 <code>.create()</code> 정적 메서드를 사용해야 함.</li>
</ul>
<h3 id="교정된-코드"><strong>교정된 코드</strong></h3>
<pre><code class="language-python">from pyflink.table import EnvironmentSettings, TableEnvironment

# 잘못된 방식: t_env = TableEnvironment(setting)
# 올바른 방식:
setting = EnvironmentSettings.new_instance().in_streaming_mode().build()
t_env = TableEnvironment.create(setting)</code></pre>
<hr>
<h2 id="4-문제-상황-3-kinesis-connector-인식-불가-jar-버전-mismatch">4. 문제 상황 3: Kinesis Connector 인식 불가 (JAR 버전 mismatch)</h2>
<p><code>CREATE TABLE</code> 구문에서 <code>&#39;connector&#39; = &#39;kinesis&#39;</code>를 사용했으나, Flink 엔진이 커넥터를 찾지 못함.</p>
<h3 id="원인-분석-2"><strong>원인 분석</strong></h3>
<ul>
<li><strong>팩트 1:</strong> Flink SQL을 쓰기 위해서는 일반 Connector가 아닌 <strong>SQL 전용 통합 JAR(Fat Jar)</strong>가 필요함.</li>
<li><strong>팩트 2:</strong> Flink 1.18 엔진에 1.15용 JAR를 사용하면 클래스 로딩 시 <code>NoSuchMethodError</code> 등 런타임 에러 발생 가능성이 매우 높음.</li>
<li><strong>해결:</strong> <code>flink-sql-connector-kinesis-4.2.0-1.18.jar</code> 파일로 교체.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/1b7ceba3-6211-4aa3-9e6a-d85853875653/image.png" alt=""></p>
<h2 id="5-문제-상황-4-aws-환경에서의-배포-및-권한-문제">5. 문제 상황 4: AWS 환경에서의 배포 및 권한 문제</h2>
<p>S3에 코드를 올리고 실행했으나 <code>RestHandlerException</code> 발생 및 데이터 수신 불가.</p>
<h3 id="원인-분석-3"><strong>원인 분석</strong></h3>
<ul>
<li><strong>EntryPoint 미지정:</strong> AWS Managed Flink는 ZIP 내의 어떤 파일이 메인인지 모름. <strong>&#39;런타임 속성(Runtime Properties)&#39;</strong> 설정 필수.</li>
<li><strong>IAM 권한 결여:</strong> 기본 생성된 역할에는 Kinesis 읽기/쓰기 권한이 빠져 있었음.</li>
</ul>
<h3 id="최종-설정-데이터"><strong>최종 설정 데이터</strong></h3>
<ol>
<li><strong>Runtime Properties:</strong><ul>
<li>Group ID: <code>kinesis.analytics.flink.run.options</code></li>
<li>Key: <code>python</code> / Value: <code>app.py</code></li>
<li>Key: <code>jarfile</code> / Value: <code>flink-sql-connector-kinesis-4.2.0-1.18.jar</code></li>
</ul>
</li>
<li><strong>IAM Policy:</strong> <code>kinesis:DescribeStream</code>, <code>kinesis:GetRecords</code>, <code>kinesis:PutRecord</code> 등 추가.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/576619f2-f514-4b28-a902-b475d0075f75/image.png" alt=""></p>
<hr>
<h2 id="6-결론-및-회고">6. 결론 및 회고</h2>
<ol>
<li><strong>버전 정합성:</strong> PyFlink는 Python, Flink 엔진, Connector JAR의 3박자 버전이 완벽히 맞아야 한다.</li>
<li><strong>환경의 차이:</strong> 로컬(Windows)과 배포(AWS Linux) 환경의 경로 체계 차이를 인지하고 하드코딩을 피해야 한다.</li>
<li><strong>권한 확인:</strong> &quot;시동이 안 걸리면 설정 문제, 달리다 멈추면 권한 문제&quot;라는 가설을 데이터(CloudWatch)로 증명하는 과정이 중요하다.</li>
</ol>
<hr>
<p><strong>Reference</strong></p>
<ul>
<li>Apache Flink Documentation (v1.18)</li>
<li>Maven Repository: flink-sql-connector-kinesis</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python 가상 로그 생성부터 AWS Firehose를 거쳐 S3 적재까지]]></title>
            <link>https://velog.io/@mason_dev/Python-%EA%B0%80%EC%83%81-%EB%A1%9C%EA%B7%B8-%EC%83%9D%EC%84%B1%EB%B6%80%ED%84%B0-AWS-Firehose%EB%A5%BC-%EA%B1%B0%EC%B3%90-S3-%EC%A0%81%EC%9E%AC%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@mason_dev/Python-%EA%B0%80%EC%83%81-%EB%A1%9C%EA%B7%B8-%EC%83%9D%EC%84%B1%EB%B6%80%ED%84%B0-AWS-Firehose%EB%A5%BC-%EA%B1%B0%EC%B3%90-S3-%EC%A0%81%EC%9E%AC%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Wed, 15 Apr 2026 08:41:56 GMT</pubDate>
            <description><![CDATA[<p>실시간 스트리밍 데이터를 다룰 때, 발생하는 모든 로그를 S3에 실시간으로 &#39;직접&#39; 꽂아 넣는 것은 안티 패턴(Anti-pattern)이다. S3에 자잘한 파일이 무수히 쌓이게 되면(Small File Problem), 추후 Athena나 Spark로 데이터를 읽어 들일 때 I/O 병목이 발생하고 탐색 비용이 폭발하기 때문이다. </p>
<p>이 문제를 해결하기 위해 중간에 <strong>버퍼(Buffer)</strong> 역할을 해주는 <strong>Amazon Data Firehose(ADF)</strong>를 배치한다. 오늘은 Python으로 가상의 로그를 생성하고, Firehose를 통해 일정한 조건(크기/시간)에 맞춰 S3에 덩어리(Batch) 단위로 적재하는 파이프라인을 구축해 본다.</p>
<h2 id="1-아키텍처-및-파이프라인-흐름">1. 아키텍처 및 파이프라인 흐름</h2>
<p>전체적인 데이터의 흐름은 다음과 같다.</p>
<ol>
<li><strong>Producer:</strong> Python <code>Faker</code> 라이브러리를 이용해 금융/이커머스 등 가상 도메인 로그 생성</li>
<li><strong>Delivery:</strong> Boto3 (AWS SDK)를 통해 Firehose 스트림으로 데이터 전송 (<code>put_record</code>)</li>
<li><strong>Buffering &amp; Sink:</strong> Firehose가 지정된 버퍼 조건(예: 1MiB or 60초)을 채운 뒤 S3에 파일로 저장</li>
</ol>
<hr>
<h2 id="2-가짜-데이터-생성-및-직렬화-log-generator">2. 가짜 데이터 생성 및 직렬화 (Log Generator)</h2>
<p>데이터 파이프라인 테스트를 위해 가장 먼저 할 일은 현실성 있는 데이터를 만드는 것이다. <code>Faker</code> 라이브러리를 활용해 금융(Finance) 트랜잭션 로그를 생성하는 클래스를 구성한다.</p>
<pre><code class="language-python"># log_generator.py (일부 발췌)
from faker import Faker
from datetime import datetime
import random

fake = Faker(&#39;ko_KR&#39;)

class LogGenerator:
    def finance(self):
        return {
            &quot;timestamp&quot; : datetime.now().isoformat(),
            &quot;industry&quot;  : &quot;FINANCE&quot;,
            &quot;user_id&quot;   : fake.uuid4()[:12],
            &quot;transaction&quot; : random.choice([&#39;이체&#39;,&#39;출금&#39;,&#39;입금&#39;,&#39;결제&#39;]),
            &quot;amount&quot; : round(random.uniform(1000, 1000000), -2),
            &quot;status&quot; : random.choices([&quot;SUCCESS&quot;, &quot;FAIL&quot;], weights=[0.95, 0.05])[0]
        }</code></pre>
<p>이 객체 형태의 데이터를 AWS로 전송하려면 <strong>문자열 형태(JSON str)</strong>로 변환하는 직렬화(Serialization) 과정이 필수적이다. </p>
<pre><code class="language-python"># run.py
import json

def make_one_log():
    # ensure_ascii=False 옵션으로 한글 깨짐 방지
    return json.dumps(log_gen.finance(), ensure_ascii=False) </code></pre>
<hr>
<h2 id="3-boto3를-이용한-aws-firehose-연결-인증">3. Boto3를 이용한 AWS Firehose 연결 (인증)</h2>
<p>이제 Python 스크립트가 AWS 인프라에 접근하기 위한 &#39;통행증&#39;을 발급받아야 한다. <code>boto3</code> 라이브러리를 사용해 Firehose 클라이언트를 생성한다.</p>
<p>여기서 가장 중요한 것은 <strong>코드가 실행되는 환경에 따른 인증 방식의 분리</strong>다. 하드코딩된 자격 증명(Access Key)은 보안 사고의 주범이므로 절대 피해야 한다.</p>
<pre><code class="language-python"># adf_direct_data_put.py
import boto3

REGION = &#39;ap-northeast-2&#39;

def get_client(service_name=&#39;firehose&#39;, is_in_aws=True): 
    # 1. 로컬 환경 (is_in_aws=False)
    # 로컬에 설정된 aws configure 프로필이나 환경변수를 통해 인증
    if not is_in_aws:
        session = boto3.Session(region_name=REGION)
        return session.client(service_name)    

    # 2. AWS 내부 환경 (is_in_aws=True)
    # EC2, CloudShell 등 IAM Role이 부여된 환경에서는 키 입력 없이 자동 인증
    return boto3.client(service_name, region_name=REGION)

# 로컬에서 테스트할 경우 get_client(&#39;firehose&#39;, False) 로 호출
firehose = get_client() </code></pre>
<hr>
<h2 id="4-데이터-전송-put_record">4. 데이터 전송 (<code>put_record</code>)</h2>
<p>클라이언트 객체가 생성되었으면, 직렬화된 로그 데이터를 Firehose Delivery Stream으로 쏜다.</p>
<pre><code class="language-python">def send_log():
    response = firehose.put_record(
        DeliveryStreamName=&#39;de-ai-06-an2-kdf-log-to-s3&#39;,
        Record={
            # 주의: 데이터의 끝을 알리는 개행문자(\n)를 반드시 추가해야 함
            &#39;Data&#39;: make_one_log() + &quot;\n&quot; 
        }
    )
    print(f&#39;전송결과 : {response}&#39;) # HTTP 200이면 정상</code></pre>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/c50a1d7f-f4a8-4896-9533-65ec002c0261/image.png" alt=""></p>
<ul>
<li><strong>엔지니어링 포인트:</strong> S3에 적재된 파일을 나중에 읽어 들일 때, 각 로그 객체가 줄바꿈으로 구분되어 있어야 (JSON Lines 형태) 파싱 에러가 나지 않는다. 따라서 <code>&#39;Data&#39;: make_one_log() + &quot;\n&quot;</code> 처리는 사소하지만 매우 중요한 부분이다.</li>
</ul>
<hr>
<h2 id="5-firehose의-핵심-버퍼링-size--interval">5. Firehose의 핵심: 버퍼링 (Size &amp; Interval)</h2>
<p>Firehose가 받은 데이터를 즉시 S3에 넣지 않는다고 했다. 그렇다면 언제 넣을까? 
AWS 콘솔에서 Firehose를 생성할 때 설정한 <strong>Buffer size(크기)</strong>와 <strong>Buffer interval(시간)</strong> 조건에 따른다.</p>
<ul>
<li><strong>Buffer Size (예: 1MiB):</strong> 전송된 데이터가 버퍼 메모리 내에 1 메가바이트만큼 쌓이면 S3로 Flush(전송)한다.</li>
<li><strong>Buffer Interval (예: 60초):</strong> 데이터가 용량을 다 채우지 못했더라도, 첫 데이터가 들어온 지 60초가 경과하면 S3로 Flush한다.</li>
</ul>
<p><strong>&quot;둘 중 하나라도 먼저 조건을 만족하면(Whichever happens first)&quot;</strong> S3로 데이터가 압축되어(또는 원본 그대로) 업로드된다. 이를 통해 파일의 크기를 적절히 키워 S3에 저장함으로써 다운스트림(Downstream)에서의 데이터 처리 효율을 극대화할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TroubleShooting] PythonOperator TypeError 및 MySQL IntegrityError]]></title>
            <link>https://velog.io/@mason_dev/TroubleShooting-PythonOperator-TypeError-%EB%B0%8F-MySQL-IntegrityError</link>
            <guid>https://velog.io/@mason_dev/TroubleShooting-PythonOperator-TypeError-%EB%B0%8F-MySQL-IntegrityError</guid>
            <pubDate>Tue, 14 Apr 2026 03:00:41 GMT</pubDate>
            <description><![CDATA[<p>Airflow를 활용해 신용 평가 데이터를 DB에 적재하는 파이프라인을 구축하던 중 발생한 두 가지 주요 에러와 해결 과정을 정리한다.</p>
<h3 id="1-typeerror-string-indices-must-be-integers-not-str">1. TypeError: string indices must be integers, not &#39;str&#39;</h3>
<p><strong>문제 상황</strong>
신용 평가 API 호출 결과를 파싱하여 DB 적재 함수(<code>_load_users_credit</code>)로 전달하는 과정에서 발생했다. <code>data[&#39;user_id&#39;]</code>와 같이 딕셔너리 키로 접근하려 했으나 에러가 발생하며 태스크가 실패했다.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/9f4ebbde-d3f3-41db-a9f9-c77f1d44a0bf/image.png" alt=""></p>
<p><strong>원인 분석</strong>
에러 메시지는 <code>data</code> 변수가 딕셔너리가 아닌 <strong>문자열(String)</strong>임을 가리키고 있었다. </p>
<ul>
<li><strong>가정:</strong> API 응답이나 이전 태스크에서 넘어온 값이 JSON 객체(dict)일 것이라 판단했다.</li>
<li><strong>팩트:</strong> 실제 데이터는 역직렬화(Deserialization)되지 않은 Raw String 상태였다. Python에서 문자열에 대괄호(<code>[]</code>)로 접근하면 인덱스(정수)를 기대하지만, 문자열 키를 넣었기 때문에 발생한 문법 오류였다.</li>
</ul>
<p><strong>해결 방법</strong>
데이터를 사용하는 시점에서 <code>json.loads()</code>를 통해 딕셔너리로 변환하거나, 데이터를 넘겨주는 시점에서 파이프라인의 데이터 직렬화 상태를 점검하여 객체 타입으로 전달되도록 수정했다.</p>
<hr>
<h3 id="2-mysqldbintegrityerror-duplicate-entry-for-key-primary">2. MySQLdb.IntegrityError: Duplicate entry for key PRIMARY</h3>
<p><strong>문제 상황</strong>
데이터 타입 이슈 해결 후, 실제 DB에 데이터를 <code>INSERT</code> 하는 과정에서 발생했다. 특정 유저 아이디(<code>C001</code>)가 이미 테이블의 기본키(Primary Key)로 존재하여 중복 삽입이 불가능하다는 에러였다.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/47463e70-f89a-46dd-83df-201a7bb67d18/image.png" alt=""></p>
<p><strong>원인 분석</strong></p>
<ul>
<li><strong>설계 결함:</strong> 해당 DAG는 주기적으로 신용 점수를 업데이트해야 한다. 하지만 초기 코드는 단순 <code>INSERT</code> 쿼리로 작성되어 있어, 이미 데이터가 존재하는 유저의 경우 제약 조건 위반으로 처리가 중단되었다.</li>
<li><strong>테이블 구조:</strong> <code>customers</code> 테이블의 PK가 <code>user_id</code>로 설정되어 있어, 동일 유저에 대한 다중 레코드 삽입이 차단된 상태였다.</li>
</ul>
<hr>
<h3 id="3-on-duplicate-key-update를-활용한-해결">3. ON DUPLICATE KEY UPDATE를 활용한 해결</h3>
<p>단순히 에러를 회피하는 것이 아니라, 비즈니스 로직에 맞게 <strong>Upsert(Update + Insert)</strong> 방식을 채택하기로 했다.</p>
<p><strong>수정된 SQL 로직</strong></p>
<pre><code class="language-sql">INSERT INTO customers (user_id, credit_score, grade)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE
    credit_score = VALUES(credit_score),
    grade = VALUES(grade);</code></pre>
<p><strong>결과 및 기대 효과</strong></p>
<ol>
<li><strong>멱등성(Idempotency) 확보:</strong> 동일한 DAG를 여러 번 실행해도 에러 없이 최신 데이터로 유지된다.</li>
<li><strong>효율성:</strong> 데이터 존재 여부를 먼저 조회(SELECT)하고 조건문으로 분기할 필요 없이, DB 레벨에서 한 번의 쿼리로 삽입과 갱신을 동시에 처리한다.</li>
</ol>
<hr>
<h3 id="4-마치며">4. 마치며</h3>
<p>이번 이슈를 통해 두 가지 교훈을 얻었다. 
첫째, 데이터의 타입을 맹신하지 말고 항상 로그를 통해 객체의 형상을 확인해야 한다. 
둘째, DB 적재 로직을 설계할 때는 해당 테이블이 이력(History) 관리용인지, 현재 상태(Master) 관리용인지를 명확히 구분하여 적절한 쿼리 전략을 세워야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TroubleShooting] 동일한 dag_id로 여러가지의 dag를 만들었을 때 문제 상황]]></title>
            <link>https://velog.io/@mason_dev/TroubleShooting-%EB%8F%99%EC%9D%BC%ED%95%9C-dagid%EB%A1%9C-%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80%EC%9D%98-dag%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%97%88%EC%9D%84-%EB%95%8C-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@mason_dev/TroubleShooting-%EB%8F%99%EC%9D%BC%ED%95%9C-dagid%EB%A1%9C-%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80%EC%9D%98-dag%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%97%88%EC%9D%84-%EB%95%8C-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 09 Apr 2026 08:35:21 GMT</pubDate>
            <description><![CDATA[<h2 id="issue-background">Issue Background</h2>
<p>최근 스마트팩토리 온도 센서 데이터를 MySQL로 적재하는 ETL 파이프라인 실습을 진행하며 겪은 치명적인 오류와 그 해결 과정을 공유합니다.</p>
<p>비슷한 형태의 파이프라인 여러 개를 테스트하기 위해 코드를 여러 파이썬 파일(<code>file_A.py</code>, <code>file_B.py</code>)로 나누어 작성했습니다. 이때, <strong>&quot;동일한 <code>dag_id</code>를 부여하면 AWS의 로드밸런서(ELB)처럼 스케줄러가 여러 DAG를 라운드로빈 방식으로 분산 처리해 주지 않을까?&quot;</strong>라는 가설을 세웠습니다.</p>
<p>하지만 결과는 처참한 파이프라인 붕괴였습니다.</p>
<h2 id="symptoms-발생한-증상">Symptoms (발생한 증상)</h2>
<p>대시보드와 실행 로그에서 다음과 같은 기괴한 현상들이 발생했습니다.</p>
<ol>
<li><strong>대시보드의 깜빡임(Flickering) 현상:</strong> 웹 UI에서 새로고침(F5)을 누를 때마다 DAG의 구조나 태스크 목록이 계속 다른 파일의 형태로 바뀌어 보였습니다.</li>
<li><strong>Task Failed 및 무한 Retry 지옥:</strong> 워커가 태스크를 실행하려다 말고 갑자기 <code>Task not found in DAG</code>라는 에러를 뱉으며 실패했습니다. Airflow는 재시도(Retry)를 지시하지만, 영원히 성공하지 못하고 상태가 꼬여버렸습니다.</li>
</ol>
<h2 id="root-cause-analysis-원인-분석">Root Cause Analysis (원인 분석)</h2>
<p>결론부터 말하자면, 저의 가설은 <strong>Airflow의 시스템 아키텍처를 오판한 치명적인 착각</strong>이었습니다. Airflow는 DAG 단위로 로드밸런싱을 수행하는 네트워크 도구가 아닙니다.</p>
<p>이 사태의 근본적인 원인은 <strong>&#39;메타데이터 DB의 고유키 충돌&#39;</strong>과 <strong>&#39;스케줄러의 파싱(Parsing) 방식&#39;</strong>에 있었습니다.</p>
<h3 id="1-dag_id는-데이터베이스의-primary-key고유키다">1. <code>dag_id</code>는 데이터베이스의 Primary Key(고유키)다</h3>
<p>Airflow의 모든 설정과 상태는 메타데이터 DB(Postgres, MySQL 등)에 저장됩니다. 이때 <code>dag</code> 테이블에서 <code>dag_id</code>는 무조건 고유해야 하는 Primary Key(PK)입니다. 
동일한 식별자를 가진 파일이 여러 개 존재한다는 것은, 분산 처리가 아니라 <strong>데이터 충돌(Collision)</strong>을 의미합니다.</p>
<h3 id="2-scheduler의-덮어쓰기overwrite-경합-race-condition">2. Scheduler의 덮어쓰기(Overwrite) 경합 (Race Condition)</h3>
<p>Airflow 스케줄러는 백그라운드에서 <code>dags/</code> 폴더 내의 파일들을 주기적으로 순회하며 파싱(Parsing)합니다.</p>
<ul>
<li><strong>T1 시점:</strong> 스케줄러가 <code>file_A.py</code>를 파싱합니다. DB의 <code>dag_id=&quot;my_etl&quot;</code> 정보가 <code>file_A</code>의 구조로 <strong>덮어써집니다.</strong> (UI에는 A가 보임)</li>
<li><strong>T2 시점:</strong> 몇 초 뒤, 스케줄러가 <code>file_B.py</code>를 파싱합니다. DB의 <code>my_etl</code> 정보가 <code>file_B</code>의 구조로 다시 <strong>덮어써집니다.</strong> (새로고침하면 UI에 B가 보임)</li>
</ul>
<p>즉, 로드밸런싱이 되는 것이 아니라 스케줄러가 파싱할 때마다 <strong>DB 레코드가 엎치락뒤치락하며 덮어쓰기 전쟁</strong>을 하고 있었던 것입니다.</p>
<h3 id="3-고아-태스크orphan-task의-발생">3. 고아 태스크(Orphan Task)의 발생</h3>
<p>이 상태에서 Worker가 <code>file_A</code>의 <code>task_1</code>을 실행하려는데, 찰나의 순간 스케줄러가 <code>file_B</code>를 파싱해버렸다고 가정해 봅시다. 
DB 상의 DAG 구조는 <code>file_B</code>로 바뀌었으므로, Worker는 자신이 실행해야 할 <code>task_1</code>을 DB에서 찾지 못합니다. 그 결과 <strong>&quot;DAG 안에 해당 Task가 존재하지 않는다&quot;</strong>며 비정상 종료(Failed) 처리되는 것입니다.</p>
<h2 id="solution--action-item-해결책">Solution &amp; Action Item (해결책)</h2>
<p>시스템의 원리를 이해했다면 해결책은 간단합니다.</p>
<p><strong>1. 파일별로 독립적인 <code>dag_id</code> 부여</strong>
각 파이썬 파일마다 반드시 고유한 <code>dag_id</code>를 사용해야 합니다. (예: <code>05_mysql_etl_sensor_A</code>, <code>05_mysql_etl_sensor_B</code>)</p>
<p><strong>2. Dynamic DAG (동적 DAG) 활용</strong>
만약 수십 개의 센서에서 들어오는 데이터를 동일한 로직으로 처리해야 해서 하드코딩이 어렵다면, 하나의 파이썬 파일 내에서 <code>for</code> 문을 활용해 <strong>동적 DAG</strong>를 생성하는 방식을 채택해야 합니다.</p>
<pre><code class="language-python"># Dynamic DAG 생성 예시
sensors = [&#39;sensor_A&#39;, &#39;sensor_B&#39;, &#39;sensor_C&#39;]

for sensor in sensors:
    dag_id = f&quot;05_mysql_etl_{sensor}&quot;

    with DAG(dag_id=dag_id, ...) as dag:
        # 태스크 정의
        # ...

    # 글로벌 네임스페이스에 DAG 등록 (Airflow가 인식하게 함)
    globals()[dag_id] = dag</code></pre>
<h2 id="📝-conclusion-마치며">📝 Conclusion (마치며)</h2>
<p>이번 트러블슈팅을 통해 Airflow의 스케줄러가 파일을 파싱하는 루프 방식과, 메타데이터 DB가 전체 시스템의 중심에서 어떻게 작동하는지 명확히 깨달았습니다.
<img src="https://velog.velcdn.com/images/mason_dev/post/248729b0-d9a4-4b9b-b741-08d6e281bcba/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(TroubleShooting)airflow webserver 컨테이너 재시작시 발생하는 오류에 대해]]></title>
            <link>https://velog.io/@mason_dev/TroubleShootingairflow-webserver-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%9E%AC%EC%8B%9C%EC%9E%91%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%98%A4%EB%A5%98%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@mason_dev/TroubleShootingairflow-webserver-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%9E%AC%EC%8B%9C%EC%9E%91%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%98%A4%EB%A5%98%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Thu, 09 Apr 2026 08:27:56 GMT</pubDate>
            <description><![CDATA[<p>Airflow 웹서버 컨테이너를 재시작했는데 아래와 같은 에러가 발생했다.</p>
<pre><code>Error: Already running on PID 42 
(or pid file &#39;/opt/airflow/airflow-webserver.pid&#39; is stale)
```![](https://velog.velcdn.com/images/mason_dev/post/258df9f9-be95-4bf3-bddb-4e7d1b6e4729/image.png)


그리고 `localhost:8080` 접속이 되지 않는 상황.

---

## 📌 문제 원인 분석

Airflow는 웹서버 실행 시 다음 경로에 PID 파일을 생성한다.
</code></pre><p>/opt/airflow/airflow-webserver.pid</p>
<pre><code>
이 파일에는 실행 중인 프로세스 ID(PID)가 기록된다.

하지만 아래와 같은 상황이 발생하면:

* 컨테이너 강제 종료
* `kill -9`
* 비정상 종료
* Docker 재시작 중 충돌

👉 **PID 파일이 삭제되지 않고 남게 된다.**

그 결과 Airflow는:

&gt; “이미 실행 중이다”라고 잘못 판단하고 종료한다.

이를 **stale PID file 문제**라고 한다.

---

## 구조 이해 (추천 이미지)

velog에 아래와 같은 구조 다이어그램 하나 넣으면 이해도가 확 올라간다.

### 📷 추천 이미지 1: Airflow PID 동작 구조
</code></pre><p>[ airflow webserver 실행 ]
            ↓
[ airflow-webserver.pid 생성 ]
            ↓
[ 비정상 종료 ]
            ↓
[ pid 파일은 남아있음 ]
            ↓
[ 재실행 시 충돌 발생 ]</p>
<pre><code>
(다이어그램 툴: draw.io / Excalidraw 추천)

---

# 해결 방법 1 — 컨테이너 내부에서 직접 해결

### 1️⃣ 실행 중인 컨테이너 확인

```bash
docker ps</code></pre><hr>
<h3 id="2️⃣-컨테이너-접속">2️⃣ 컨테이너 접속</h3>
<pre><code class="language-bash">docker exec -it &lt;webserver_container_name&gt; bash</code></pre>
<hr>
<h3 id="3️⃣-pid-파일-삭제">3️⃣ PID 파일 삭제</h3>
<pre><code class="language-bash">rm -f /opt/airflow/airflow-webserver.pid</code></pre>
<p>확인:</p>
<pre><code class="language-bash">ls /opt/airflow | grep pid</code></pre>
<hr>
<h3 id="4️⃣-웹서버-재실행">4️⃣ 웹서버 재실행</h3>
<pre><code class="language-bash">airflow webserver</code></pre>
<p>또는 (Airflow 2.x)</p>
<pre><code class="language-bash">airflow webserver --port 8080</code></pre>
<hr>
<h1 id="해결-방법-2--docker-compose-사용-시-권장">해결 방법 2 — docker-compose 사용 시 (권장)</h1>
<p>Airflow를 docker-compose로 실행 중이라면 통째로 재기동하는 것이 더 깔끔하다.</p>
<pre><code class="language-bash">docker-compose down
docker-compose up -d</code></pre>
<p>그래도 해결되지 않으면:</p>
<pre><code class="language-bash">docker-compose down -v
docker-compose up -d</code></pre>
<p>⚠ <code>-v</code> 옵션은 볼륨 삭제 → DB 초기화됨</p>
<hr>
<h1 id="추가-확인-실제로-프로세스가-살아있는지-체크">추가 확인 (실제로 프로세스가 살아있는지 체크)</h1>
<p>혹시 진짜 웹서버 프로세스가 떠 있는 경우도 있으니 확인:</p>
<pre><code class="language-bash">ps -ef | grep airflow</code></pre>
<p>실행 중이라면 kill 후 재시작.</p>
<hr>
<h1 id="실무-정리">실무 정리</h1>
<table>
<thead>
<tr>
<th>상황</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>90% 케이스</td>
<td>PID 파일 삭제</td>
</tr>
<tr>
<td>docker-compose 사용</td>
<td>down → up -d</td>
</tr>
<tr>
<td>여전히 안 됨</td>
<td>logs 확인</td>
</tr>
</tbody></table>
<pre><code class="language-bash">docker logs &lt;webserver_container_name&gt;</code></pre>
<hr>
<h1 id="근본적-예방-방법">근본적 예방 방법</h1>
<ol>
<li>컨테이너 강제 종료 지양</li>
<li><code>docker stop</code> 사용</li>
<li>운영 환경에서는 systemd 또는 supervisor 사용</li>
<li>healthcheck 설정 고려</li>
</ol>
<hr>
<h1 id="핵심-한-줄-정리">핵심 한 줄 정리</h1>
<blockquote>
<p>Airflow <code>Already running on PID</code> 오류는
대부분 stale PID 파일 때문이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apache Airflow에 대해 알아보자]]></title>
            <link>https://velog.io/@mason_dev/Apache-Airflow%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@mason_dev/Apache-Airflow%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 08 Apr 2026 07:59:53 GMT</pubDate>
            <description><![CDATA[<p>데이터 파이프라인을 구축하고 운영하다 보면, 수많은 작업(Task)들의 순서를 제어하고 실패 시 재시도 처리 등을 관리해야 하는 상황에 직면하게 됩니다. 기존에는 운영체제의 <code>Cron</code>을 사용하여 스케줄링을 처리하는 경우가 많았으나, 작업 간의 복잡한 의존성을 관리하거나 모니터링하기에는 한계가 명확했습니다.</p>
<p>이러한 문제를 해결하기 위해 에어비앤비(Airbnb)에서 개발하여 오픈소스로 공개한 워크플로우 관리 플랫폼이 바로 <strong>Apache Airflow</strong>입니다. 오늘은 Airflow의 핵심 개념과 내부 아키텍처에 대해 정리해 봅니다.</p>
<hr>
<h3 id="1-apache-airflow란">1. Apache Airflow란?</h3>
<p>Airflow는 복잡한 데이터 파이프라인을 파이썬 코드로 작성하고, 스케줄링 및 모니터링할 수 있는 플랫폼입니다. &quot;Configuration as Code&quot; 원칙을 따르기 때문에, 모든 워크플로우를 파이썬 스크립트로 정의할 수 있어 버전 관리 및 유지보수에 매우 유리합니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/03ddca8a-7abf-4abc-9742-cbbe1e766685/image.png" alt=""></p>
</blockquote>
<h4 id="주요-특징">주요 특징</h4>
<ul>
<li><strong>동적 파이프라인</strong>: 파이썬 코드를 이용해 동적으로 파이프라인을 구성할 수 있습니다.</li>
<li><strong>확장성</strong>: 다양한 시스템(AWS, GCP, Hadoop 등)과 연동할 수 있는 수많은 플러그인과 오퍼레이터를 지원합니다.</li>
<li><strong>우수한 UI</strong>: 작업의 상태, 로그, 실행 시간 등을 웹 대시보드에서 직관적으로 파악할 수 있습니다.</li>
</ul>
<hr>
<h3 id="2-핵심-개념">2. 핵심 개념</h3>
<p>Airflow를 다루기 위해 반드시 알아야 할 3가지 핵심 요소가 있습니다.</p>
<h4 id="dag-directed-acyclic-graph">DAG (Directed Acyclic Graph)</h4>
<p>Airflow에서 워크플로우를 구성하는 가장 기본이 되는 단위입니다. 수학적 개념인 &#39;방향성 비순환 그래프&#39;를 의미하며, 작업들이 실행되는 순서와 의존성을 정의하지만 <strong>절대 순환(Loop) 구조를 가지지 않는 것</strong>이 특징입니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/a6b40836-0fbb-4ab5-bf85-e6e40103acf7/image.png" alt=""></p>
</blockquote>
<h4 id="operator와-task">Operator와 Task</h4>
<ul>
<li><strong>Operator (오퍼레이터)</strong>: 수행하고자 하는 작업의 &#39;템플릿&#39; 또는 &#39;클래스&#39;입니다. 어떤 작업을 할 것인지(예: 파이썬 함수 실행, Bash 명령어 실행 등)를 정의합니다. (ex. <code>BashOperator</code>, <code>PythonOperator</code>)</li>
<li><strong>Task (태스크)</strong>: 오퍼레이터가 DAG 내에서 인스턴스화되어 <strong>실제 실행을 위해 구체화된 객체</strong>입니다. DAG를 구성하는 하나의 노드(Node)가 됩니다.</li>
</ul>
<hr>
<h3 id="3-airflow-내부-아키텍처">3. Airflow 내부 아키텍처</h3>
<p>Airflow가 어떻게 수많은 스케줄을 누락 없이 관리하고 실행하는지 이해하려면 내부 아키텍처를 살펴보아야 합니다. Airflow는 단일 프로그램이 아니라, 여러 컴포넌트가 유기적으로 통신하는 분산 시스템에 가깝습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/db18032a-6561-47bf-8391-9d3564ce5843/image.png" alt=""></p>
</blockquote>
<h4 id="주요-컴포넌트">주요 컴포넌트</h4>
<ol>
<li><strong>Scheduler (스케줄러)</strong>: 가장 핵심적인 데몬입니다. 주기적으로 DAG 폴더를 파싱하여 예약된 작업이 있는지 확인하고, 실행 조건이 충족된 Task를 실행 큐(Queue)에 넣습니다.</li>
<li><strong>Executor (익스큐터)</strong>: 큐에 들어온 Task를 어떻게 실행할지 결정하는 메커니즘입니다. 로컬에서 프로세스를 띄울지(LocalExecutor), Celery를 통해 분산 워커로 보낼지(CeleryExecutor), Kubernetes Pod으로 실행할지(KubernetesExecutor)를 결정합니다.</li>
<li><strong>Worker (워커)</strong>: Executor의 명령을 받아 실제 Task 작업을 수행하는 프로세스입니다.</li>
<li><strong>Metadata Database</strong>: DAG의 정의, 현재 상태, Task의 실행 이력 등 Airflow의 모든 상태 정보를 저장하는 RDBMS(PostgreSQL, MySQL 등)입니다.</li>
<li><strong>Web Server</strong>: Metadata DB에 저장된 상태 정보를 읽어와 사용자에게 웹 UI로 제공합니다.</li>
</ol>
<hr>
<h3 id="4-task의-생명-주기-lifecycle">4. Task의 생명 주기 (Lifecycle)</h3>
<p>스케줄러에 의해 Task가 실행될 때, Task는 여러 상태(State)를 거치게 됩니다. 이 상태 변화를 이해해야 파이프라인 디버깅이 수월해집니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/e4c6f820-f79a-4363-b536-5a9e7ad58d3b/image.png" alt=""></p>
</blockquote>
<ul>
<li><strong>No status (None)</strong>: 스케줄러가 인지하기 전의 초기 상태</li>
<li><strong>Scheduled</strong>: 스케줄러가 실행 조건이 충족되었다고 판단한 상태</li>
<li><strong>Queued</strong>: Executor에게 실행을 요청하여 큐에 대기 중인 상태</li>
<li><strong>Running</strong>: Worker가 Task를 할당받아 실제로 실행 중인 상태</li>
<li><strong>Success / Failed</strong>: 작업이 정상적으로 완료되었거나 오류로 종료된 상태</li>
</ul>
<hr>
<h3 id="5-강력한-모니터링-ui">5. 강력한 모니터링 UI</h3>
<p>작성한 코드(DAG)가 의도대로 동작하는지, 병목 현상이 발생하는 구간은 없는지 파악하는 것은 데이터 엔지니어링의 핵심입니다. Airflow는 이를 위해 다양한 View를 제공합니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/17893ec0-2e07-4dd7-a560-06383f128697/image.png" alt=""></p>
</blockquote>
<ul>
<li><strong>Grid View</strong>: 과거의 실행 이력(DAG Runs)을 그리드 형태로 배열하여, 특정 날짜에 어떤 Task가 실패했는지 직관적으로 파악할 수 있습니다.</li>
<li><strong>Gantt Chart</strong>: 작업들의 병렬 처리 상태와 소요 시간을 확인하여 파이프라인의 성능을 최적화(Tuning)할 때 유용합니다.</li>
</ul>
<hr>
<h3 id="마무리">마무리</h3>
<p>Airflow는 코드로 인프라와 워크플로우를 관리한다는 점에서 강력한 유연성을 제공합니다. 단, 구조가 복잡하여 러닝 커브가 존재하고 초기 세팅(Metadata DB, 메세지 브로커 등)에 비용이 든다는 점은 고려해야 합니다. 다음 포스팅에선 vscode에서 직접 DAG를 생성해보기 위해 docker-compose.yaml 파일를 이용하여 docker에 여러개의 컨테이너를 생성해 airflow에 필요한 각각의 프로그램을 설치해 실습해보겠습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB 서브넷 격리 아키텍처: Multi-AZ 복제 및 VPC Endpoint의 구조 ]]></title>
            <link>https://velog.io/@mason_dev/DB-%EC%84%9C%EB%B8%8C%EB%84%B7-%EA%B2%A9%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-Multi-AZ-%EB%B3%B5%EC%A0%9C-%EB%B0%8F-VPC-Endpoint%EC%9D%98-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@mason_dev/DB-%EC%84%9C%EB%B8%8C%EB%84%B7-%EA%B2%A9%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-Multi-AZ-%EB%B3%B5%EC%A0%9C-%EB%B0%8F-VPC-Endpoint%EC%9D%98-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Thu, 02 Apr 2026 13:13:14 GMT</pubDate>
            <description><![CDATA[<h2 id="0-backgrounds">0. Backgrounds</h2>
<p>인프라 설계 시 보안 강화를 위해 DB 서브넷을 외부 인터넷과 완전히 격리(<strong>Isolated</strong>)하는 아키텍처를 채택하곤 합니다. 그러나 이 과정에서 고가용성(HA)을 위한 데이터 동기화 메커니즘과 네트워크 엔드포인트의 역할을 혼동하여 불필요한 설계를 추가하는 실수가 빈번히 발생합니다.</p>
<ul>
<li><strong>핵심 의문:</strong> &quot;Multi-AZ 환경에서 가용 영역 간 DB 데이터 동기화를 위해 별도의 VPC Endpoint나 네트워크 피어링 설정이 필요한가?&quot;</li>
<li><strong>결론:</strong> <strong>그렇지 않습니다.</strong> 데이터 복제는 사용자가 제어하는 라우팅 영역 밖에서 이루어집니다.</li>
</ul>
<hr>
<h2 id="1-db-데이터-동기화replication의-실제-작동-메커니즘">1. DB 데이터 동기화(Replication)의 실제 작동 메커니즘</h2>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/18614a40-7048-445c-94e8-42cf289362e9/image.png" alt=""></p>
<p>AWS RDS의 Multi-AZ 구성은 서비스 연속성을 위한 핵심 요소입니다. 하지만 이들의 통신 방식은 일반적인 VPC 내부 통신과는 궤를 달리합니다.</p>
<h3 id="11-작동-원리-동기식-복제-synchronous-replication">1.1. 작동 원리: 동기식 복제 (Synchronous Replication)</h3>
<ul>
<li><strong>인프라 추상화:</strong> 데이터 복제는 인프라 관리자가 정의한 <strong>서브넷 라우팅 테이블</strong>이나 <strong>VPC Endpoint</strong>를 경유하지 않습니다.</li>
<li><strong>전용 백엔드 네트워크:</strong> AWS가 관리하는 별도의 내부 전용 백엔드 네트워크를 통해 물리적 데이터 센터 간 실시간 동기화가 수행됩니다.</li>
<li><strong>관리 주체:</strong> 이는 관리형 서비스(Managed Service)의 영역으로, 사용자가 YAML 파일이나 콘솔에서 통로를 설정하거나 개입할 수 없습니다.</li>
</ul>
<h3 id="12-결론">1.2. 결론</h3>
<p>DB 노드 간의 복제 트래픽은 사용자의 VPC 트래픽 쿼터나 보안 그룹 설정(Egress)에 영향을 받지 않는 <strong>완전 격리된 평면(Control/Data Plane)</strong>에서 동작합니다.</p>
<hr>
<h2 id="2-vpc-endpoint의-역할과-완전-격리의-맹점">2. VPC Endpoint의 역할과 완전 격리의 맹점</h2>
<p>인터넷 게이트웨이(IGW)와 NAT 게이트웨이가 없는 &#39;완전 격리 서브넷&#39;은 보안상 유리하지만, 서비스 운영 측면에서는 &#39;고립&#39;이라는 부작용을 낳습니다.</p>
<h3 id="21-도입-필요성">2.1. 도입 필요성</h3>
<p>DB 서브넷에서 외부 인터넷(0.0.0.0/0) 경로를 삭제하면 외부 위협은 차단되지만, AWS의 다른 필수 관리 서비스와의 접점도 상실됩니다. 이를 해결하기 위해 <strong>VPC Endpoint</strong>라는 프라이빗 전용 통로가 필요합니다.</p>
<h3 id="22-db-서브넷-필수-endpoint-연동-대상">2.2. DB 서브넷 필수 Endpoint 연동 대상</h3>
<p>완전 격리 환경에서도 운영을 위해 반드시 고려해야 할 엔드포인트는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th align="left">서비스명</th>
<th align="left">유형</th>
<th align="left">용도 및 필요성</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>S3</strong></td>
<td align="left">Gateway</td>
<td align="left">DB 스냅샷 백업 데이터 전송 및 로그 저장.</td>
</tr>
<tr>
<td align="left"><strong>Secrets Manager</strong></td>
<td align="left">Interface</td>
<td align="left">DB 자격 증명(Password)의 안전한 호출 및 로테이션.</td>
</tr>
<tr>
<td align="left"><strong>Systems Manager (SSM)</strong></td>
<td align="left">Interface</td>
<td align="left">퍼블릭 접속 없이 프라이빗 환경에서의 OS 패치 및 보안 관리.</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-실무-아키텍처-설계-요약-best-practice">3. 실무 아키텍처 설계 요약 (Best Practice)</h2>
<p>안전하고 견고한 격리형 DB 인프라를 설계하기 위한 3대 원칙입니다.</p>
<h3 id="①-고가용성-high-availability"><strong>① 고가용성 (High Availability)</strong></h3>
<p>2개 이상의 가용 영역(AZ)에 서브넷을 분산 배치하고 <strong>RDS Multi-AZ</strong> 옵션을 활성화합니다. 이때 데이터 동기화는 RDS 자체 네트워크 인프라에 위임합니다.</p>
<h3 id="②-보안성-isolation"><strong>② 보안성 (Isolation)</strong></h3>
<p>DB 서브넷의 라우팅 테이블에서 <strong>IGW 및 NAT Gateway</strong>로 향하는 모든 라우팅을 제거하여 외부 노출을 원천 차단합니다.</p>
<h3 id="③-운영-가능성-feasibility"><strong>③ 운영 가능성 (Feasibility)</strong></h3>
<p>격리된 환경 내에서 AWS 서비스와 통신할 수 있도록 <strong>VPC Endpoint(S3, SSM 등)</strong>를 구성하여 백업 및 관리 효율성을 확보합니다.</p>
<hr>
<blockquote>
<p><strong>Editor&#39;s Note:</strong>
결국 DB 보안의 핵심은 &quot;무조건적인 차단&quot;이 아니라, <strong>&quot;필요한 통로(Endpoint)만 선별적으로 열어주는 전략적 격리&quot;</strong>에 있습니다. 복제 메커니즘에 대한 오해를 바로잡는 것이 효율적인 아키텍처 설계의 시작입니다.</p>
</blockquote>
<h2 id="4-마무리">4. 마무리</h2>
<p>만약 자신이 회사에서 aws를 이용해 인프라를 구축해야 될 상황이 왔을 때 단순히 db subnet은 외부와 완전히 차단되야 된다고 생각해 Nat 게이트웨이뿐만아니라 vpc endpoint까지 연결하지 않는다면 그 subnet은 db로써 역할할 수 없는 &quot;외딴섬&quot;이 되버립니다.
지속적인 db 백업이나 보안 업데이트를 위해 AWS 서비스들과 통신하기 위해서 꼭 vpc endpoint가 있어야 된다는 것을 알게 되었습니다.<img src="https://velog.velcdn.com/images/mason_dev/post/0fd7f5f5-ac3c-4ba5-9b00-3b4e2470670b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3-Tier 아키텍처 구축기: Bastion Host부터 Dual NAT까지]]></title>
            <link>https://velog.io/@mason_dev/3-Tier-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B5%AC%EC%B6%95%EA%B8%B0-Bastion-Host%EB%B6%80%ED%84%B0-Dual-NAT%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@mason_dev/3-Tier-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B5%AC%EC%B6%95%EA%B8%B0-Bastion-Host%EB%B6%80%ED%84%B0-Dual-NAT%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 02 Apr 2026 07:47:39 GMT</pubDate>
            <description><![CDATA[<h2 id="📝-velog-포스팅-목차-및-핵심-내용">📝 Velog 포스팅 목차 및 핵심 내용</h2>
<h3 id="1-bastion-host-보안의-시작이자-끝">1. Bastion Host: 보안의 시작이자 끝</h3>
<ul>
<li><strong>Bastion Host의 정의:</strong> 외부 인터넷에서 프라이빗 서브넷에 있는 자원에 안전하게 접근하기 위한 &#39;프록시(Proxy)&#39; 역할의 서버.</li>
<li><strong>필요성:</strong> 프라이빗 서브넷은 공인 IP가 없어 직접 접속이 불가능하다. 하지만 관리자는 패치나 설정을 위해 접속해야 하므로, 퍼블릭에 &#39;대문(Bastion)&#39;을 하나 두고 이를 통해서만 들어가게 설계한다.</li>
</ul>
<h3 id="2-아키텍처-설계-multi-az-3-tier-구성">2. 아키텍처 설계: Multi-AZ 3-Tier 구성</h3>
<p>오늘 구축한 네트워크의 핵심 설계를 공유합니다.</p>
<ul>
<li><strong>VPC CIDR 설계:</strong> <code>10.0.0.0/20</code>을 사용하여 총 4,096개의 IP 확보.</li>
<li><strong>서브넷 전략:</strong> * <strong>Public:</strong> 로드밸런서(ALB), NAT 게이트웨이, Bastion 배치.<ul>
<li><strong>App Private:</strong> 애플리케이션 서버 배치. EKS 파드 확장을 고려해 <code>/22</code>(1024개 IP)로 넉넉하게 할당.</li>
<li><strong>DB Private:</strong> 외부와 완전히 차단된 Isolated 영역.</li>
</ul>
</li>
<li><strong>고가용성(HA):</strong> 가용 영역(AZ) 1, 2에 각각 NAT 게이트웨이를 배치하여 데이터센터 장애에 대비.</li>
</ul>
<h3 id="3-오늘의-삽질-트러블슈팅-리포트-핵심-섹션">3. 오늘의 삽질: 트러블슈팅 리포트 (핵심 섹션)</h3>
<h4 id="이슈-1-서브넷은-퍼블릭인데-인스턴스는-퍼블릭이-아니다"><strong>이슈 1: &quot;서브넷은 퍼블릭인데, 인스턴스는 퍼블릭이 아니다?&quot;</strong></h4>
<ul>
<li><strong>상황:</strong> Bastion 호스트에 접속하려니 &quot;퍼블릭 서브넷에 있지 않다&quot;는 에러 발생.</li>
<li><strong>원인:</strong> CloudFormation의 <strong>경쟁 상태(Race Condition)</strong>. 인터넷 게이트웨이(IGW)가 VPC에 완전히 붙기 전에 인스턴스가 먼저 생성되면서 경로를 찾지 못한 것.</li>
<li><strong>해결:</strong> <code>DependsOn: VPCGatewayAttachment</code> 속성을 통해 리소스 생성 순서를 명시적으로 제어.</li>
</ul>
<h4 id="이슈-2-error-in-libcrypto-ssh-접속-실패"><strong>이슈 2: <code>error in libcrypto</code> SSH 접속 실패</strong></h4>
<ul>
<li><strong>상황:</strong> Bastion에서 프라이빗 서버로 접속 시 <code>.pem</code> 키 로드 에러 발생.</li>
<li><strong>원인:</strong> 키 파일의 권한(<code>chmod 400</code>) 미비 및 복사 과정에서의 포맷 손상.</li>
<li><strong>해결:</strong> PuttyGen을 통한 올바른 <code>OpenSSH</code> 포맷 변환 및 권한 설정의 중요성 확인.</li>
</ul>
<h3 id="4-아키텍처적-고찰-alb는-왜-퍼블릭에-있는가">4. 아키텍처적 고찰: ALB는 왜 퍼블릭에 있는가?</h3>
<ul>
<li>로드밸런서(ALB)는 사용자를 맞는 &#39;정문&#39;이므로 퍼블릭에 위치해야 한다.</li>
<li>하지만 실제 데이터가 있는 서버는 &#39;안방(Private)&#39;에 숨긴다.</li>
<li><strong>결론:</strong> 로드밸런서가 퍼블릭에서 트래픽을 받아 프라이빗 타겟 그룹으로 넘겨주는 구조가 보안의 정석임을 실습으로 증명.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Troubleshooting] EKS EBS CSI Driver 설치 시 ConfigurationConflict 및 ResourceInUseException 해결]]></title>
            <link>https://velog.io/@mason_dev/Troubleshooting-EKS-EBS-CSI-Driver-%EC%84%A4%EC%B9%98-%EC%8B%9C-ConfigurationConflict-%EB%B0%8F-ResourceInUseException-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@mason_dev/Troubleshooting-EKS-EBS-CSI-Driver-%EC%84%A4%EC%B9%98-%EC%8B%9C-ConfigurationConflict-%EB%B0%8F-ResourceInUseException-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 01 Apr 2026 06:20:20 GMT</pubDate>
            <description><![CDATA[<h3 id="1-문제-상황"><strong>1. 문제 상황</strong></h3>
<p>EKS 클러스터에서 EBS CSI Driver를 설정하던 중, 크게 두 가지 단계에서 에러가 발생했습니다.</p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/e952cc12-29ab-496b-a6d8-0a1030f5ce2a/image.png" alt=""></p>
<h4 id="에러-1-eksctl을-이용한-iam-서비스-계정irsa-생성-실패"><strong>에러 1: <code>eksctl</code>을 이용한 IAM 서비스 계정(IRSA) 생성 실패</strong></h4>
<pre><code class="language-bash">eksctl create iamserviceaccount \
  --name ebs-csi-controller-sa \
  --role-name AmazonEKS_EBS_CSI_DriverRole_new \
  ... --approve</code></pre>
<ul>
<li><strong>증상:</strong> <code>waiter state transitioned to Failure</code>, <code>Error: failed to create iamserviceaccount(s)</code></li>
<li><strong>원인:</strong> <code>--role-name</code>에 지정한 이름(<code>AmazonEKS_EBS_CSI_DriverRole_new</code>)의 IAM 역할이 이미 AWS 계정에 존재하기 때문입니다. CloudFormation은 동일한 이름의 리소스를 생성하려고 할 때 충돌을 일으키며 롤백됩니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/c5cea78e-baa1-4e92-89a6-7258d5aa4b9d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mason_dev/post/091f0720-b263-44fe-8e8e-7ee7020a2570/image.png" alt=""></p>
<h4 id="에러-2-eks-addon-생성-후-create_failed-및-업데이트-불가"><strong>에러 2: EKS Addon 생성 후 <code>CREATE_FAILED</code> 및 업데이트 불가</strong></h4>
<ul>
<li><strong>증상:</strong> <code>aws eks describe-addon</code> 결과 상태가 <code>CREATE_FAILED</code>이며, <code>ConfigurationConflict</code> 메시지 출력. 이를 해결하기 위해 <code>update-addon</code>을 시도했으나 <code>ResourceInUseException</code> 발생.</li>
<li><strong>원인:</strong> 이미 <code>eksctl</code>을 통해 생성된 <code>ebs-csi-controller-sa</code>가 존재하기 때문에, EKS 애드온 관리자가 해당 리소스에 대한 권한(Managed-by)을 가져오지 못해 충돌이 발생한 것입니다.</li>
</ul>
<p>*여기서 ebs-csi-controller-sa 란 </p>
<ul>
<li><p>Amazon EKS 환경에서 EBS(블록 스토리지) 볼륨의 생명주기를 관리하는 ebs-csi-controller 파드에 IAM 권한을 부여하기 위해 사용하는 쿠버네티스 ServiceAccount</p>
</li>
<li><p>AWS IAM 역할(IRSA)과 연결되어, 컨트롤러가 볼륨 생성, 연결 등 AWS API 작업을 수행할 수 있도록 인증하는 핵심 요소</p>
</li>
</ul>
<hr>
<h3 id="2-원인-분석-deep-dive"><strong>2. 원인 분석 (Deep Dive)</strong></h3>
<h4 id="가설-1-리소스-네이밍-충돌"><strong>가설 1: 리소스 네이밍 충돌</strong></h4>
<p><code>eksctl</code>은 CloudFormation 스택을 통해 작업을 수행합니다. <code>--role-name</code> 옵션을 고정하면, 해당 이름의 IAM Role이 이미 존재할 경우 스택 생성이 실패합니다. <strong>IAM Role은 전역 리소스이므로 고유한 이름을 가져야 합니다.</strong></p>
<h4 id="가설-2-상태-관리의-교착-상태"><strong>가설 2: 상태 관리의 교착 상태</strong></h4>
<p>AWS EKS 애드온의 상태 머신(State Machine)은 매우 엄격합니다.</p>
<ol>
<li><strong><code>ConfigurationConflict</code></strong>: 서비스 계정(SA)의 관리 주체가 <code>eksctl</code>인지 <code>EKS 애드온</code>인지 결정되지 않아 발생합니다.</li>
<li><strong><code>ResourceInUseException</code></strong>: 애드온이 <code>CREATE_FAILED</code> 상태에 빠지면, AWS API는 이를 &#39;불완전한 리소스&#39;로 간주하여 <code>update</code> 명령을 허용하지 않습니다. 오직 <code>delete</code> 후 다시 <code>create</code> 하는 것만 허용됩니다.</li>
</ol>
<hr>
<h3 id="3-해결-방법-step-by-step"><strong>3. 해결 방법 (Step-by-Step)</strong></h3>
<h4 id="step-1-기존-실패-흔적-정리"><strong>Step 1: 기존 실패 흔적 정리</strong></h4>
<p>가장 먼저 실패한 애드온 객체를 삭제하여 깨끗한 상태로 만듭니다.</p>
<pre><code class="language-bash">aws eks delete-addon --cluster-name $CLUSTER_NAME --addon-name aws-ebs-csi-driver</code></pre>
<h4 id="step-2-iam-role-생성-확인-또는-고유-이름-사용"><strong>Step 2: IAM Role 생성 확인 (또는 고유 이름 사용)</strong></h4>
<p>이미 Role이 존재한다면 그대로 사용하고, 새로 만든다면 반드시 기존에 없는 이름을 사용해야 합니다. </p>
<ul>
<li><strong>Tip:</strong> <code>eksctl</code> 실행 시 <code>--role-name</code>을 생략하면 <code>eksctl</code>이 고유한 이름을 자동으로 생성해 줍니다. 만약 수동 지정이 필요하다면 버전 번호나 날짜를 붙여 중복을 피하십시오.</li>
</ul>
<h4 id="step-3-overwrite-옵션과-함께-애드온-재설치"><strong>Step 3: OVERWRITE 옵션과 함께 애드온 재설치</strong></h4>
<p>이미 존재하는 ServiceAccount와의 갈등을 무시하고 EKS 애드온이 관리 권한을 강제로 가져오도록 설정합니다.</p>
<pre><code class="language-bash">aws eks create-addon \
  --cluster-name $CLUSTER_NAME \
  --addon-name aws-ebs-csi-driver \
  --service-account-role-arn arn:aws:iam::$ACCOUNT_ID:role/고유한_IAM_역할_이름 \
  --resolve-conflicts OVERWRITE</code></pre>
<hr>
<h3 id="4-최종-결과-확인"><strong>4. 최종 결과 확인</strong></h3>
<pre><code class="language-bash">aws eks describe-addon --cluster-name $CLUSTER_NAME --addon-name aws-ebs-csi-driver --query &#39;addon.status&#39;
# &quot;ACTIVE&quot; 확인 시 성공</code></pre>
<h3 id="5-핵심-요약-lessons-learned"><strong>5. 핵심 요약 (Lessons Learned)</strong></h3>
<ol>
<li><strong>IAM Role Name</strong>: 자동화 도구(<code>eksctl</code>, <code>terraform</code>) 사용 시 리소스 이름 중복 여부를 반드시 체크할 것.</li>
<li><strong>Conflict Strategy</strong>: 기존에 Kubernetes 리소스(SA 등)가 존재하는 상태에서 EKS 애드온을 추가할 때는 반드시 <code>--resolve-conflicts OVERWRITE</code> 옵션을 고려할 것.</li>
<li><strong>State Recovery</strong>: EKS 애드온이 <code>CREATE_FAILED</code>라면 업데이트는 불가능하다. 삭제(Delete) 후 재생성이 답이다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TroubleShooting] 테라폼 배포 실패?  반드시 체크해야 할 3가지]]></title>
            <link>https://velog.io/@mason_dev/TroubleShooting-%ED%85%8C%EB%9D%BC%ED%8F%BC-%EB%B0%B0%ED%8F%AC-%EC%8B%A4%ED%8C%A8-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%B2%B4%ED%81%AC%ED%95%B4%EC%95%BC-%ED%95%A0-3%EA%B0%80%EC%A7%80</link>
            <guid>https://velog.io/@mason_dev/TroubleShooting-%ED%85%8C%EB%9D%BC%ED%8F%BC-%EB%B0%B0%ED%8F%AC-%EC%8B%A4%ED%8C%A8-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%B2%B4%ED%81%AC%ED%95%B4%EC%95%BC-%ED%95%A0-3%EA%B0%80%EC%A7%80</guid>
            <pubDate>Wed, 01 Apr 2026 01:50:52 GMT</pubDate>
            <description><![CDATA[<h2 id="1-이슈-발생-배경-뇌는-있는데-몸통이-없다">1. 이슈 발생 배경: &quot;뇌는 있는데 몸통이 없다&quot;</h2>
<p>EKS 클러스터 배포 후 <code>kubectl get nodes</code>를 입력했을 때 <code>No resources found</code>가 출력되었다. 클러스터(Control Plane)라는 뇌는 생성되었으나, 실제 연산을 수행할 워커 노드(Worker Node)가 조인되지 않은 상태다.</p>
<h3 id="원인-분석">원인 분석</h3>
<p>명령을 내리는 <strong>Bastion Host의 IAM 역할(Role) 부재</strong>가 도미노 현상을 일으킨 것이 핵심이다.</p>
<ul>
<li>테라폼 실행 주체에게 권한이 없어 노드 그룹 생성 API 호출이 거절되었다.</li>
<li>이 과정에서 일부 리소스(KMS, Log Group)만 생성되고 상태 파일(<code>tfstate</code>) 기록에는 실패하여 데이터 불일치가 발생했다.</li>
</ul>
<hr>
<h2 id="2-주요-트러블슈팅-케이스별-대응">2. 주요 트러블슈팅 케이스별 대응</h2>
<h3 id="case-1-alreadyexistsexception-kms-cloudwatch">Case 1. AlreadyExistsException (KMS, CloudWatch)</h3>
<ul>
<li><strong>증상</strong>: <code>terraform apply</code> 시 특정 리소스가 이미 존재한다며 중단된다.</li>
<li><strong>이유</strong>: 이전 배포 실패로 리소스는 AWS에 생성되었으나, 테라폼의 &#39;기억(State)&#39;에는 등록되지 않아 발생하는 충돌이다.</li>
<li><strong>대응</strong>: <code>terraform import</code> 명령어로 기존 자원을 테라폼 관리 하에 편입하거나, AWS CLI로 해당 리소스를 직접 삭제 후 재배포한다.</li>
</ul>
<h3 id="case-2-failed-to-create-iamserviceaccount-eksctl">Case 2. failed to create iamserviceaccount (eksctl)</h3>
<ul>
<li><strong>증상</strong>: AWS Load Balancer Controller 설치를 위한 서비스 계정 생성에 실패한다.</li>
<li><strong>이유</strong>: 클러스터와 IAM 간의 신뢰 관계(OIDC Provider)가 설정되지 않았거나, 정책 ARN의 Account ID에 하이픈(<code>-</code>)이 포함된 경우다.</li>
<li><strong>대응</strong>: <code>eksctl utils associate-iam-oidc-provider</code>를 선행 실행하고 ARN 형식을 다시 확인한다.</li>
</ul>
<h3 id="case-3-bastion-host-권한-불일치">Case 3. Bastion Host 권한 불일치</h3>
<ul>
<li><strong>증상</strong>: 모든 명령어가 <code>AccessDenied</code>를 반환하거나 불완전하게 실행된다.</li>
<li><strong>이유</strong>: EC2 인스턴스 역할과 <code>aws configure</code>로 설정된 Access Key 권한이 충돌하여 우선순위가 정립되지 않은 상태다.</li>
<li><strong>대응</strong>: 인스턴스 프로파일을 확인하고 <code>~/.aws/credentials</code> 삭제를 통해 권한 체계를 단일화한다.</li>
</ul>
<hr>
<h2 id="3-clean-slate-전략-완벽한-리셋-프로세스">3. &quot;Clean Slate&quot; 전략: 완벽한 리셋 프로세스</h2>
<p>설정이 꼬였을 때 가장 빠르게 복구할 수 있는 단계별 가이드다.</p>
<h3 id="step-1-잔여-리소스-수동-삭제">Step 1: 잔여 리소스 수동 삭제</h3>
<pre><code class="language-bash"># EKS 클러스터 삭제 (약 15분 소요)
aws eks delete-cluster --name my-eks-cluster

# KMS 및 로그 그룹 삭제
aws kms delete-alias --alias-name alias/eks/my-eks-cluster
aws logs delete-log-group --log-group-name /aws/eks/my-eks-cluster/cluster</code></pre>
<h3 id="step-2-테라폼-상태-초기화">Step 2: 테라폼 상태 초기화</h3>
<pre><code class="language-bash"># 상태 파일 및 캐시 강제 삭제
rm -rf .terraform* terraform.tfstate*</code></pre>
<h3 id="step-3-기반-권한-재점검">Step 3: 기반 권한 재점검</h3>
<ul>
<li>Bastion Host에 <code>AdministratorAccess</code> 역할이 연결되었는지 확인한다.</li>
<li><code>aws sts get-caller-identity</code>로 현재 권한 주체를 확정한다.</li>
</ul>
<h3 id="step-4-재배포">Step 4: 재배포</h3>
<pre><code class="language-bash">terraform init
terraform plan
terraform apply -auto-approve</code></pre>
<hr>
<h2 id="4-회고">4. 회고</h2>
<p>인프라 구축은 명령어의 나열이 아니라 <strong>권한(IAM) - 네트워크(VPC) - 상태(State)</strong>라는 세 요소의 정렬을 맞추는 과정이다. 이번 트러블슈팅을 통해 리소스 간의 의존성과 테라폼의 작동 원리를 깊게 이해하게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 요즘 EKS 환경에서는 Karpenter를 더 많이 사용할까?]]></title>
            <link>https://velog.io/@mason_dev/%EC%99%9C-%EC%9A%94%EC%A6%98-EKS-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EB%8A%94-Karpenter%EB%A5%BC-%EB%8D%94-%EB%A7%8E%EC%9D%B4-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@mason_dev/%EC%99%9C-%EC%9A%94%EC%A6%98-EKS-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EB%8A%94-Karpenter%EB%A5%BC-%EB%8D%94-%EB%A7%8E%EC%9D%B4-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 31 Mar 2026 14:17:19 GMT</pubDate>
            <description><![CDATA[<p>Kubernetes에서 HPA를 사용하면 Pod 개수는 자동으로 늘릴 수 있다.<br>하지만 Pod가 늘어났을 때 이를 실제로 수용할 Node 자원이 부족하면 일부 Pod는 <code>Pending</code> 상태로 남게 된다.</p>
<p>이때 필요한 것이 <strong>Node Autoscaling</strong>이다.<br>대표적인 도구가 <strong>Cluster Autoscaler</strong>와 <strong>Karpenter</strong>인데, 둘 다 노드를 자동으로 늘려준다는 점은 같지만 동작 방식은 꽤 다르다.</p>
<hr>
<h2 id="hpa와-node-autoscaler의-관계">HPA와 Node Autoscaler의 관계</h2>
<ul>
<li><strong>HPA</strong>: Pod 개수 조절</li>
<li><strong>Cluster Autoscaler / Karpenter</strong>: Node 개수 조절</li>
</ul>
<p>즉, HPA가 Pod를 늘리고, Node가 부족하면 Node Autoscaler가 새 Node를 추가하는 구조다.</p>
<h3 id="아키텍처-흐름">아키텍처 흐름</h3>
<pre><code class="language-mermaid">flowchart LR
    A[User Traffic Increase] --&gt; B[HPA scales out Pods]
    B --&gt; C[Some Pods become Pending]
    C --&gt; D[Node Autoscaler works]
    D --&gt; E[New Node added]
    E --&gt; F[Pending Pods scheduled]</code></pre>
<hr>
<h2 id="cluster-autoscaler란">Cluster Autoscaler란?</h2>
<p>Cluster Autoscaler는 <strong>미리 만들어둔 Node Group</strong>을 기준으로 확장한다.
예를 들어 AWS EKS에서 Managed Node Group이나 Auto Scaling Group을 먼저 구성해두고, 스케줄되지 못한 Pod가 생기면 그중 하나의 크기를 늘리는 방식이다.</p>
<p>즉, Cluster Autoscaler는
<strong>“이미 준비된 Node Group의 크기를 조절하는 방식”</strong> 이라고 볼 수 있다.</p>
<hr>
<h2 id="karpenter란">Karpenter란?</h2>
<p>Karpenter는 <code>Pending Pod</code>의 요구사항을 보고, 그에 맞는 Node를 <strong>그때그때 새로 생성</strong>한다.
CPU, Memory, Architecture, Spot 여부 같은 조건을 기준으로 더 적절한 인스턴스를 선택할 수 있다.</p>
<p>즉, Karpenter는
<strong>“Node Group 중심이 아니라 워크로드 중심으로 확장하는 방식”</strong> 이다.</p>
<hr>
<h2 id="두-도구의-가장-큰-차이">두 도구의 가장 큰 차이</h2>
<p>핵심 차이는 <strong>확장의 기준</strong>이다.</p>
<ul>
<li><strong>Cluster Autoscaler</strong>: 미리 정의된 Node Group 기준</li>
<li><strong>Karpenter</strong>: Pending Pod의 실제 요구사항 기준</li>
</ul>
<p>한 문장으로 정리하면 이렇다.</p>
<blockquote>
<p>Cluster Autoscaler는 <strong>Node Group 중심</strong>, Karpenter는 <strong>워크로드 중심</strong>이다.</p>
</blockquote>
<h3 id="비교-다이어그램">비교 다이어그램</h3>
<pre><code class="language-mermaid">flowchart TD
    subgraph CA[Cluster Autoscaler]
        A1[Pending Pod 발생] --&gt; A2[기존 Node Group 확인]
        A2 --&gt; A3[Node Group 크기 증가]
        A3 --&gt; A4[새 Node 추가]
    end

    subgraph KP[Karpenter]
        B1[Pending Pod 발생] --&gt; B2[Pod 요구사항 분석]
        B2 --&gt; B3[적절한 인스턴스 선택]
        B3 --&gt; B4[새 Node 생성]
    end</code></pre>
<hr>
<h2 id="왜-요즘은-karpenter를-많이-사용할까">왜 요즘은 Karpenter를 많이 사용할까?</h2>
<h3 id="1-비용-최적화">1. 비용 최적화</h3>
<p>Karpenter는 Pod 요구사항에 맞는 인스턴스를 더 유연하게 선택할 수 있어서, 불필요하게 큰 Node를 유지할 가능성을 줄여준다.</p>
<h3 id="2-운영-단순화">2. 운영 단순화</h3>
<p>Cluster Autoscaler는 Node Group이 많아질수록 설계와 관리가 복잡해진다.
반면 Karpenter는 Node Group 설계 부담을 줄여준다.</p>
<h3 id="3-워크로드-변화-대응">3. 워크로드 변화 대응</h3>
<p>서비스가 많고 요구사항이 다양한 환경에서는, Karpenter처럼 상황에 맞는 Node를 바로 생성하는 방식이 더 잘 맞는다.</p>
<h3 id="karpenter가-선호되는-이유">Karpenter가 선호되는 이유</h3>
<pre><code class="language-mermaid">flowchart LR
    A[Workload Diversity] --&gt; D[Karpenter]
    B[Cost Optimization] --&gt; D
    C[Operational Simplicity] --&gt; D</code></pre>
<hr>
<h2 id="cluster-autoscaler가-여전히-괜찮은-경우">Cluster Autoscaler가 여전히 괜찮은 경우</h2>
<p>그렇다고 Cluster Autoscaler가 무조건 뒤처진 것은 아니다.
클러스터 구조가 단순하고, Node Group 수가 많지 않으며, 현재 운영 방식이 안정적이라면 Cluster Autoscaler도 충분히 좋은 선택이다.</p>
<p>즉, 무조건 하나가 정답이라기보다는
<strong>클러스터 규모와 운영 복잡도에 따라 선택이 달라진다.</strong></p>
<hr>
<h2 id="마무리">마무리</h2>
<p>정리하면 아래와 같다.</p>
<ul>
<li><strong>Cluster Autoscaler</strong>: 미리 준비한 Node Group을 확장</li>
<li><strong>Karpenter</strong>: Pending Pod에 맞는 Node를 직접 생성</li>
</ul>
<p>최근 EKS 환경에서 Karpenter가 많이 사용되는 이유는
<strong>비용 효율성</strong>, <strong>운영 단순화</strong>, <strong>유연한 확장성</strong> 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IaC] 테라폼을 활용한 EKS 구축]]></title>
            <link>https://velog.io/@mason_dev/IaC-%ED%85%8C%EB%9D%BC%ED%8F%BC%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-EKS-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@mason_dev/IaC-%ED%85%8C%EB%9D%BC%ED%8F%BC%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-EKS-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Tue, 31 Mar 2026 12:53:53 GMT</pubDate>
            <description><![CDATA[<ol>
<li>왜 테라폼을 사용해야 할까요? </li>
</ol>
<p>실무에서 테라폼을 도입하면 얻을 수 있는 구체적인 장점들은 다음과 같습니다:
장애 방지 및 신속한 복구: 수동 작업 시 발생할 수 있는 실수를 방지하고, 장애 발생 시 코드를 통해 빠르게 동일한 환경을 재구축할 수 있습니다.
업무 효율성 (칼퇴 보장): 수동으로 일일이 생성하던 자원들을 코드로 단시간에 생성할 수 있어 업무 시간을 획기적으로 줄여줍니다.
컴플라이언스 대응: ISMS-P(개인정보보호법) 심사나 전자금융거래법 감사 시, 콘솔을 일일이 보여주는 대신 코드를 통해 인프라 현황을 명확하게 증명할 수 있습니다.
멀티 클라우드 및 종속성 탈피: 특정 CSP(AWS, Google Cloud, Naver 등)에 종속되지 않고 동일한 문법(HCL)으로 여러 클라우드를 관리할 수 있습니다
. 예를 들어 게임사에서 해외 데이터센터 응답 속도 최적화를 위해 AWS와 GCP를 병행 운영할 때 매우 유용합니다
.
불변 인프라(Immutable Infrastructure): 서버를 수정하는 대신 기존 것을 삭제하고 새로 생성하는 클라우드 네이티브 방식에 최적화되어 있습니다
.
잠깐! 용어 정리
CSP (Cloud Service Provider): 인프라를 제공하는 업체 (AWS, Google, Naver 등)
MSP (Managed Service Provider): 기업의 클라우드 설계를 대행하고 운영/관리해주는 전문 업체 (메가존, 베스핀글로벌, 가비아 등)</p>
<hr>
<ol start="2">
<li>테라폼의 핵심 개념
테라폼을 다루기 위해 반드시 알아야 할 5가지 개념입니다
:
Provider: 테라폼과 외부 서비스(AWS, GCP 등)를 연결하는 플러그인입니다.
Resource: 실제 생성할 인프라 자원 (EC2, S3, VPC 등)을 정의합니다.
State (.tfstate): 현재 인프라의 상태를 저장하는 파일입니다. 팀 협업 시에는 S3 같은 원격 저장소에 저장하고 <strong>Locking(DynamoDB 활용)</strong>을 하는 것이 원칙입니다.
Module: 리소스들을 그룹화하여 재사용 가능하게 만든 패키지입니다.
HCL (HashiCorp Configuration Language): 테라폼 전용 언어로 가독성이 매우 뛰어납니다.</li>
</ol>
<hr>
<ol start="3">
<li>테라폼 동작 프로세스
테라폼은 보통 다음의 4단계 흐름으로 작업합니다
:
명령어
설명
terraform init
작업 디렉토리 초기화 및 Provider 플러그인 다운로드
terraform plan
생성/수정/삭제될 리소스 예측 및 검토 (실제 반영 X)
terraform apply
실제 인프라 배포 (실행 시 yes 입력 필요)
terraform destroy
생성된 모든 리소스 삭제
Tip: -auto-approve 옵션을 사용하면 yes 입력 없이 바로 실행이 가능합니다.</li>
</ol>
<hr>
<ol start="4">
<li>실전! 테라폼으로 EKS(Elastic Kubernetes Service) 구축하기
비용 절감 꿀팁
EKS Managed Node Group 설정 시 비용을 아끼고 싶다면 capacity_type을 ON_DEMAND가 아닌 <strong>SPOT</strong>으로 설정하세요
.
주요 구축 흐름
VPC 생성: EKS를 위해 최소 2개의 가용 영역(AZ)과 Public/Private 서브넷을 구성합니다.
EKS 클러스터 생성: 공식 EKS 모듈을 사용하여 Control Plane과 노드 그룹을 설정합니다.
접속 설정: 인프라 생성 후 내 PC에서 클러스터를 확인하려면 kubeconfig 업데이트가 필요합니다.</li>
</ol>
<hr>
<ol start="5">
<li>운영 중 발생하는 문제 및 꿀팁 (Troubleshooting)
Q1. HPA 설정 후 서버 부하를 주었는데 파드가 늘어나지 않아요.
원인: Metrics Server가 설치되어 있는지 확인하세요.
작동 원리: HPA 컨트롤러는 15초마다 메트릭 서버를 체크하여 목표 사용량과 대조 후 Pod 개수를 조절합니다.
Q2. 노드 자리가 부족해서 노드를 늘려야 해요.
Cluster Autoscaling이 필요하며, 이때 ASG(Auto Scaling Group) 이름과 클러스터 이름 두 가지 정보가 필수입니다.
Q3. 재설치 시 CloudWatch Logs 그룹이 이미 존재한다는 오류가 떠요.
이 경우 다음 순서로 조치하세요
:
콘솔에서 EKS 삭제
CloudWatch Logs 그룹 수동 삭제
로그 그룹 재사용 선언 후 재설치</li>
</ol>
<hr>
<ol start="6">
<li>마치며
테라폼은 이제 클라우드 엔지니어에게 선택이 아닌 필수 도구입니다. 더 많은 프로바이더 사용 예시는 Terraform Registry에서 직접 확인하고 적용해 보시기 바랍니다.</li>
</ol>
<hr>
<p>#Terraform #IaC #AWS #EKS #CloudNative #Kubernetes #멀티클라우드</p>
]]></description>
        </item>
    </channel>
</rss>