<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>leenah.log</title>
        <link>https://velog.io/</link>
        <description>정도를 걷는 엔지니어</description>
        <lastBuildDate>Tue, 05 May 2026 13:19:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>leenah.log</title>
            <url>https://velog.velcdn.com/images/lee_nah/profile/2dac464d-968c-4d72-a818-a46f23b5115b/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. leenah.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lee_nah" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Hikari Connection Pool 튜닝으로 p95 응답시간 60% 개선하기]]></title>
            <link>https://velog.io/@lee_nah/Hikari-Connection-Pool-%ED%8A%9C%EB%8B%9D%EC%9C%BC%EB%A1%9C-p95-%EC%9D%91%EB%8B%B5%EC%8B%9C%EA%B0%84-60-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/Hikari-Connection-Pool-%ED%8A%9C%EB%8B%9D%EC%9C%BC%EB%A1%9C-p95-%EC%9D%91%EB%8B%B5%EC%8B%9C%EA%B0%84-60-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 13:19:27 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@lee_nah/k6%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0">전 포스팅</a>에서 k6로 300 VUs 부하테스트를 돌렸더니 평균 응답시간이 4초가 넘게 나왔다. 원인을 찾아보니 <code>application-prod.yaml</code>에 Hikari 설정 자체가 없어서 기본값인 커넥션 풀 10개로 돌아가고 있었다.</p>
<br>

<h1 id="문제">문제</h1>
<pre><code>avg=4.15s / p(90)=5.73s / p(95)=6.09s</code></pre><p>300명이 동시에 주문 요청을 보내면 DB 커넥션 풀이 금방 고갈된다. 커넥션을 얻지 못한 요청들은 줄을 서서 대기하게 되고, 그게 응답 지연으로 이어지는 구조였다.</p>
<blockquote>
<p><strong>HikariCP란?</strong> Spring Boot에서 기본으로 사용하는 DB 커넥션 풀 라이브러리다. 커넥션 풀이란 DB 연결을 미리 여러 개 만들어두고 요청이 들어올 때마다 재사용하는 방식인데, 풀 사이즈가 너무 작으면 동시 요청이 몰릴 때 병목이 생긴다. 기본값은 <code>maximum-pool-size: 10</code>으로, 동시에 10개의 DB 연결만 허용한다.</p>
</blockquote>
<br>

<h1 id="해결">해결</h1>
<p><code>application-prod.yaml</code>에 Hikari 설정이 아예 없었기 때문에 아래 설정을 추가해주었다.</p>
<pre><code class="language-yaml">hikari:
  maximum-pool-size: 30      # 최대 커넥션 수 (기본값 10 → 30으로 증가)
  minimum-idle: 10           # 유휴 상태에서 유지할 최소 커넥션 수
  connection-timeout: 30000  # 커넥션 획득 대기 최대 시간 (30초)
  idle-timeout: 600000       # 유휴 커넥션 유지 시간 (10분)
  max-lifetime: 1800000      # 커넥션 최대 수명 (30분)</code></pre>
<p>핵심은 <code>maximum-pool-size</code>를 10에서 30으로 늘린 것이다. 동시에 처리할 수 있는 DB 연결이 3배로 늘어나니 대기 줄이 줄어들 수밖에 없다.</p>
<br>

<h1 id="결과">결과</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/3dab1576-c9bf-4c64-b0ec-37f4a4fb0106/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/c4b389d2-e2d1-4089-960e-dc58baa02810/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>튜닝 전</th>
<th>튜닝 후</th>
<th>개선율</th>
</tr>
</thead>
<tbody><tr>
<td>avg</td>
<td>4.15s</td>
<td>1.69s</td>
<td>59% ↓</td>
</tr>
<tr>
<td>p(90)</td>
<td>5.73s</td>
<td>3.4s</td>
<td>41% ↓</td>
</tr>
<tr>
<td>p(95)</td>
<td>6.09s</td>
<td>4.62s</td>
<td>24% ↓</td>
</tr>
</tbody></table>
<p>설정 몇 줄 추가했을 뿐인데 평균 응답시간이 4.15초 → 1.69초로 약 60% 개선됐다.</p>
<p>코드 한 줄 안 바꾸고 설정만으로 이 정도 성능 차이가 난다는 게 인상적이었다. 기본값이 항상 최선은 아니다. 특히 트래픽이 몰리는 서비스라면 커넥션 풀 사이즈는 반드시 확인하고 튜닝해야 한다는 걸 이번에 직접 체감하게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[k6로 재고 부하테스트하기]]></title>
            <link>https://velog.io/@lee_nah/k6%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/k6%EB%A1%9C-%EC%9E%AC%EA%B3%A0-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 12:16:55 GMT</pubDate>
            <description><![CDATA[<p>현재 부트캠프에서 <strong>뽀시래기</strong>라는 프로젝트를 진행하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/888d5a39-1880-4054-91e5-9a5d6f60a6cd/image.png" alt=""></p>
<p>도메인은 <a href="https://pposiraegi.cloud/login">https://pposiraegi.cloud/login</a> 이며, 타임딜 서비스 특성상 특정 시간대에 트래픽이 순간적으로 폭증하는 상황이 발생한다. 이 트래픽을 실제로 버틸 수 있는지 검증하기 위해 부하 테스트 도구인 <strong>k6</strong>를 사용해보기로 했다.</p>
<blockquote>
<p>k6란? JavaScript로 테스트 스크립트를 작성해 HTTP 요청을 대량으로 보내고, 응답 시간·성공률 등을 측정할 수 있는 오픈소스 부하 테스트 도구다.</p>
</blockquote>
<br>

<h1 id="k6-설치">k6 설치</h1>
<p>공식 설치 문서: <a href="https://grafana.com/docs/k6/latest/set-up/install-k6/">https://grafana.com/docs/k6/latest/set-up/install-k6/</a></p>
<p>Windows 환경이므로 PowerShell에서 아래 명령어를 실행한다.</p>
<pre><code class="language-bash">winget install k6 --source winget</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/44cd0d66-63b1-4e5d-8239-bdff09df42ca/image.png" alt=""></p>
<br>

<h1 id="k6-테스트-파일-작성">k6 테스트 파일 작성</h1>
<p><code>test.js</code> 파일을 생성하고 아래 스크립트를 작성했다.</p>
<pre><code class="language-javascript">import http from &#39;k6/http&#39;;
import { sleep, check } from &#39;k6&#39;;

export const options = {
  vus: 300,       // 동시 접속 가상 유저 300명
  duration: &#39;30s&#39;, // 30초 동안 테스트 실행
};

// 테스트 시작 전 1회 실행 - 로그인해서 토큰을 가져옴
export function setup() {
  const res = http.post(
    &#39;https://pposiraegi.cloud/api/v1/auth/login&#39;,
    JSON.stringify({ email: &#39;test@test.com&#39;, password: &#39;123456&#39; }),
    { headers: { &#39;Content-Type&#39;: &#39;application/json&#39; } }
  );
  console.log(&#39;로그인 응답:&#39;, res.status, res.body);
  const token = res.json(&#39;data.accessToken&#39;);
  console.log(&#39;토큰:&#39;, token);
  return { token };
}

// 가상 유저 300명이 30초 동안 반복 실행하는 시나리오
export default function (data) {
  const body = `{&quot;orderItems&quot;:[{&quot;skuId&quot;:839470480585919574,&quot;quantity&quot;:1}]}`;

  const res = http.post(
    &#39;https://pposiraegi.cloud/api/v1/orders&#39;,
    body,
    {
      headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
        &#39;Authorization&#39;: `Bearer ${data.token}`,
      }
    }
  );
  console.log(&#39;주문 응답:&#39;, res.status, res.body);
  check(res, {
    &#39;status is 200&#39;: (r) =&gt; r.status === 200,
  });
  sleep(1);
}</code></pre>
<p>작성 후 아래 명령어로 실행한다.</p>
<pre><code class="language-bash">k6 run test.js</code></pre>
<br>

<h1 id="실행-결과">실행 결과</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/066c5dcc-45a9-42ed-988d-115c0dc06971/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/2d3b87b3-09b3-40cb-ae70-f03730f9dc24/image.png" alt=""></p>
<p>결과를 해석하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>checks_succeeded</td>
<td>100% (1856개 요청 전부 성공)</td>
</tr>
<tr>
<td>평균 응답 시간 (avg)</td>
<td>4.15초</td>
</tr>
<tr>
<td>90% 응답 시간 (p90)</td>
<td>5.73초 이내</td>
</tr>
<tr>
<td>95% 응답 시간 (p95)</td>
<td>6.09초 이내</td>
</tr>
<tr>
<td>최대 응답 시간 (max)</td>
<td>9.7초</td>
</tr>
</tbody></table>
<p>요청 자체는 전부 성공했지만 응답 속도가 너무 느리다. 평균 4초, 최대 약 10초는 타임딜 서비스에서는 치명적인 수치다. DB 커넥션 풀을 관리하는 <strong>HikariCP 튜닝</strong>이 필요할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD 삽질기3] ArgoCD CD 고치기]]></title>
            <link>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B03-ArgoCD-CD-%EA%B3%A0%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B03-ArgoCD-CD-%EA%B3%A0%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 11:23:42 GMT</pubDate>
            <description><![CDATA[<p>GitHub Actions도 고치고, 부트스트랩도 정상 실행됐는데 이번엔 ArgoCD가 CD(배포) 과정에서 문제가 생겼다. ArgoCD는 Git 저장소를 바라보며 Kubernetes 클러스터에 자동으로 배포해주는 도구인데, 이 단계에서 또 막혔다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d13ea4bc-1670-41a1-8e78-4c5070812721/image.png" alt=""></p>
<p>위처럼 <strong>OutOfSync + Degraded</strong> 상태가 떠서 로그를 확인해보기로 했다.</p>
<blockquote>
<ul>
<li><strong>OutOfSync</strong>: Git에 있는 설정과 실제 클러스터 상태가 다르다는 뜻</li>
<li><strong>Degraded</strong>: 배포된 애플리케이션이 정상 동작하지 않는다는 뜻</li>
</ul>
</blockquote>
<br>

<h1 id="로그-확인">로그 확인</h1>
<pre><code class="language-bash">kubectl describe application pposiraegi -n argocd</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/607157f1-020b-47da-9996-d34bd3c53bcb/image.png" alt=""></p>
<p>로그에서 눈에 띄는 내용은 다음과 같았다.</p>
<pre><code class="language-bash">external-secrets.io/ClusterSecretStore CRD가 없음
external-secrets.io/ExternalSecret CRD가 없음</code></pre>
<p><code>ClusterSecretStore</code>와 <code>ExternalSecret</code> CRD가 없다는 에러였다.</p>
<blockquote>
<p>CRD(Custom Resource Definition)란 Kubernetes에 기본으로 없는 리소스 타입을 사용자가 직접 정의해서 추가하는 것이다. ESO를 설치하면 이 CRD들이 함께 등록된다.</p>
</blockquote>
<p>원인은 이전 bootstrap 스크립트 실행 중 Loki 설치 단계에서 Windows 환경 문제로 오류가 발생했고, 그 이후 단계인 ESO(External Secrets Operator) 설치까지 진행되지 못했기 때문이었다. ESO는 AWS Secrets Manager 같은 외부 시크릿 저장소의 값을 Kubernetes Secret으로 자동으로 동기화해주는 도구다.</p>
<p>따라서 ESO만 따로 설치해주기로 했다.</p>
<br>

<h1 id="eso-따로-설치">ESO 따로 설치</h1>
<pre><code class="language-bash">./scripts/bootstrap-platform.sh --from eso</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/4b7a1f38-b9fc-4c8d-9e9d-cec8f89e116d/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/feb172a6-ec2f-4481-9881-ebcf774a7cac/image.png" alt=""></p>
<p>ESO 설치는 완료됐는데 여전히 OutOfSync 상태였다. Pod 상태를 확인해봤다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/6dbf3023-d6c2-458e-a487-4ef7051d576d/image.png" alt=""></p>
<p>waypoint만 떠 있고 나머지 서비스들이 올라오지 않은 상태였다. ArgoCD가 자동으로 sync를 하지 못하고 있어서 강제로 sync를 실행해보기로 했다.</p>
<br>

<h1 id="argocd-sync-강제-실행">ArgoCD sync 강제 실행</h1>
<pre><code class="language-bash">kubectl -n argocd exec -it $(kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o jsonpath=&#39;{.items[0].metadata.name}&#39;) -- argocd app sync pposiraegi --insecure --server argocd-server:80</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/f017812e-4d65-407b-9089-524cd9946bcd/image.png" alt=""></p>
<p>ArgoCD CLI를 사용하려면 먼저 로그인이 필요하다고 떴다. 초기 admin 비밀번호를 확인한 뒤, port-forward로 ArgoCD 서버에 접근해서 로그인했다.</p>
<pre><code class="language-bash"># 초기 admin 비밀번호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | base64 -d

# port-forward 후 로그인
kubectl port-forward svc/argocd-server -n argocd 8080:80 &amp;
sleep 3
kubectl -n argocd exec -it $(kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o jsonpath=&#39;{.items[0].metadata.name}&#39;) -- argocd login localhost:8080 --insecure --username admin --password [위에서 확인한 비밀번호]</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/1199e592-d15e-48cd-b2c3-ac35cafe0734/image.png" alt=""></p>
<p>로그인 성공. 이제 sync를 다시 실행했다.</p>
<pre><code class="language-bash">kubectl -n argocd exec -it $(kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o jsonpath=&#39;{.items[0].metadata.name}&#39;) -- argocd app sync pposiraegi --insecure --server localhost:8080</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/34246581-66da-4474-88c0-a2dbd7d896a2/image.png" alt=""></p>
<p>sync가 정상적으로 진행되는 것을 확인했다. 이제 production 네임스페이스의 Pod 상태를 확인해봤다.</p>
<br>

<h1 id="production-pod-확인">production Pod 확인</h1>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d67b91d6-135c-4301-9cb4-652388305426/image.png" alt=""></p>
<p>모든 서비스의 Pod가 정상적으로 올라왔다. 이제 실제 서비스에 접속해봤다.</p>
<br>

<h1 id="서비스-확인">서비스 확인</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/7d63db05-b1d5-49fc-8e73-658edc55c4b1/image.png" alt=""></p>
<p>pposiraegi.cloud에 정상적으로 접속되는 것을 확인했다. 길고 긴 삽질이 드디어 끝났다. </p>
<br>

<h1 id="마무리-정리">마무리 정리</h1>
<p>팀원은 MacOS 환경이라 bootstrap 스크립트가 한 번에 실행됐지만, Windows 환경에서는 여러 문제가 연달아 발생했다.</p>
<ul>
<li>Git Bash에서 <code>sudo</code> 권한 없음 → helm 수동 설치</li>
<li>helm 실행 경로 문제 → PATH 직접 설정</li>
<li>Windows에 <code>/proc</code> 경로가 없음 → Loki 설치 실패</li>
<li>kubectl이 aws CLI 경로를 인식하지 못함 → 심볼릭 링크 + kubeconfig 수동 수정</li>
<li>ESO 미설치로 인한 CRD 누락 → ESO 단독 설치</li>
</ul>
<p>같은 스크립트라도 OS 환경에 따라 전혀 다르게 동작할 수 있다는 걸 몸소 경험했다. 다음에 스크립트를 작성할 때는 OS 환경 분기 처리를 꼭 고려해야겠다고 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Windows에서 EKS 붙이기 — Git Bash, helm, kubeconfig 삽질 총정리 (경로/인증/권한)]]></title>
            <link>https://velog.io/@lee_nah/Windows%EC%97%90%EC%84%9C-EKS-%EB%B6%99%EC%9D%B4%EA%B8%B0-Git-Bash-helm-kubeconfig-%EC%82%BD%EC%A7%88-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EA%B2%BD%EB%A1%9C%EC%9D%B8%EC%A6%9D%EA%B6%8C%ED%95%9C</link>
            <guid>https://velog.io/@lee_nah/Windows%EC%97%90%EC%84%9C-EKS-%EB%B6%99%EC%9D%B4%EA%B8%B0-Git-Bash-helm-kubeconfig-%EC%82%BD%EC%A7%88-%EC%B4%9D%EC%A0%95%EB%A6%AC-%EA%B2%BD%EB%A1%9C%EC%9D%B8%EC%A6%9D%EA%B6%8C%ED%95%9C</guid>
            <pubDate>Tue, 05 May 2026 10:57:23 GMT</pubDate>
            <description><![CDATA[<p>Terraform apply와 GitHub Actions CI/CD까지 성공한 후, 이제 EKS 클러스터에 플랫폼 컴포넌트를 설치할 차례였다. 팀원이 만들어준 <code>bootstrap-platform.sh</code> 스크립트를 실행하면 ArgoCD, Karpenter, Istio 등을 한 번에 설치할 수 있다.</p>
<br>

<h1 id="powershell-및-vs-code에서-실행-불가">PowerShell 및 VS Code에서 실행 불가</h1>
<p>처음에는 VS Code 터미널에서 그냥 실행하려 했다.</p>
<pre><code class="language-bash">./scripts/bootstrap-platform.sh</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e0e129ec-b249-4da6-b86b-e73e3469897b/image.png" alt=""></p>
<p>당연히 실패했다. <code>.sh</code> 파일은 Linux/Mac 환경의 bash 스크립트이기 때문에, Windows의 PowerShell이나 VS Code 기본 터미널에서는 실행할 수 없다. bash를 실행할 수 있는 환경이 필요했다.</p>
<br>

<h1 id="git-bash-설치--helm-설치">Git Bash 설치 + helm 설치</h1>
<p><a href="https://git-scm.com/download/win">https://git-scm.com/download/win</a> 에서 Git을 설치하면 Git Bash가 함께 설치된다. Git Bash는 Windows에서 bash 명령어를 쓸 수 있게 해주는 터미널 환경이다.</p>
<p>설치 후 Git Bash를 열고 다시 시도했다.</p>
<pre><code class="language-bash">cd /c/pposiraegi-ecommerce
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm
./scripts/bootstrap-platform.sh</code></pre>
<p>이번엔 <code>helm</code>이 설치되어 있지 않다는 오류가 떴다.</p>
<pre><code class="language-bash">[ERROR] helm 미설치</code></pre>
<p>helm은 Kubernetes 패키지 매니저로, 차트(chart)라는 단위로 애플리케이션을 클러스터에 쉽게 배포할 수 있게 해준다. bootstrap 스크립트 내부에서 helm을 사용하기 때문에 반드시 설치가 필요했다.</p>
<p>Linux라면 <code>sudo</code> 명령어로 간단히 설치할 수 있지만, Git Bash 환경에서는 sudo 권한이 없어서 수동으로 다운받아 설치했다.</p>
<pre><code class="language-bash">curl -L https://get.helm.sh/helm-v3.16.0-windows-amd64.zip -o helm.zip
unzip helm.zip
mv windows-amd64/helm.exe ./helm.exe
export PATH=$PATH:/c/pposiraegi-ecommerce</code></pre>
<br>

<h1 id="eks-인증-문제">EKS 인증 문제</h1>
<p>helm까지 설치하고 다시 실행했더니 이번엔 EKS 인증 오류가 떴다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/bbba661c-aa3b-467a-8907-44d3f6e49ef7/image.png" alt=""></p>
<p><code>kubectl get nodes</code>도 동일한 오류가 발생했다. <code>aws eks get-token</code>은 정상적으로 토큰을 반환하는데, kubectl이 이를 제대로 활용하지 못하는 상황이었다.</p>
<p>kubeconfig를 확인해보니 <code>command: aws</code>로 설정되어 있었는데, Git Bash 환경에서 aws CLI의 경로를 제대로 찾지 못하는 것이 원인이었다.</p>
<blockquote>
<p>kubeconfig란? kubectl이 어떤 클러스터에 어떻게 접속할지 정의해둔 설정 파일이다. 보통 <code>~/.kube/config</code> 경로에 위치한다.</p>
</blockquote>
<br>

<h1 id="kubeconfig-초기화-및-경로-문제-해결">kubeconfig 초기화 및 경로 문제 해결</h1>
<p>전체 경로로 바꿔봤지만 경로에 공백이 포함되어 있어 또 실패했다. kubeconfig를 완전히 초기화하고 다시 시도했다.</p>
<pre><code class="language-bash">rm ~/.kube/config
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/1901e0ef-a642-446b-8de8-76c8288ad9ef/image.png" alt=""></p>
<p>여전히 안 됐다. kubeconfig 내용을 직접 확인해봤다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e771f43c-cd5c-4f65-ad73-bee888a936f4/image.png" alt=""></p>
<p><code>command: aws</code> 부분이 문제였다. aws CLI의 전체 경로로 바꿔야 하는데, Windows 특성상 경로에 공백이 포함되어 있어(<code>C:\Program Files\...</code>) 그대로 쓸 수가 없었다.</p>
<p>먼저 aws CLI의 실제 경로를 확인했다.</p>
<pre><code class="language-bash">which aws</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/1e582c44-39e6-4421-b7e1-7816f36ed032/image.png" alt=""></p>
<p>공백 문제를 우회하기 위해 심볼릭 링크를 만들기로 했다.</p>
<br>

<h1 id="심볼릭-링크-생성">심볼릭 링크 생성</h1>
<p>심볼릭 링크란 특정 파일이나 경로를 가리키는 바로가기 파일이다. 공백 없는 경로에 링크를 걸어두면 경로 문제를 우회할 수 있다.</p>
<pre><code class="language-bash">ln -s &quot;/c/Program Files/Amazon/AWSCLIV2/aws.exe&quot; ~/aws.exe
sed -i &#39;s|command: aws|command: /c/Users/gkthf/aws.exe|g&#39; ~/.kube/config
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/96960368-a431-44d0-88c9-c310cd7b3d36/image.png" alt=""></p>
<p>kubectl이 Windows 네이티브 바이너리라 Windows 경로 형식으로 바꿔서 다시 시도했다.</p>
<pre><code class="language-bash">sed -i &#39;s|command: /c/Users/gkthf/aws.exe|command: C:\\Program Files\\Amazon\\AWSCLIV2\\aws.exe|g&#39; ~/.kube/config
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/9a42c4ea-5342-4d04-b52b-bab60b81e2c8/image.png" alt=""></p>
<p>경로는 이제 제대로 찾는데, 이번엔 인증 문제가 또 떴다. kubeconfig 상태를 다시 확인했다.</p>
<pre><code class="language-bash">cat ~/.kube/config | grep &quot;command:&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/5b062948-42ac-4ec0-8f9b-6d1fc14c9570/image.png" alt=""></p>
<p>경로는 맞는데 AWS_PROFILE 환경변수가 제대로 전달되지 않는 것 같아서, kubeconfig에 profile 설정이 있는지 확인했다.</p>
<pre><code class="language-bash">cat ~/.kube/config | grep -A3 &quot;env:&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/71c05580-00f6-4507-b00f-184df7171c06/image.png" alt=""></p>
<p>profile은 존재했다. 그렇다면 AWS 자체 권한 문제일 수 있겠다 싶어 AWS 콘솔에 접속해서 확인해봤다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/306ad71a-dbae-4312-a62d-e2f3a7102bf4/image.png" alt=""></p>
<p>IAM 사용자 목록을 보니 <code>nahyung</code>과 <code>jihoon</code>은 있는데 <code>terraform-user</code>가 없었다. kubeconfig에는 <code>terraform-user</code> 프로파일로 인증하도록 설정되어 있었으니 당연히 권한 오류가 날 수밖에 없었다.</p>
<p><code>terraform-user</code>를 IAM에 생성한 뒤 다시 <code>kubectl get nodes</code>를 실행했다.</p>
<br>

<h1 id="권한-할당">권한 할당</h1>
<p>연결은 됐는데 이번엔 권한이 없다는 오류가 떴다. EKS는 IAM 사용자가 존재하더라도 클러스터 내부에 별도로 접근 권한을 부여해야 한다.</p>
<p>AWS 콘솔에서 다음 순서로 권한을 추가했다.</p>
<p><strong>EKS → 해당 클러스터 → Access entries → terraform-user 선택 → Add access policy → AmazonEKSClusterAdminPolicy 추가</strong></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/c8716bf1-b0e5-48e7-a390-ee578222fe07/image.png" alt=""></p>
<p>추가 완료 후 다시 실행했다.</p>
<pre><code class="language-bash">kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/7fd3a31d-6318-4b05-a84b-c02ec30d5df3/image.png" alt=""></p>
<p>노드 목록이 정상적으로 출력됐다.</p>
<br>

<h1 id="bootstrap-실행">bootstrap 실행</h1>
<p>경로 문제와 권한 문제가 모두 해결되어 드디어 bootstrap 스크립트를 실행할 수 있었다.</p>
<pre><code class="language-bash">export PATH=$PATH:/c/pposiraegi-ecommerce
helm version
./scripts/bootstrap-platform.sh</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/5f84aacf-f441-45c4-8883-0fe056d54e10/image.png" alt=""></p>
<p>bootstrap이 정상적으로 실행되며 ArgoCD, Karpenter, Istio 등 플랫폼 컴포넌트들이 클러스터에 설치되기 시작했다. 삽질이 길었지만 결국 해결됐다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD 삽질기2] GitHub Actions - backend쪽 고치기]]></title>
            <link>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B02-GitHub-Actions-backend%EC%AA%BD-%EA%B3%A0%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B02-GitHub-Actions-backend%EC%AA%BD-%EA%B3%A0%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 May 2026 09:50:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lee_nah/post/d67f5871-6c9e-435a-98d1-0cc33881f11f/image.png" alt="">
저번 포스팅에서 프론트엔드 CI 쪽은 수정했지만, 백엔드 4개 서비스는 해결하지 못했다. 이번 포스팅에서는 백엔드 쪽 오류를 수정한 과정을 정리해보려 한다.</p>
<br>

<h1 id="production-not-found-에러">&quot;production&quot; not found 에러</h1>
<p>이 에러는 production 네임스페이스가 존재하지 않아 발생하는 오류다.
원인을 파악해보니, terraform apply만 완료된 상태에서 아직 부트스트랩을 실행하지 않았기 때문에 발생하는 문제로 보였다.
기존 워크플로우에는 ECR에 이미지가 푸시된 이후 kubectl rollout restart를 실행하는 단계가 있었는데, ArgoCD가 자동으로 Sync를 수행하므로 해당 스텝은 사실상 불필요했다. 따라서 deploy-all.yml에서 아래 구간을 제거했다.</p>
<pre><code class="language-yaml">- name: Update kubeconfig
        run: aws eks update-kubeconfig --region ${{ env.AWS_REGION }} --name ${{ env.EKS_CLUSTER }}

      - name: Rollout restart (ArgoCD sync fallback)
        run: |
          kubectl rollout restart deployment/${{ matrix.service }} -n production</code></pre>
<p>그런 다음 자동으로 Actions가 실행되기 때문에 기다려주었다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/52a93c72-fbfe-4b86-83d0-4898345a4ba0/image.png" alt=""></p>
<p>수정 후 GitHub Actions가 자동으로 트리거되었고, 결과는 성공이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD 삽질기1] GitHub Actions로 EKS 자동 배포 구축하며 만난 오류들]]></title>
            <link>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B01-GitHub-Actions</link>
            <guid>https://velog.io/@lee_nah/CICD-%EC%82%BD%EC%A7%88%EA%B8%B01-GitHub-Actions</guid>
            <pubDate>Sun, 03 May 2026 06:48:51 GMT</pubDate>
            <description><![CDATA[<h1 id="github-actions-조직-정책으로-서드파티-액션-차단">GitHub Actions 조직 정책으로 서드파티 액션 차단</h1>
<p>처음에 깃허브 argocd가 안되어서 왜 그런가 봤더니</p>
<p>조직 레벨에서 외부 액션 사용이 막혀있어서 pnpm/action-setup@v3 같은 액션을 못 쓰는 문제가 생겼다.
이거를 프로젝트 레포에서 보니 변경이 막혀있어서</p>
<p>조직 Settings에서 &quot;Allow all actions&quot; 으로 변경해서 해결하였다.</p>
<br>


<h1 id="terraformtfvars가-gitignore에-막혀서-변수-못-읽는-문제">terraform.tfvars가 .gitignore에 막혀서 변수 못 읽는 문제</h1>
<p>보안상 tfvars 파일은 git에 올리지 않는데, GitHub Actions는 로컬 파일을 못 읽는 문제도 발생했다. 이는 CI 실행 시 GitHub Secrets에서 값을 주입해서 tfvars를 동적으로 생성하는 방식으로 해결하였다.</p>
<br>

<h1 id="dockerfile-못찾는-오류">DockerFile 못찾는 오류</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/8f18acde-bd3d-4c2e-9c36-e5366f275be4/image.png" alt=""></p>
<p>이렇게 apply까진 했는데 오류가 떴다.
에러를 확인해보니 DockerFIle을 못찾는 오류이다.</p>
<p>워크플로우에서는 working-directory: ./backend 로 설정했는데 실제 Dockerfile 위치가 다른것이다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b63cf111-595f-4862-8ca7-782f896b85a2/image.png" alt=""></p>
<p>이렇게 백엔드 경로에 DockerFile이 없어서, </p>
<p>deploy-all.yml에서 </p>
<pre><code class="language-yaml">      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: pposiraegi-${{ matrix.service }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build --build-arg MODULE_NAME=${{ matrix.service }} -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest</code></pre>
<p>working-directory: ./backend 이 줄을 삭제해준다.</p>
<p>그리고나서 다시 Actions를 실행해본다.</p>
<br>

<h1 id="프론트엔드-오류">프론트엔드 오류</h1>
<h2 id="패키지-이름-오류">패키지 이름 오류</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/cb920342-7396-4c6e-b118-d7e28545a83a/image.png" alt="">
이번에는 프론트엔드쪽이 오류가 떴다.</p>
<p>프론트엔드 패키지 이름이 frontend가 아닌 것이다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b1285820-298f-4c27-a9be-154735907ba6/image.png" alt=""></p>
<p>Get-Content해서 보니 timedeal-front라고 뜬다.</p>
<p>따라서 deploy-all.yml에서 패키지이름을 timedeal-front로 바꿔준다
(json을 frontend로 바꾸는게 낫지 않나 싶었는데 다른곳에서 참조할 경우가 있기 때문에 json에 맞추어서 depoly-all 파일을 바꿔준다.)</p>
<pre><code class="language-yaml">##기존
      - name: Build Frontend
        run: pnpm turbo run build --filter=frontend
##바꾼 코드
      - name: Build Frontend
        run: pnpm turbo run build --filter=timedeal-front</code></pre>
<p>이렇게 코드를 바꿔준다. </p>
<br>

<h2 id="eslint-오류로-빌드-실패">ESLint 오류로 빌드 실패</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/cc26345a-83db-4d56-a607-4f5e037b642a/image.png" alt=""></p>
<p>이렇게 오류가 또 생겼는데 찾아보니, React useEffect missing dependencies, 미사용 변수 선언 등 ESLint 규칙 위반으로 빌드 실패한 것이었다.</p>
<p>프론트엔드도 내 담당이기 때문에 코드를 직접 수정해준다.</p>
<pre><code class="language-bash">Run pnpm turbo run build --filter=timedeal-front

Attention:
Turborepo now collects completely anonymous telemetry regarding usage.
This information is used to shape the Turborepo roadmap and prioritize features.
You can learn more, including how to opt-out if you&#39;d not like to participate in this anonymous program, by visiting the following URL:
https://turborepo.dev/docs/telemetry


   • Packages in scope: timedeal-front
   • Running build in 1 packages
   • Remote caching disabled

timedeal-front:build
cache miss, executing b851ac8de420a9b8

&gt; timedeal-front@0.1.0 build /home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend
&gt; react-scripts build

Creating an optimized production build...

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.

Failed to compile.

[eslint] 
src/api/auth.js
  Line 167:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/api/config.js
  Line 7:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/api/order.js
  Line 118:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/api/timedeal.js
  Line 182:1:  Assign object to a variable before exporting as module default  import/no-anonymous-default-export

src/pages/AddressManager.jsx
  Line 30:6:  React Hook useEffect has missing dependencies: &#39;navigate&#39; and &#39;user&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps

src/pages/AdminPage.jsx
  Line 57:6:  React Hook useEffect has a missing dependency: &#39;fetchDeals&#39;. Either include it or remove the dependency array  react-hooks/exhaustive-deps

src/pages/MyPage.jsx
  Line 24:6:    React Hook useEffect has missing dependencies: &#39;navigate&#39; and &#39;user&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps
  Line 227:28:  &#39;v&#39; is assigned to itself                                                                                                 no-self-assign

src/pages/OrderResult.jsx
  Line 26:6:  React Hook useEffect has missing dependencies: &#39;fetchOrder&#39; and &#39;order&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps

src/pages/TimeDealDetail.jsx
  Line 41:6:  React Hook useEffect has missing dependencies: &#39;fetchDeal&#39;, &#39;fetchRelatedDeals&#39;, and &#39;user&#39;. Either include them or remove the dependency array  react-hooks/exhaustive-deps
  Line 47:6:  React Hook useEffect has a missing dependency: &#39;fetchDeal&#39;. Either include it or remove the dependency array                                     react-hooks/exhaustive-deps

src/pages/TimeDealList.jsx
  Line 6:26:   &#39;logout&#39; is defined but never used            no-unused-vars
  Line 12:16:  &#39;setUser&#39; is assigned a value but never used  no-unused-vars

src/pages/WishList.jsx
  Line 18:6:  React Hook useEffect has a missing dependency: &#39;navigate&#39;. Either include it or remove the dependency array  react-hooks/exhaustive-deps


 ELIFECYCLE  Command failed with exit code 1.
Error: timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)
 ERROR  timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)

 Tasks:    0 successful, 1 total
Cached:    0 cached, 1 total
  Time:    13.829s 
Failed:    timedeal-front#build

 ERROR  run failed: command  exited (1)
Error: Process completed with exit code 1.</code></pre>
<p>일단 오류 로그는 이렇게 떴다. </p>
<ul>
<li>frontend/src/api/auth.js</li>
<li>frontend/src/api/config.js</li>
<li>frontend/src/api/order.js</li>
<li>frontend/src/api/timedeal.js</li>
<li>frontend/src/pages/AddressManager.jsx</li>
<li>frontend/src/pages/AdminPage.jsx</li>
<li>frontend/src/pages/MyPage.jsx</li>
<li>frontend/src/pages/OrderResult.jsx</li>
<li>frontend/src/pages/TimeDealDetail.jsx</li>
<li>frontend/src/pages/TimeDealList.jsx</li>
<li>frontend/src/pages/WishList.jsx
이렇게, 
고쳐야할 파일이 11개나 되기 때문에 아찔했다.
그래도 임시방편으로 deploy-all에서 CI: false 하는것보단 근본적으로 고치는게 내 성격에 맞기 때문에 고쳐준다.</li>
</ul>
<p>일단 코드가 frontend/src/api/auth.js같은 경우에는</p>
<pre><code class="language-js">##중략
// 401 인터셉터: 토큰 만료 시 자동 로그아웃 + 로그인 페이지 이동
axios.interceptors.response.use(
  res =&gt; res,
  err =&gt; {
    if (err.response?.status === 401) {
      localStorage.removeItem(&#39;accessToken&#39;);
      localStorage.removeItem(&#39;refreshToken&#39;);
      localStorage.removeItem(&#39;user&#39;);
      sessionStorage.removeItem(&#39;user&#39;);
      // 현재 페이지가 /login이 아닐 때만 리다이렉트
      if (!window.location.pathname.startsWith(&#39;/login&#39;)) {
        window.location.href = &#39;/login?expired=1&#39;;
      }
    }
    return Promise.reject(err);
  }
);

export default { login, register, logout, getCurrentUser, saveAddress, getAddress };
</code></pre>
<p>이렇게 되어있는데 맨 마지막줄을</p>
<pre><code class="language-js">const authAPI = { login, register, logout, getCurrentUser, saveAddress, getAddress };
export default authAPI;</code></pre>
<p>이런식으로 고쳐준다. 
(거의 모든 파일이 저렇게 되어있어서 저런식으로 고쳐준다.)</p>
<p>또는 
useEffect에서 발생하는 오류가 있는데, 이는 dependency array 문제다.
React의 useEffect는 두 번째 인자로 dependency array를 받는데, useEffect 내부에서 사용하는 변수나 함수가 dependency array에 없으면 ESLint가 경고를 띄운다.</p>
<p>예를 들어 AddressManager.jsx의 경우</p>
<pre><code class="language-js">jsuseEffect(() =&gt; {
    if (!user) { navigate(&#39;/login&#39;); return; }
    ...
}, []); // ← navigate, user를 사용하는데 dependency array가 비어있음</code></pre>
<p>이런 경우 navigate와 user를 dependency array에 추가해줘야 한다.</p>
<pre><code class="language-js">jsuseEffect(() =&gt; {
    if (!user) { navigate(&#39;/login&#39;); return; }
    ...
}, [navigate, user]); // ← 추가</code></pre>
<p>단, TimeDealDetail.jsx처럼 fetchDeal, fetchRelatedDeals 같은 함수를 dependency에 넣으면 무한루프가 발생할 수 있다. 이런 경우 해당 함수를 useCallback으로 감싸서 함수 참조가 변경되지 않도록 처리해준다.</p>
<br>

<p>그리고나서 Actions가 잘 돌아가는지 확인한다.</p>
<br>

<br>

<h2 id="fetchdeals-fetchorder를-usecallback으로-감싸-무한루프-방지">fetchDeals, fetchOrder를 useCallback으로 감싸 무한루프 방지</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/7d8988f4-0332-4966-8f20-02a45b21add9/image.png" alt="">
Actions를 돌렸더니 이러한 오류가 떴다. </p>
<pre><code class="language-bash">Run pnpm turbo run build --filter=timedeal-front

Attention:
Turborepo now collects completely anonymous telemetry regarding usage.
This information is used to shape the Turborepo roadmap and prioritize features.
You can learn more, including how to opt-out if you&#39;d not like to participate in this anonymous program, by visiting the following URL:
https://turborepo.dev/docs/telemetry


   • Packages in scope: timedeal-front
   • Running build in 1 packages
   • Remote caching disabled

timedeal-front:build
cache miss, executing 381ed73731ab9079

&gt; timedeal-front@0.1.0 build /home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend
&gt; react-scripts build

Creating an optimized production build...

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.

Failed to compile.

[eslint] 
src/pages/AdminPage.jsx
  Line 57:7:  &#39;fetchDeals&#39; was used before it was defined                                                                                                                                             no-use-before-define
  Line 59:9:  The &#39;fetchDeals&#39; function makes the dependencies of useEffect Hook (at line 57) change on every render. To fix this, wrap the definition of &#39;fetchDeals&#39; in its own useCallback() Hook  react-hooks/exhaustive-deps

src/pages/OrderResult.jsx
  Line 26:6:  React Hook useEffect has a missing dependency: &#39;fetchOrder&#39;. Either include it or remove the dependency array  react-hooks/exhaustive-deps
Error: timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)
 ERROR  timedeal-front#build: command (/home/runner/work/pposiraegi-ecommerce/pposiraegi-ecommerce/frontend) /home/runner/setup-pnpm/node_modules/.bin/pnpm run build exited (1)


 ELIFECYCLE  Command failed with exit code 1.

 Tasks:    0 successful, 1 total
Cached:    0 cached, 1 total
  Time:    13.493s 
Failed:    timedeal-front#build

 ERROR  run failed: command  exited (1)
Error: Process completed with exit code 1.</code></pre>
<p>아까보다는 나아졌다.
이제는 AdminPage.jsx랑 OrderResult.jsx만 에러가 뜨고 있다.</p>
<p>AdminPage.jsx - import에 useCallback 추가하고 fetchDeals를 useCallback으로 감싸고 useEffect 위로 올려 수정해준다.</p>
<p>OrderResult.jsx - import에 useCallback 추가하고 fetchOrder를 useCallback으로 감싸 수정해준다.</p>
<p>그리고 다시 Actions를 실행해준다.</p>
<br>

<h2 id="fetchdeals-fetchorder-함수-누락으로-인한-not-defined-오류-수정">fetchDeals, fetchOrder 함수 누락으로 인한 not defined 오류 수정</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/3a868f00-36ad-42c2-9570-49252aab4a4a/image.png" alt=""></p>
<p>방금 고친 AdminPage랑 OrderResult에서 여러가지를 수정하며, 쓰지 않는 함수를 삭제했는데 잘못 삭제하여 함수를 찾지 못한다고 에러가 떠서 다시 추가해준다. </p>
<p>그리고 또 Actions를 기대해준다.</p>
<br>

<h2 id="프론트엔드-마침내-성공">프론트엔드 마침내 성공</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/ba67560e-54f4-4f7b-a3cc-e2fd9c043c62/image.png" alt=""></p>
<p>마침내 프론트엔드 빌드가 성공했다. 길고 길었던 ESLint 오류와의 싸움이 끝났다. </p>
<p>이제 4개의 백엔드와의 긴 싸움이 남았다.</p>
<br>




]]></description>
        </item>
        <item>
            <title><![CDATA[ArgoCD sync는 됐는데 Pod가 안 떠요 — 원인 추적기]]></title>
            <link>https://velog.io/@lee_nah/%EC%99%9C%EC%A3%BD%EB%8A%94%EC%A7%80-%EB%AA%A8%EB%A5%B4%EA%B2%A0%EB%8A%94-production-pod%EA%B3%A0%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/%EC%99%9C%EC%A3%BD%EB%8A%94%EC%A7%80-%EB%AA%A8%EB%A5%B4%EA%B2%A0%EB%8A%94-production-pod%EA%B3%A0%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Thu, 30 Apr 2026 06:28:06 GMT</pubDate>
            <description><![CDATA[<h1 id="crashloopbackoff-해결기--eks-pod-살리기-feat-argocd-재설치">CrashLoopBackOff 해결기 — EKS Pod 살리기 (feat. ArgoCD 재설치)</h1>
<p><a href="https://velog.io/@lee_nah/ArgoCD-Sync-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A0%88%ED%8F%AC-%EC%98%A4%ED%83%80%EB%B6%80%ED%84%B0-RDS-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9%EA%B9%8C%EC%A7%80">저번 포스팅</a>에서 ArgoCD 연결까지는 성공했는데, production Pod를 띄우는 과정에서 <code>CrashLoopBackOff</code> 오류가 발생했다.</p>
<p>이번 포스팅에서는 원인을 추적하고 해결하는 과정을 기록한다.</p>
<blockquote>
<p><strong>CrashLoopBackOff란?</strong><br>컨테이너가 시작됐다가 바로 죽는 걸 반복하는 상태다.<br>쿠버네티스가 &quot;계속 죽으니까 잠깐 기다렸다가 다시 시작할게&quot; 하면서 점점 재시작 간격을 늘린다.<br>보통 앱 설정 오류, DB 연결 실패, 환경변수 누락 등이 원인이다.</p>
</blockquote>
<hr>
<h2 id="1-eks-연결">1. EKS 연결</h2>
<p>먼저 로컬에서 EKS 클러스터에 접근할 수 있도록 kubeconfig를 설정한다.</p>
<pre><code class="language-bash"># kubeconfig 연결
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm

# 노드 상태 확인
kubectl get nodes</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/4f64912b-9503-4533-8685-36b04d318f65/image.png" alt=""></p>
<p>노드가 <code>Ready</code> 상태로 뜨면 클러스터 연결은 정상이다.</p>
<blockquote>
<p><strong>노드(Node)란?</strong><br>쿠버네티스에서 실제로 컨테이너가 실행되는 서버(가상머신)다.<br>EKS에서는 EC2 인스턴스가 노드 역할을 한다.</p>
</blockquote>
<hr>
<h2 id="2-어떤-pod가-죽었는지-확인">2. 어떤 Pod가 죽었는지 확인</h2>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/288f7dc3-c5d5-49b1-9420-297400efd655/image.png" alt=""></p>
<p>아무것도 뜨지 않았다.</p>
<blockquote>
<p><strong><code>-n production</code>이란?</strong><br><code>-n</code>은 네임스페이스(namespace)를 지정하는 옵션이다.<br>쿠버네티스는 리소스를 네임스페이스라는 공간으로 분리해서 관리한다.<br><code>production</code>은 실제 서비스가 올라가는 공간이다.</p>
</blockquote>
<p>production 네임스페이스에 Pod가 하나도 없다는 건, <strong>ArgoCD가 아직 sync를 하지 않은 것</strong>이다.<br>즉, GitHub 레포에 있는 매니페스트 파일들이 클러스터에 아직 반영되지 않은 상태라는 뜻이다.</p>
<p>ArgoCD부터 다시 설치하고 연결해야 한다.</p>
<hr>
<h2 id="3-argocd-재설치">3. ArgoCD 재설치</h2>
<pre><code class="language-bash"># argocd 네임스페이스 생성
kubectl create namespace argocd

# 공식 매니페스트 적용
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/0afaadbd-cab8-4f1e-9315-702d3f885b50/image.png" alt=""></p>
<p><code>created</code> 메시지들이 쭉 뜨면 설치가 진행 중인 것이다.</p>
<p>설치 완료 후 Pod 상태를 확인한다:</p>
<pre><code class="language-bash">kubectl get pods -n argocd</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d01b43f8-f768-482b-b528-237f3b8dcb48/image.png" alt=""></p>
<p>모든 Pod의 STATUS가 <code>Running</code>으로 바뀌면 다음 단계로 넘어간다.<br>처음엔 <code>ContainerCreating</code>이나 <code>Pending</code>으로 뜨는 게 정상이니 잠깐 기다리면 된다.</p>
<hr>
<h2 id="4-argocd-ui-접속">4. ArgoCD UI 접속</h2>
<p>ArgoCD는 웹 UI를 제공한다. 포트 포워딩으로 로컬에서 접속한다.</p>
<pre><code class="language-bash">kubectl port-forward svc/argocd-server -n argocd 8080:443</code></pre>
<blockquote>
<p><strong>포트 포워딩이란?</strong><br>클러스터 내부 서비스를 외부에 노출하지 않고, 내 PC에서만 접근할 수 있도록 통로를 만드는 것이다.<br>위 명령어는 &quot;내 PC의 8080번 포트로 들어오는 요청을 ArgoCD 서버의 443 포트로 전달해줘&quot;라는 뜻이다.</p>
</blockquote>
<p>브라우저에서 <code>https://localhost:8080</code> 접속 후 로그인한다.</p>
<p>초기 비밀번호 확인 (PowerShell):</p>
<pre><code class="language-powershell">kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | ForEach-Object { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_)) }</code></pre>
<ul>
<li>아이디: <code>admin</code></li>
<li>비밀번호: 위 명령어 출력값</li>
</ul>
<blockquote>
<p>⚠️ 브라우저에서 &quot;안전하지 않은 연결&quot; 경고가 뜨면 <strong>고급 → 계속 진행</strong>을 누르면 된다.<br>자체 서명 인증서라서 뜨는 경고로, 로컬 개발 환경에서는 정상이다.</p>
</blockquote>
<hr>
<h2 id="5-namespace-및-secret-생성">5. namespace 및 Secret 생성</h2>
<p>ArgoCD가 배포할 <code>production</code> 네임스페이스와 DB/Redis 접속 정보를 미리 만들어줘야 한다.<br>이 작업을 먼저 해두지 않으면 Pod가 뜨다가 환경변수를 못 찾아서 바로 죽는다.</p>
<pre><code class="language-bash"># namespace, configmap 먼저 생성
kubectl apply -f kubernetes/base/namespace.yaml
kubectl apply -f kubernetes/base/configmap.yaml</code></pre>
<p>RDS, Redis 엔드포인트를 확인한다:</p>
<pre><code class="language-bash">terraform output rds_endpoint
terraform output elasticache_endpoint</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b9d9297d-043d-44f4-8a9e-f0362887a125/image.png" alt=""></p>
<p>확인한 엔드포인트로 Secret을 생성한다:</p>
<pre><code class="language-bash">kubectl create secret generic app-secret \
  --namespace=production \
  --from-literal=DB_HOST=RDS엔드포인트 \
  --from-literal=DB_USERNAME=pposiraegi \
  --from-literal=DB_PASSWORD=DB비밀번호 \
  --from-literal=REDIS_HOST=Redis엔드포인트 \
  --from-literal=JWT_SECRET=JWT시크릿</code></pre>
<blockquote>
<p><strong>Secret이란?</strong><br>비밀번호, API 키처럼 코드에 직접 넣으면 안 되는 민감한 값을 쿠버네티스 내부에서 안전하게 관리하는 리소스다.<br><code>--from-literal</code>로 값을 넣으면 자동으로 base64로 인코딩되어 저장된다.<br>Pod에서는 환경변수 형태로 이 값을 꺼내 쓸 수 있다.</p>
</blockquote>
<hr>
<h2 id="6-argocd-app-연결-및-sync">6. ArgoCD App 연결 및 Sync</h2>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml</code></pre>
<p>터미널에서 직접 sync한다:</p>
<pre><code class="language-bash"># argocd-server Pod 이름 먼저 확인
kubectl get pods -n argocd

# ArgoCD 서버에 로그인
kubectl -n argocd exec -it [argocd-server-Pod이름] -- \
  argocd login localhost:8080 --insecure --username admin --password [비밀번호]

# Sync 실행
kubectl -n argocd exec -it [argocd-server-Pod이름] -- \
  argocd app sync pposiraegi --insecure</code></pre>
<blockquote>
<p><strong>exec -it란?</strong><br>실행 중인 Pod 안으로 직접 들어가서 명령어를 실행하는 것이다.<br>마치 서버에 SSH로 접속해서 명령어를 치는 것과 같다.</p>
</blockquote>
<hr>
<h2 id="7-pod-상태-확인-및-로그-분석">7. Pod 상태 확인 및 로그 분석</h2>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p>Pod들이 뜨기 시작하면 각 Pod의 로그를 확인한다:</p>
<pre><code class="language-bash"># 현재 로그 확인
kubectl logs -n production [죽은Pod이름]

# 이미 죽은 컨테이너의 직전 로그 확인
kubectl logs -n production [죽은Pod이름] --previous</code></pre>
<p><code>order-service</code> Pod의 로그를 먼저 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/15c56d60-b3e4-42c7-a191-64df9bd89c69/image.png" alt=""></p>
<pre><code>Connect timed out</code></pre><p><strong>원인 분석:</strong></p>
<table>
<thead>
<tr>
<th>에러</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>Connect timed out</code></td>
<td>RDS(데이터베이스)에 접속 요청을 보냈는데 응답이 없음</td>
</tr>
<tr>
<td>원인</td>
<td>EKS 노드 → RDS 보안 그룹이 막혀있음</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>보안 그룹(Security Group)이란?</strong><br>AWS에서 서버 간 트래픽을 허용/차단하는 방화벽 역할을 한다.<br>RDS의 보안 그룹에 EKS 노드가 포함되어 있지 않으면 아무리 올바른 주소로 접속해도 연결이 차단된다.</p>
</blockquote>
<hr>
<h2 id="8-eks-노드-보안-그룹을-동적으로-추가">8. EKS 노드 보안 그룹을 동적으로 추가</h2>
<p>EKS는 배포할 때마다 보안 그룹 ID가 바뀐다.<br>그래서 ID를 하드코딩하지 않고, <strong>Terraform이 자동으로 가져오도록</strong> 동적으로 연결했다.</p>
<blockquote>
<p><strong>왜 동적으로 해야 할까?</strong><br><code>terraform apply</code>를 할 때마다 EKS가 새 보안 그룹을 만든다.<br>매번 수동으로 ID를 복사해서 넣으면 실수가 생기고 유지보수가 어렵다.<br>Terraform 모듈끼리 연결해두면 항상 최신 ID를 자동으로 참조한다.</p>
</blockquote>
<h3 id="1-modulessecurityvariablestf에-변수-추가">1) <code>modules/security/variables.tf</code>에 변수 추가</h3>
<pre><code class="language-hcl">variable &quot;eks_cluster_sg_id&quot; {
  description = &quot;EKS cluster security group ID&quot;
  default     = &quot;&quot;
}</code></pre>
<h3 id="2-modulessecuritymaintf-수정">2) <code>modules/security/main.tf</code> 수정</h3>
<p>RDS와 Redis 보안 그룹에 EKS 노드 접근 허용 규칙을 추가한다.</p>
<pre><code class="language-hcl"># RDS 보안 그룹 — PostgreSQL 5432 포트
ingress {
  from_port       = 5432
  to_port         = 5432
  protocol        = &quot;tcp&quot;
  security_groups = [var.eks_cluster_sg_id]
}

# Redis 보안 그룹 — Redis 6379 포트
ingress {
  from_port       = 6379
  to_port         = 6379
  protocol        = &quot;tcp&quot;
  security_groups = [var.eks_cluster_sg_id]
}</code></pre>
<h3 id="3-moduleseksoutputstf에-출력값-추가">3) <code>modules/eks/outputs.tf</code>에 출력값 추가</h3>
<p>EKS 모듈이 보안 그룹 ID를 외부로 내보낼 수 있도록 output을 추가한다.</p>
<pre><code class="language-hcl">output &quot;cluster_security_group_id&quot; {
  value = aws_eks_cluster.main.vpc_config[0].cluster_security_group_id
}</code></pre>
<h3 id="4-루트-maintf에서-모듈-간-연결">4) 루트 <code>main.tf</code>에서 모듈 간 연결</h3>
<pre><code class="language-hcl">module &quot;security&quot; {
  source = &quot;./modules/security&quot;

  project_name      = var.project_name
  vpc_id            = module.networking.vpc_id
  eks_cluster_sg_id = module.eks.cluster_security_group_id  # EKS에서 자동으로 가져옴
}</code></pre>
<p>이렇게 연결해두면 <code>terraform apply</code>를 할 때마다 EKS가 새로 만드는 보안 그룹 ID를 자동으로 참조한다.</p>
<p>수정 후 적용:</p>
<pre><code class="language-bash">terraform apply -var-file=&quot;terraform.tfvars&quot;</code></pre>
<hr>
<h2 id="9-terraform-apply-후-argocd-재sync">9. Terraform apply 후 ArgoCD 재sync</h2>
<p>apply 후 ArgoCD 상태를 확인했는데 뭔가 이상해 보였다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/f334aa05-572c-4390-a7b1-d1864ee77fe6/image.png" alt=""></p>
<p>혹시 Secret이나 엔드포인트가 잘못됐나 싶어서 다시 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/fb794773-7bbe-4047-a8f8-701b3756363e/image.png" alt=""></p>
<p>Secret과 엔드포인트는 정상적으로 등록된 것을 확인했다. 그래서 다시 sync를 시도했다.</p>
<pre><code class="language-bash"># ArgoCD 서버에 로그인
kubectl -n argocd exec -it argocd-server-78f5bb67d5-xt9g6 -- \
  argocd login localhost:8080 --insecure --username admin --password [패스워드]

# Sync 실행
kubectl -n argocd exec -it argocd-server-78f5bb67d5-xt9g6 -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p>각 명령어의 의미를 정리하면:</p>
<p><strong>첫 번째 명령어 — ArgoCD 로그인</strong></p>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>kubectl -n argocd exec -it [Pod이름]</code></td>
<td>argocd 네임스페이스의 argocd-server Pod 안으로 접속</td>
</tr>
<tr>
<td><code>argocd login localhost:8080</code></td>
<td>Pod 안에서 ArgoCD에 로그인</td>
</tr>
<tr>
<td><code>--insecure</code></td>
<td>HTTPS 인증서 검증 건너뛰기</td>
</tr>
<tr>
<td><code>--username admin --password [패스워드]</code></td>
<td>admin 계정으로 로그인</td>
</tr>
</tbody></table>
<p><strong>두 번째 명령어 — App Sync</strong></p>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>kubectl -n argocd exec -it [Pod이름]</code></td>
<td>argocd-server Pod 안으로 접속</td>
</tr>
<tr>
<td><code>argocd app sync pposiraegi</code></td>
<td>pposiraegi 앱을 GitHub 레포와 동기화</td>
</tr>
<tr>
<td><code>--insecure</code></td>
<td>HTTPS 인증서 검증 건너뛰기</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/8cd3cb2d-d65c-475d-a8c4-d2e56317921a/image.png" alt=""></p>
<hr>
<h2 id="10-그런데-또-문제가--kubeconfig와-rbac">10. 그런데 또 문제가 — kubeconfig와 RBAC</h2>
<p>여기까지 했는데도 production Pod가 여전히 뜨지 않았다.</p>
<p>팀원과 공유하는 과정에서 원인이 두 가지 더 있다는 걸 알게 됐다.</p>
<p><strong>원인 1 — kubeconfig가 예전 엔드포인트를 가리키고 있음</strong></p>
<blockquote>
<p><code>terraform apply</code>를 다시 하면 EKS 클러스터가 재생성되면서 API 서버 엔드포인트 주소가 바뀐다.<br>그런데 로컬의 kubeconfig는 예전 주소를 그대로 들고 있기 때문에, <code>kubectl</code> 명령어가 존재하지 않는 클러스터에 계속 요청을 보내고 있던 것이다.<br>해결 방법은 간단하다. <code>aws eks update-kubeconfig</code>를 다시 실행해서 최신 엔드포인트로 갱신해주면 된다.</p>
</blockquote>
<pre><code class="language-bash">aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm</code></pre>
<p><strong>원인 2 — RBAC 권한 문제</strong></p>
<blockquote>
<p><strong>RBAC(Role-Based Access Control)란?</strong><br>쿠버네티스에서 &quot;누가 무엇을 할 수 있는지&quot; 권한을 관리하는 시스템이다.<br>ArgoCD가 production 네임스페이스에 Pod를 배포하려면 그에 맞는 권한이 부여되어 있어야 한다.<br>권한이 없으면 sync는 성공해도 실제 리소스가 생성되지 않거나 오류가 발생한다.</p>
</blockquote>
<p>이 두 가지 문제가 복합적으로 작용해서 production Pod가 계속 뜨지 않았던 것이다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 포스팅에서 겪은 문제들을 정리하면:</p>
<table>
<thead>
<tr>
<th>순서</th>
<th>문제</th>
<th>원인</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>CrashLoopBackOff</code></td>
<td>EKS → RDS 보안 그룹 차단</td>
<td>Terraform으로 동적 보안 그룹 연결</td>
</tr>
<tr>
<td>2</td>
<td>kubectl 연결 안 됨</td>
<td>kubeconfig가 예전 엔드포인트를 가리킴</td>
<td><code>update-kubeconfig</code> 재실행</td>
</tr>
<tr>
<td>3</td>
<td>Pod 배포 안 됨</td>
<td>ArgoCD RBAC 권한 누락</td>
<td>다음 포스팅에서 해결 예정</td>
</tr>
</tbody></table>
<p>트러블슈팅을 하다 보면 문제 하나를 해결하면 또 다른 문제가 나오는 게 일상이다.<br>RBAC 권한 설정과 부트스트랩 스크립트 적용 과정은 다음 포스팅에서 이어서 정리할 예정이다. 😅</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ArgoCD Sync 트러블슈팅 — 레포 오타부터 RDS 보안 그룹까지]]></title>
            <link>https://velog.io/@lee_nah/ArgoCD-Sync-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A0%88%ED%8F%AC-%EC%98%A4%ED%83%80%EB%B6%80%ED%84%B0-RDS-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@lee_nah/ArgoCD-Sync-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A0%88%ED%8F%AC-%EC%98%A4%ED%83%80%EB%B6%80%ED%84%B0-RDS-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Thu, 30 Apr 2026 04:58:53 GMT</pubDate>
            <description><![CDATA[<h1 id="argocd-sync-트러블슈팅--레포-경로-오류부터-rds-보안-그룹까지">ArgoCD Sync 트러블슈팅 — 레포 경로 오류부터 RDS 보안 그룹까지</h1>
<p>이번 포스팅은 ArgoCD Sync를 시도하다가 연속으로 두 가지 문제를 맞닥뜨린 트러블슈팅 기록이다.<br>결론부터 말하면 <strong>레포 이름 오타 → 폴더 경로 오류 → RDS 보안 그룹 차단</strong> 순서로 문제가 터졌고, 아직 완전히 해결 전 단계다.</p>
<hr>
<h2 id="사전-준비--엔드포인트-확인-및-리소스-생성">사전 준비 — 엔드포인트 확인 및 리소스 생성</h2>
<p>Sync 전에 RDS, ElastiCache 엔드포인트를 확인하고 쿠버네티스 리소스를 미리 만들어둔다.</p>
<pre><code class="language-bash"># RDS, ElastiCache 엔드포인트 확인
terraform output rds_endpoint
terraform output elasticache_endpoint</code></pre>
<pre><code class="language-bash"># namespace, configmap 생성
kubectl apply -f kubernetes/base/namespace.yaml
kubectl apply -f kubernetes/base/configmap.yaml</code></pre>
<pre><code class="language-bash"># DB, Redis, JWT 정보를 Secret으로 등록
kubectl create secret generic app-secret \
  --namespace=production \
  --from-literal=DB_HOST=pposiraegi-db.c5wkmcaauwn2.ap-northeast-2.rds.amazonaws.com \
  --from-literal=DB_USERNAME=pposiraegi \
  --from-literal=DB_PASSWORD=DB비밀번호 \
  --from-literal=REDIS_HOST=pposiraegi-redis.qka9g8.0001.apn2.cache.amazonaws.com \
  --from-literal=JWT_SECRET=JWT시크릿</code></pre>
<blockquote>
<p><strong>Secret이란?</strong><br>비밀번호, API 키처럼 외부에 노출되면 안 되는 값을 쿠버네티스 안에서 안전하게 관리하는 리소스다.<br><code>--from-literal</code>로 값을 직접 넣으면 자동으로 base64 인코딩되어 저장된다.</p>
</blockquote>
<hr>
<h2 id="트러블슈팅-1--argocd-sync-실패-레포경로-문제">트러블슈팅 1 — ArgoCD Sync 실패 (레포/경로 문제)</h2>
<h3 id="sync-시도">Sync 시도</h3>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml

# UI에서 SYNC → SYNCHRONIZE 클릭 후 반응 없음
# 터미널로 직접 해결하기로 함

# ArgoCD 파드 확인
kubectl get pods -n argocd

# ArgoCD 서버에 직접 로그인
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd login localhost:8080 --insecure --username admin --password NDiUuc1t8XJKmF2O

# 수동 Sync
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p>UI에서 Sync 버튼을 눌렀는데 아무 반응이 없어서, ArgoCD 서버 파드에 직접 exec로 들어가서 CLI로 진행했다.</p>
<hr>
<h3 id="문제-1--feateks-migration-브랜치를-못-찾음">문제 1 — <code>feat/eks-migration</code> 브랜치를 못 찾음</h3>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/05cba9d5-1ad9-4c60-8ce5-3fbaf2bb7e45/image.png" alt=""></p>
<p>브랜치를 찾지 못한다는 에러가 발생했다. 하나씩 확인했다.</p>
<pre><code class="language-bash"># 원격 브랜치 목록 확인
git branch -r</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e416a982-25da-494a-a53f-cd78f8374654/image.png" alt=""></p>
<p>→ 브랜치는 실제로 존재함.</p>
<pre><code class="language-bash"># 해당 브랜치에 커밋이 있는지 확인
git log --oneline origin/feat/eks-migration</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/0cc7fa60-d5b4-4639-ab90-b053f845e787/image.png" alt=""></p>
<p>→ 커밋도 있음.</p>
<pre><code class="language-bash"># argocd-app.yaml 내용 확인
cat argocd-app.yaml</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/51d96ba5-531d-4b6e-b474-e07dcdf15b51/image.png" alt=""></p>
<p>→ path 설정은 이상 없음.</p>
<pre><code class="language-bash"># 실제 레포 이름 확인
git remote -v</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/81c98362-0171-4fb0-80c3-5de59213a653/image.png" alt=""></p>
<p><strong>원인 발견 — 레포 이름 오타</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>URL</th>
</tr>
</thead>
<tbody><tr>
<td>argocd-app.yaml에 입력된 값</td>
<td><code>https://github.com/Goorm4I/pposiraegi-ecommerce-msa</code> ❌</td>
</tr>
<tr>
<td>실제 레포 주소</td>
<td><code>https://github.com/Goorm4I/pposiraegi-ecommerce</code> ✅</td>
</tr>
</tbody></table>
<p><code>argocd-app.yaml</code>의 <code>repoURL</code>을 올바른 주소로 수정 후 재적용했다.</p>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<hr>
<h3 id="문제-2--app-path-does-not-exist">문제 2 — <code>app path does not exist</code></h3>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/25748db8-8b0d-491f-a216-f3095455f65d/image.png" alt=""></p>
<p>레포 이름은 고쳤는데 이번엔 경로 문제가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/af76d346-6011-43fd-8419-1286e7182649/image.png" alt=""></p>
<p><strong>원인</strong></p>
<p>ArgoCD는 레포 루트에서 <code>kubernetes/</code> 폴더를 찾는데,<br>실제 폴더 구조는 <code>infrastructure/kubernetes/</code> 였다.</p>
<pre><code>pposiraegi-ecommerce/
└── infrastructure/
    └── kubernetes/   ← 실제 위치</code></pre><p><code>argocd-app.yaml</code>의 <code>path</code>를 <code>infrastructure/kubernetes</code>로 수정 후 재적용했다.</p>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/503bb0e6-9847-4d7f-baf5-6638b81a178e/image.png" alt=""></p>
<p>→ 연결 성공 🎉</p>
<hr>
<h2 id="트러블슈팅-2--pod-crashloopbackoff-rds-보안-그룹-차단">트러블슈팅 2 — Pod CrashLoopBackOff (RDS 보안 그룹 차단)</h2>
<h3 id="다시-sync-후-pod-상태-확인">다시 Sync 후 Pod 상태 확인</h3>
<pre><code class="language-bash">kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/2aec0896-3995-491b-9253-5e4929dc4a1f/image.png" alt=""></p>
<p>→ <code>successfully synced</code></p>
<p>그런데 Pod 상태를 확인하니 문제가 있었다.</p>
<pre><code class="language-bash">kubectl get pods -n production</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e0337797-400c-4b66-a3a4-5fd930ccd030/image.png" alt=""></p>
<p>처음엔 Running처럼 보였는데...</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/af5fc35c-beb3-49ac-acf8-b1aa2c74e185/image.png" alt=""></p>
<p>→ <code>CrashLoopBackOff</code> 발생</p>
<blockquote>
<p><strong>CrashLoopBackOff란?</strong><br>컨테이너가 시작됐다가 바로 죽는 걸 반복하는 상태다.<br>보통 앱 내부 에러, 설정 오류, 외부 서비스 연결 실패 등이 원인이다.</p>
</blockquote>
<hr>
<h3 id="no-more-tasks-문제">no more tasks 문제</h3>
<p>Sync 중 <code>no more tasks</code> 에러도 함께 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/65c8155a-e914-4991-8cd0-6b8fe218ffd3/image.png" alt=""></p>
<p>ArgoCD가 <code>infrastructure/kubernetes/</code> 하위 폴더를 재귀적으로 탐색하지 못해서 production Pod를 찾지 못하는 문제였다.</p>
<p><strong>해결 — <code>directory.recurse: true</code> 추가</strong></p>
<p><code>argocd-app.yaml</code>에 재귀 탐색 옵션을 추가했다.</p>
<pre><code class="language-yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: pposiraegi
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Goorm4I/pposiraegi-ecommerce
    targetRevision: feat/eks-migration
    path: infrastructure/kubernetes
    directory:
      recurse: true   # 하위 폴더까지 재귀적으로 탐색
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true</code></pre>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml
kubectl -n argocd exec -it argocd-server-7648988dc6-zq7hn -- \
  argocd app sync pposiraegi --insecure</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/410c661b-2050-4b15-b617-2fe1089f5184/image.png" alt=""></p>
<p>Pod는 뜨기 시작했지만... CrashLoopBackOff는 계속됐다.</p>
<hr>
<h3 id="로그로-원인-파악">로그로 원인 파악</h3>
<pre><code class="language-bash">kubectl logs -n production order-service-88547fbf4-vcl4k</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/bb9b4c9e-1b30-4b50-a88f-6411ef00701f/image.png" alt=""></p>
<pre><code>Connect timed out</code></pre><p><strong>원인 — EKS 노드 → RDS 보안 그룹이 막혀있음</strong></p>
<p>기존 RDS 보안 그룹 인바운드 규칙이 <code>api_gateway_sg</code>, <code>internal_msa_sg</code>만 허용하고 있었는데, EKS 노드는 다른 보안 그룹을 사용하고 있어서 접근이 차단된 상태였다.</p>
<hr>
<h3 id="해결--eks-노드-보안-그룹-id-확인-후-rdsredis-보안-그룹에-추가">해결 — EKS 노드 보안 그룹 ID 확인 후 RDS/Redis 보안 그룹에 추가</h3>
<pre><code class="language-bash"># EKS 클러스터 보안 그룹 ID 확인
aws eks describe-cluster \
  --name pposiraegi-cluster \
  --query &quot;cluster.resourcesVpcConfig.clusterSecurityGroupId&quot; \
  --output text \
  --profile goorm</code></pre>
<p>→ <code>sg-0aa36452edaf69dd9</code> 확인</p>
<p><code>modules/security/main.tf</code>에서 <code>rds_sg</code>와 <code>redis_sg</code>에 EKS 노드 보안 그룹을 추가했다.</p>
<p><strong>rds_sg — PostgreSQL 5432 포트 허용 추가</strong></p>
<pre><code class="language-hcl">resource &quot;aws_security_group&quot; &quot;rds_sg&quot; {
  vpc_id      = var.vpc_id
  name        = &quot;${var.project_name}-rds-sg&quot;
  description = &quot;RDS PostgreSQL security group&quot;

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = &quot;tcp&quot;
    security_groups = [aws_security_group.api_gateway_sg.id, aws_security_group.internal_msa_sg.id]
  }

  # EKS 노드 접근 허용 추가
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = &quot;tcp&quot;
    security_groups = [&quot;sg-0aa36452edaf69dd9&quot;]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = &quot;-1&quot;
    cidr_blocks = [&quot;0.0.0.0/0&quot;]
  }

  tags = { Name = &quot;${var.project_name}-rds-sg&quot; }
}</code></pre>
<p><strong>redis_sg — Redis 6379 포트 허용 추가</strong></p>
<pre><code class="language-hcl"># EKS 노드 접근 허용 추가
ingress {
  from_port       = 6379
  to_port         = 6379
  protocol        = &quot;tcp&quot;
  security_groups = [&quot;sg-0aa36452edaf69dd9&quot;]
}</code></pre>
<pre><code class="language-bash"># Terraform으로 보안 그룹 변경 적용
terraform apply -var-file=&quot;terraform.tfvars&quot;</code></pre>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>순서</th>
<th>문제</th>
<th>원인</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>브랜치 못 찾음</td>
<td><code>repoURL</code> 레포 이름 오타</td>
<td>yaml 수정 후 재적용</td>
</tr>
<tr>
<td>2</td>
<td><code>app path does not exist</code></td>
<td><code>path</code>가 실제 폴더 위치와 다름</td>
<td><code>infrastructure/kubernetes</code>로 수정</td>
</tr>
<tr>
<td>3</td>
<td><code>no more tasks</code></td>
<td>하위 폴더 탐색 안 됨</td>
<td><code>directory.recurse: true</code> 추가</td>
</tr>
<tr>
<td>4</td>
<td><code>CrashLoopBackOff</code></td>
<td>EKS → RDS 보안 그룹 차단</td>
<td>Terraform으로 인바운드 규칙 추가</td>
</tr>
</tbody></table>
<p>Terraform 적용 후 Pod가 정상적으로 뜨는지는 다음 포스팅에서 이어서 정리할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EKS + ArgoCD로 자동 배포 파이프라인 만들기]]></title>
            <link>https://velog.io/@lee_nah/kubeconfig-%EC%97%B0%EA%B2%B0-%EB%B0%8F-ArgoCD-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@lee_nah/kubeconfig-%EC%97%B0%EA%B2%B0-%EB%B0%8F-ArgoCD-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Thu, 30 Apr 2026 04:52:58 GMT</pubDate>
            <description><![CDATA[<h1 id="eks에-argocd-설치하고-gitops-배포-환경-구축하기">EKS에 ArgoCD 설치하고 GitOps 배포 환경 구축하기</h1>
<p>이번 포스팅에서는 AWS EKS 클러스터에 ArgoCD를 설치하고, GitHub 레포지토리와 연동해서 자동 배포까지 설정하는 과정을 정리했다.</p>
<blockquote>
<p><strong>ArgoCD란?</strong><br>Kubernetes 환경에서 <strong>GitOps</strong> 방식의 배포를 도와주는 툴이다.<br>쉽게 말하면, GitHub에 코드를 올리면 ArgoCD가 자동으로 감지해서 클러스터에 배포해주는 자동화 배포 도구다.</p>
</blockquote>
<hr>
<h2 id="1-kubeconfig-연결">1. kubeconfig 연결</h2>
<p>먼저 로컬 환경에서 EKS 클러스터에 접근할 수 있도록 kubeconfig를 설정해야 한다.<br><code>kubectl</code> 명령어가 어느 클러스터를 바라볼지 알려주는 작업이라고 생각하면 된다.</p>
<pre><code class="language-bash"># kubeconfig 연결
aws eks update-kubeconfig --region ap-northeast-2 --name pposiraegi-cluster --profile goorm

# 클러스터 연결 확인
kubectl get nodes</code></pre>
<ul>
<li><code>--region</code>: 클러스터가 위치한 AWS 리전</li>
<li><code>--name</code>: EKS 클러스터 이름</li>
<li><code>--profile</code>: AWS CLI에 등록된 프로파일 이름</li>
</ul>
<p><code>kubectl get nodes</code>로 노드 목록이 출력되면 연결 성공이다.</p>
<hr>
<h2 id="2-argocd-설치">2. ArgoCD 설치</h2>
<p>ArgoCD를 위한 네임스페이스를 먼저 만들고, 공식 매니페스트를 적용해서 설치한다.</p>
<pre><code class="language-bash"># ArgoCD 전용 네임스페이스 생성
kubectl create namespace argocd

# ArgoCD 공식 매니페스트 적용
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 설치 완료 확인 (모든 Pod가 Running 상태여야 함)
kubectl get pods -n argocd</code></pre>
<blockquote>
<p><strong>네임스페이스(namespace)란?</strong><br>쿠버네티스 안에서 리소스를 논리적으로 분리하는 공간이다.<br>ArgoCD 관련 리소스만 모아두기 위해 별도 네임스페이스를 만든다.</p>
</blockquote>
<p>설치 후 <code>kubectl get pods -n argocd</code>를 실행했을 때, 모든 Pod의 STATUS가 <code>Running</code>으로 바뀌면 설치 완료다. (처음엔 <code>ContainerCreating</code>으로 뜨는 게 정상이니 잠깐 기다리자.)</p>
<hr>
<h2 id="3-argocd-ui-접속">3. ArgoCD UI 접속</h2>
<p>ArgoCD는 웹 UI를 제공한다. 외부에 바로 노출하지 않고 <strong>포트 포워딩</strong>으로 로컬에서 접속할 수 있다.</p>
<pre><code class="language-bash"># 로컬 8080 포트 → ArgoCD 서버 443 포트로 포워딩
kubectl port-forward svc/argocd-server -n argocd 8080:443</code></pre>
<blockquote>
<p><strong>포트 포워딩이란?</strong><br>클러스터 내부 서비스를 외부로 노출하지 않고, 내 로컬 PC에서만 접근할 수 있도록 터널을 뚫어주는 것이다.</p>
</blockquote>
<p>이 명령어를 실행한 상태로 브라우저에서 <a href="https://localhost:8080">https://localhost:8080</a> 에 접속하면 ArgoCD 로그인 화면이 나온다.</p>
<blockquote>
<p>⚠️ 브라우저에서 &quot;안전하지 않은 연결&quot;이라고 경고가 뜰 수 있는데, 자체 서명 인증서라서 그렇다. 고급 → 계속 진행을 누르면 된다.</p>
</blockquote>
<hr>
<h2 id="4-초기-비밀번호-확인">4. 초기 비밀번호 확인</h2>
<p>초기 관리자 계정은 <code>admin</code>이고, 비밀번호는 쿠버네티스 시크릿에 저장되어 있다.</p>
<p><strong>Linux / macOS:</strong></p>
<pre><code class="language-bash">kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath=&quot;{.data.password}&quot; | base64 -d</code></pre>
<p><strong>Windows (PowerShell):</strong></p>
<pre><code class="language-powershell">kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | ForEach-Object { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_)) }</code></pre>
<p>출력된 값이 초기 비밀번호다. 로그인 후에는 보안을 위해 비밀번호를 변경하는 걸 추천한다.</p>
<hr>
<h2 id="5-argocd-app-설정-gitops-연동">5. ArgoCD App 설정 (GitOps 연동)</h2>
<p>이제 핵심이다. ArgoCD가 GitHub 레포지토리를 감시하고 자동으로 배포하도록 Application 리소스를 생성한다.</p>
<pre><code class="language-bash">kubectl apply -f - &lt;&lt;EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: pposiraegi
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/Goorm4I/pposiraegi-ecommerce-msa  # 감시할 GitHub 레포
    targetRevision: feat/eks-migration                             # 대상 브랜치
    path: kubernetes                                               # 매니페스트 파일 경로
  destination:
    server: https://kubernetes.default.svc
    namespace: production                                          # 배포할 네임스페이스
  syncPolicy:
    automated:
      prune: true      # 레포에서 삭제된 리소스는 클러스터에서도 삭제
      selfHeal: true   # 클러스터가 레포와 달라지면 자동으로 복구
    syncOptions:
    - CreateNamespace=true  # production 네임스페이스 없으면 자동 생성
EOF</code></pre>
<p>yaml 파일로 따로 저장했다면 이렇게 적용할 수도 있다:</p>
<pre><code class="language-bash">kubectl apply -f argocd-app.yaml</code></pre>
<p><strong>각 설정의 의미:</strong></p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>repoURL</code></td>
<td>ArgoCD가 감시할 GitHub 레포 주소</td>
</tr>
<tr>
<td><code>targetRevision</code></td>
<td>배포 기준이 될 브랜치 또는 태그</td>
</tr>
<tr>
<td><code>path</code></td>
<td>해당 브랜치 내에서 K8s 매니페스트가 있는 폴더</td>
</tr>
<tr>
<td><code>prune: true</code></td>
<td>레포에서 파일 삭제 시 클러스터에서도 리소스 삭제</td>
</tr>
<tr>
<td><code>selfHeal: true</code></td>
<td>누군가 클러스터를 직접 수정해도 레포 기준으로 자동 복구</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-상태-확인">6. 상태 확인</h2>
<p>ArgoCD UI에서 App이 정상적으로 생성된 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/142cd02c-8494-4303-a90e-770bb29a80bf/image.png" alt="ArgoCD UI 화면"></p>
<p><code>Synced</code> + <code>Healthy</code> 상태가 뜨면 GitHub 레포와 클러스터가 정상적으로 연동된 것이다. 이제 해당 브랜치에 커밋을 푸시하면 ArgoCD가 자동으로 감지해서 클러스터에 반영해준다. </p>
<hr>
<h2 id="마무리">마무리</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>kubeconfig 연결</td>
<td>로컬에서 EKS 클러스터 접근 설정</td>
</tr>
<tr>
<td>ArgoCD 설치</td>
<td>argocd 네임스페이스에 공식 매니페스트 적용</td>
</tr>
<tr>
<td>UI 접속</td>
<td>포트 포워딩으로 localhost:8080 접속</td>
</tr>
<tr>
<td>App 생성</td>
<td>GitHub 레포와 클러스터 자동 동기화 설정</td>
</tr>
</tbody></table>
<p>GitOps를 도입하면 배포 이력이 GitHub 커밋 히스토리에 그대로 남고, 문제가 생겼을 때 revert만으로 롤백이 가능해서 운영이 훨씬 편해진다. 처음 설정이 좀 번거롭지만 한 번 해두면 그 이후엔 정말 편하다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Terraform state가 어디갔지? S3 backend 설정 삽질 해결기]]></title>
            <link>https://velog.io/@lee_nah/Terraform-state%EA%B0%80-%EC%96%B4%EB%94%94%EA%B0%94%EC%A7%80-S3-backend-%EC%84%A4%EC%A0%95-%EC%82%BD%EC%A7%88-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/Terraform-state%EA%B0%80-%EC%96%B4%EB%94%94%EA%B0%94%EC%A7%80-S3-backend-%EC%84%A4%EC%A0%95-%EC%82%BD%EC%A7%88-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Wed, 29 Apr 2026 06:20:24 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-발생">문제 발생</h1>
<p>팀원이 tfstate s3 버킷을 지정하면서 오류가 생겨서 이거때문에 terraform apply가 안되는 오류가 발생했다.</p>
<p>팀원이 보내준 사유는 다음과 같다</p>
<blockquote>
<p>현재 pposiraegi-ecommerce repo에서 Terraform/EKS 접근 문제가 있습니다.<br>
중요:
파일 수정하지 말고 먼저 진단만 해주세요.
terraform apply 금지.
terraform import 금지.
destructive command 금지.<br>
현재 확인된 상태:
AWS_PROFILE=goorm 계정: 779846782353
EKS cluster: pposiraegi-cluster, ACTIVE, version 1.32
NodeGroup: pposiraegi-node-group, ACTIVE, t3.medium 2대 healthy
Terraform backend:
bucket = pposiraegi-tf-state-779846782353
key    = ecommerce/terraform.tfstate
해당 backend key에 state object가 없는 것으로 보임
terraform plan 실행 시 기존 리소스를 인식하지 못하고 86 to add
따라서 현재 apply하면 중복 생성/충돌 위험
kubectl은 kubeconfig 생성은 되지만 Kubernetes API 접근 실패:&quot;the server has asked for the client to provide credentials&quot;
EKS authenticationMode는 CONFIG_MAP
현재 IAM user arn:aws:iam::779846782353:user/jihoon이 aws-auth에 매핑되지 않은 것으로 추정<br>
해야 할 일:
실제 Terraform state 위치를 찾기
backend.tf와 실제 state 위치 불일치 여부 확인
state 유실이면 import 대상 목록 정리만 하기
aws-auth 또는 EKS access 권한 복구 방안 제안
팀원이 공통으로 사용할 AWS_PROFILE/backend/tfvars 절차 문서화 제안</p>
</blockquote>
<br>

<h1 id="진단하기">진단하기</h1>
<p>이거에 대한 해결방법은 사실 아직까지 잘 모르겠다.
차근차근 진단해보기로 했다.</p>
<h2 id="1-terraform-state-위치-찾기">1. Terraform state 위치 찾기</h2>
<pre><code class="language-bash"># S3 버킷 목록 전체 확인
aws s3 ls --profile goorm

# 팀원이 말한 버킷 확인
aws s3 ls s3://pposiraegi-tf-state-779846782353 --profile goorm

# 버킷 안에 뭐가 있는지
aws s3 ls s3://pposiraegi-tf-state-779846782353 --recursive --profile goorm</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/f7225022-131f-499f-a1cf-793ba7413b90/image.png" alt=""></p>
<p>이렇게 가장 최근에 만든 프로필인 pposiraegi-tf-state-779846782353에는 아무것도 없고</p>
<p>pposiraegi-tfstate-779846782353에 infrastructure/terraform.tfstate 파일이 들어있는 것을 확인할 수 있다.</p>
<br>

<h2 id="2-원인-파악">2. 원인 파악</h2>
<p>S3 버킷이 두 개 존재하는 것을 확인했다.</p>
<table>
<thead>
<tr>
<th>버킷 이름</th>
<th>생성일</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td>pposiraegi-tf-state-779846782353</td>
<td>2026-04-29</td>
<td>비어있음 ❌</td>
</tr>
<tr>
<td>pposiraegi-tfstate-779846782353</td>
<td>2026-04-17</td>
<td>state 파일 존재 ✅</td>
</tr>
</tbody></table>
<p>팀원이 설정한 backend 버킷 이름은 <code>pposiraegi-tf-state-779846782353</code>이었는데,
실제 state 파일은 <code>pposiraegi-tfstate-779846782353</code>에 있었다.
버킷 이름이 미묘하게 달랐던 것이 원인이었다.</p>
<br>

<p>팀원 backend 설정:
bucket = &quot;pposiraegi-tf-state-779846782353&quot;   ← 없는 버킷 (tf-state)
key    = &quot;ecommerce/terraform.tfstate&quot;         ← 경로도 다름
실제 state 위치:
bucket = &quot;pposiraegi-tfstate-779846782353&quot;    ← 진짜 버킷 (tfstate)
key    = &quot;infrastructure/terraform.tfstate&quot;   ← 실제 경로</p>
<br>

<h2 id="3-backendtf-파일-없는-것-확인">3. backend.tf 파일 없는 것 확인</h2>
<p>팀원 backend 설정:
bucket = &quot;pposiraegi-tf-state-779846782353&quot;   ← 없는 버킷 (tf-state)
key    = &quot;ecommerce/terraform.tfstate&quot;         ← 경로도 다름
실제 state 위치:
bucket = &quot;pposiraegi-tfstate-779846782353&quot;    ← 진짜 버킷 (tfstate)
key    = &quot;infrastructure/terraform.tfstate&quot;   ← 실제 경로</p>
<p>따라서, 중복생성과 충돌이 발생할 위험이 있었다.</p>
<br>

<h2 id="4-해결-방법-backendtf-생성">4. 해결 방법: backend.tf 생성</h2>
<p><code>infrastructure/backend.tf</code> 파일을 새로 만들어서 S3 backend를 명시적으로 설정했다.</p>
<pre><code class="language-bash">terraform {
  backend &quot;s3&quot; {
    bucket  = &quot;pposiraegi-tfstate-779846782353&quot;
    key     = &quot;infrastructure/terraform.tfstate&quot;
    region  = &quot;ap-northeast-2&quot;
    profile = &quot;goorm&quot;
  }
}</code></pre>
<p>이렇게 하면 뭐가 좋아지냐면:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>backend.tf 없을 때</th>
<th>backend.tf 있을 때</th>
</tr>
</thead>
<tbody><tr>
<td>state 저장 위치</td>
<td>로컬 파일</td>
<td>S3 버킷</td>
</tr>
<tr>
<td>팀원 공유</td>
<td>불가능</td>
<td>가능 ✅</td>
</tr>
<tr>
<td>충돌 위험</td>
<td>높음</td>
<td>낮음 ✅</td>
</tr>
<tr>
<td>분실 위험</td>
<td>높음 (로컬 삭제 시)</td>
<td>낮음 ✅</td>
</tr>
</tbody></table>
<h2 id="5-terraform-init-재실행">5. terraform init 재실행</h2>
<p>backend.tf 생성 후 terraform init을 다시 실행해서 S3 backend를 인식시켜줬다.</p>
<pre><code class="language-bash">terraform init -reconfigure</code></pre>
<p><code>-reconfigure</code> 옵션은 기존 backend 설정을 무시하고 새로운 backend로 다시 초기화하는 옵션이다.</p>
<p>이후 terraform plan을 실행하면 기존 리소스를 정상적으로 인식하고
<code>No changes</code> 또는 실제 변경사항만 표시되어야 한다.</p>
<br>

<h2 id="정리">정리</h2>
<p>이번 문제의 핵심은 두 가지였다.</p>
<ol>
<li><strong>버킷 이름 불일치</strong>: 팀원이 설정한 버킷 이름과 실제 state가 있는 버킷 이름이 달랐다.</li>
<li><strong>backend.tf 파일 부재</strong>: 팀 전체가 공유해야 하는 backend 설정이 코드로 관리되지 않고 있었다.</li>
</ol>
<p>앞으로는 backend.tf를 Git에 포함해서 팀원 모두가 동일한 S3 backend를 바라보도록 관리하도록 변경하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[구름 서포터즈] bastion 서버, 진짜 필요한가? SSM으로 갈아탄 이유]]></title>
            <link>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-bastion-%EC%84%9C%EB%B2%84-%EC%A7%84%EC%A7%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80-SSM%EC%9C%BC%EB%A1%9C-%EA%B0%88%EC%95%84%ED%83%84-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-bastion-%EC%84%9C%EB%B2%84-%EC%A7%84%EC%A7%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80-SSM%EC%9C%BC%EB%A1%9C-%EA%B0%88%EC%95%84%ED%83%84-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Mon, 30 Mar 2026 02:59:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 콘텐츠는 구름 서포터즈 활동으로 지원을 받아 작성된 교육생의 실제 경험 후기입니다.</p>
</blockquote>
<br>

<p><a href="https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-%EB%93%9C%EB%94%94%EC%96%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4">이전 게시물</a>에서 프로젝트를 시작했을 때 포스팅을 남겨놓았다.
그때는 기획 수준이었는데, 지금은 그때와는 다른 고민들을 하기 시작했다.
CI/CD 파이프라인 구성이라든지, 자동 배포 전략은 어떻게 할 것인지 등등.
아직은 반 수동 배포인 상황이지만, 앱이 잘 돌아가는 상태로는 만들었다.
일단 이전 게시물과 가장 크게 달라진 점은 인프라 구성도인 것 같다.</p>
<br>

<h1 id="기존과-가장-달라진-점">기존과 가장 달라진 점</h1>
<p>기존엔 bastion 서버를 두어서 private 서브넷에 있는 자원들에 접근하게 했는데,
지금은 SSM 서비스를 이용해 콘솔에 로그인하면 바로 EC2에 접근할 수 있도록 변경했다.
(현재 이 아키텍처도 수정해야 할 부분이 많다는 건 인지하고 있다. 백엔드 EC2를 private에 두어야 한다는 점 등.)
이 아래는 프로젝트 발표 때 사용했던 PPT다.
우리가 고려한 백엔드 엣지케이스라든가, 바뀐 인프라 아키텍처를 확인해볼 수 있다.</p>
<br>

<h1 id="백엔드-엣지케이스-고려-및-바뀐-인프라-전략">백엔드 엣지케이스 고려 및 바뀐 인프라 전략</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/79e750c7-4c2e-4791-9966-40926ea7147f/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/ecb320c2-3b2f-44ca-b96f-38d7079e2fbc/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/c65376ad-df24-4fe7-86ab-197aff42f2cd/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/271c0fee-bcb6-4994-942c-b964049682c5/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/eda7b3cc-9ebc-4dff-a4e5-d7e73dc6b1b9/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/e7631b09-9e1c-4e86-9c6f-a83121f74ab9/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/e3df713c-ffab-4707-8239-435fd163e47e/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/f4cb79dc-afcb-4ce5-9c7b-2bebf52be2e6/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/df9505c7-8109-4869-8e36-1a22c1ba1e96/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/c53d7642-6568-4fd0-af32-a9640c407811/image.png" alt=""></p>
<br>

<h1 id="bastion에서-ssm으로-전환한-이유">bastion에서 SSM으로 전환한 이유</h1>
<p>프로젝트 발표 중에 가장 첫번째로 받은 질문이 왜 bastion을 두지 않았느냐 인 것 같다.
ssm으로 전환 시 가장 팀원들이랑 많이 얘기했던 부분들이기도 하고. 
왜 bastion에서 ssm으로 전환하였냐면 다음과 같은 이유들 때문이다.</p>
<ul>
<li>보안 측면에서 더 유리하다. bastion을 운영하려면 SSH 포트(22번)를 외부에 열어둬야 하는데, 이 자체가 공격 표면이 된다. SSM은 인바운드 포트를 아예 열 필요가 없고, IAM 기반으로 접근을 제어하기 때문에 보안상 더 낫다고 판단했다.</li>
<li>bastion 서버 자체의 유지비용과 관리 포인트가 줄어든다. bastion도 결국 EC2 인스턴스라 비용이 나가고, 패치나 키 관리 같은 운영 부담도 생긴다. SSM으로 전환하면 그 오버헤드를 없앨 수 있다.</li>
<li>접근 이력 추적이 편하다. SSM Session Manager는 세션 로그를 CloudWatch나 S3에 자동으로 남길 수 있어서, 누가 언제 어떤 인스턴스에 접근했는지 감사(audit) 추적이 훨씬 수월하다. bastion 방식에서는 이걸 별도로 구성해야 한다.</li>
</ul>
<p>다만 bastion을 완전히 버려야 하나에 대해선 아직도 고민 중이다. 지금은 서비스 규모가 작아서 SSM으로도 충분하지만, EC2 수가 늘어나거나 현업 환경처럼 여러 인스턴스를 빠르게 오가며 관리해야 하는 상황이라면 SSH로 직접 붙는 게 더 편할 수 있기 때문이다.</p>
<br>




]]></description>
        </item>
        <item>
            <title><![CDATA[kafka가 그래서 뭔데?]]></title>
            <link>https://velog.io/@lee_nah/kafka%EA%B0%80-%EA%B7%B8%EB%9E%98%EC%84%9C-%EB%AD%94%EB%8D%B0</link>
            <guid>https://velog.io/@lee_nah/kafka%EA%B0%80-%EA%B7%B8%EB%9E%98%EC%84%9C-%EB%AD%94%EB%8D%B0</guid>
            <pubDate>Sun, 01 Mar 2026 05:41:27 GMT</pubDate>
            <description><![CDATA[<p>팀 프로젝트에서 kafka를 사용하기로 결정했고, 이에 대한 얕은 지식만 갖고 있었다. 그러다 멘토님께서 kafka가 뭔지 설명하라고 하셨고, 이를 사용하는 이유 등 기초적인 질문을 던지셨는데 제대로 답변하지 못하는 나의 모습을 보고
제대로 알고 써야겠다는 생각에 다시 공부하고자 이 글을 쓰게 되었다.</p>
<br>

<h1 id="kafka가-그래서-뭔데">Kafka가 그래서 뭔데?</h1>
<p>kafka에대한 글을 읽는데 메세징 큐 서비스, 비동기 서비스 등 대부분의 글들이 잘 와닿지 않게 적혀있었다.</p>
<p>그래서 아주 기초적인 개념부터 파고자 했다.</p>
<p><a href="https://cloud.google.com/learn/what-is-apache-kafka?hl=ko">Google cloud : Apache Kafka란 무엇인가요?</a>
에서는 카프카에 대해 다음과 같이 정의한다:</p>
<blockquote>
<p>&quot;Apache Kafka는 별도의 시작이나 끝이 없는 스트리밍 이벤트 데이터 또는 일반 데이터를 수집, 처리, 저장하는 데 널리 사용되는 이벤트 스트리밍 플랫폼입니다. Kafka는 차세대 분산 애플리케이션이 확장을 통해 스트리밍 이벤트를 분당 수십억 개까지 처리할 수 있도록 합니다.&quot;</p>
</blockquote>
<p>라고. </p>
<p>이 말은 너무 어렵게 적혀있는 것 같아 챗지피티의 도움을 받기로 했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/8c88bbc2-afc1-4570-b17e-940da9c67091/image.png" alt="">
하지만 이 설명은 조금 어렵게 느껴졌다.</p>
<p>그래서 ChatGPT의 도움을 받아 정리해 보았다.</p>
<br>

<p>여기서 한 가지 감이 잡히기 시작했다.</p>
<p>Kafka는 이벤트가 발생하면 그 기록을 저장하고, 필요한 서비스에게 전달해주는 시스템이라는 것이다.</p>
<p>즉,</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/96ca0019-2ee9-4812-9f36-75084a804a5b/image.png" alt=""></p>
<ul>
<li>어떤 일이 발생한다.</li>
<li>그 기록을 Kafka가 보관한다.</li>
<li>필요한 서비스들이 그 데이터를 가져가서 사용한다.</li>
</ul>
<p>이렇게 이해할 수 있었다.</p>
<p>또한 여러 글을 읽어보니
내가 이해한 방향이 크게 틀리지 않았다는 것도 확인할 수 있었다.</p>
<p>Kafka는 데이터(이벤트 데이터, 일반 데이터, 사용자 행동 기록 등)를 저장하고 전달하는 역할을 한다.</p>
<br>

<br>

<p>그런 다음, 
Amazon에서 적은 kafka에 대한 글을 읽어보았다.</p>
<p><a href="https://aws.amazon.com/ko/what-is/apache-kafka/">AWS - Kafka란 무엇입니까?</a></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/8725c6e9-8482-4a5a-8126-2123d2c7e9ad/image.png" alt=""></p>
<p>기초적인 개념을 이해하고 나니
Kafka에 대한 설명이 훨씬 읽히기 시작했다.</p>
<br>

<br>

<h1 id="kafka가-어디에-사용되는가">kafka가 어디에 사용되는가</h1>
<p>aws에서 써있는 말이 조금 어려운 말이 있어서 
쉽게 풀어 써보았다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/bc547e6e-2e7c-40bd-99fb-52e36cbcc7d7/image.png" alt=""></p>
<ul>
<li><p>&quot;Kafka는 실시간 스트리밍 데이터 파이프라인을 구축하는 데 사용된다&quot;</p>
</li>
<li><blockquote>
<p>계속 발생하는 데이터를 여러 시스템으로 보내기 위해 Kafka를 사용한다.</p>
</blockquote>
</li>
<li><p>&quot; 스트리밍 애플리케이션은 데이터 스트림을 소비한다&quot;</p>
</li>
<li><blockquote>
<p>다른 서비스들이 Kafka에서 데이터를 가져가서 처리한다.</p>
</blockquote>
</li>
<li><p>&quot;Kafka는 메시지 브로커 역할을 한다&quot;</p>
</li>
<li><blockquote>
<p>서비스 사이에서 데이터를 대신 전달해주는 중간 서버이다.</p>
</blockquote>
</li>
</ul>
<p>따라서 이 글들을 보아 kafka는 계속 발생하는 데이터를 여러 시스템으로 보내기 위해, 중간 서버역할을 한다고 읽힐 수 있었다.</p>
<br>

<br>

<h1 id="kafka와-비동기">kafka와 비동기</h1>
<p><a href="https://www.redhat.com/ko/topics/integration/what-is-apache-kafka">Redhat - Apache Kafka란 무엇일까요?</a></p>
<p>다음으로 RedHat에서 작성한 Kafka 설명을 읽어보았다.
<img src="https://velog.velcdn.com/images/lee_nah/post/993ce00b-396b-4a34-b5e1-b70500254360/image.png" alt=""></p>
<p>처음에는 긴 설명 때문에 조금 복잡하게 느껴졌다.
하지만 Kafka와 비동기의 관계를 이해하기 위해 하나씩 읽어보기로 했다.</p>
<br>

<p>먼저 동기 방식과 비동기 방식을 나눠서 생각해보았다.</p>
<br>


<p>일단 동기식 통신의 경우와 비동기 통신의 경우를 나눠 생각해보기로 한다.</p>
<br>

<h2 id="동기식-통신-api-호출">동기식 통신 (API 호출)</h2>
<p>이 방식은 우리가 평소에 알고 있는 방식이다.</p>
<ul>
<li>주문서비스 </li>
<li><blockquote>
<p>결제 서비스 API 호출</p>
</blockquote>
</li>
<li>주문 서비스</li>
<li><blockquote>
<p>알림 서비스 API 호출</p>
</blockquote>
</li>
</ul>
<p>이런식으로 API를 호출하는 것을 동기식이라 한다.</p>
<br>

<h3 id="특징">특징</h3>
<p>동기식 방식의 특징은 
바로 응답을 기다리고, 상대 서비스가 죽으면 같이 문제가 발생한다는 것이다.</p>
<p>예를 들어,</p>
<ul>
<li>주문 -&gt; 결제 서버 다운 -&gt; 주문도 실패</li>
</ul>
<p>이런 형식으로 진행된다. 따라서 대규모 서비스에서 문제가 될 수 있다.</p>
<br>


<br>

<h2 id="비동기식-통신-kafka-사용">비동기식 통신 (kafka 사용)</h2>
<p>비동기식 통신에 나온 것이 kafka이다.
이때에는 직접 호출하지 않고, 중간에 kafka에 이벤트를 남기는 것이다.</p>
<ul>
<li>주문 서비스 -&gt; kafka</li>
</ul>
<p>이러면, 다른 서비스들이 알아서 kafka의 메세지를 가져가는 형식이다.</p>
<ul>
<li>결제 서비스 -&gt; kafka 읽음</li>
<li>알림 서비스 -&gt; kafka 읽음</li>
<li>통계 서비스 -&gt; kafka 읽음</li>
</ul>
<br>

<h3 id="특징-1">특징</h3>
<p>비동기 방식의 특징은 다음과 같다.</p>
<ul>
<li>응답을 기다릴 필요가 없다.</li>
<li>서비스가 서로 독립적으로 동작한다.</li>
<li>확장이 쉽다.</li>
</ul>
<br>

<h2 id="redhad-문장-다시-해석">Redhad 문장 다시 해석</h2>
<p>그래서 다시 RedHat 문장을 해석해보면 다음과 같이 이해할 수 있다.</p>
<br>

<ul>
<li>&quot;분산형 애플리케이션이 데이터를 공유하려면 통합이 필요하다.&quot; </li>
<li><blockquote>
<p>여러 서비스가 데이터를 주고 받을 방법이 필요하다.</p>
</blockquote>
</li>
<li>&quot;동기식 방법은 API를 활용한다.&quot;</li>
<li><blockquote>
<p>서버가 다른 서버를 직접 호출하는 방식(동기식)</p>
</blockquote>
</li>
<li>&quot;비동기식 방법은 중간 저장소를 사용한다.&quot;</li>
<li><blockquote>
<p>Kafka 같은 시스템에 데이터를 먼저 남긴다.</p>
</blockquote>
</li>
</ul>
<br>

<p>따라서 동기/비동기 형식의 글을 읽고 문장을 쉽게 해석하니 kafka가 어느 상황에 쓰이는 것인지, 비동기라는 것이 어떤 의미인지 이해할 수 있게 되었다.</p>
<br>

<br>

<h1 id="kafka-한-문장-정리">kafka 한 문장 정리</h1>
<blockquote>
<p>Kafka는 서비스에서 발생하는 이벤트 데이터를 모아 저장하고, 필요한 다른 서비스들에게 비동기적으로 전달해주는 분산 스트리밍 플랫폼이다.</p>
</blockquote>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[구름 서포터즈] 드디어 프로젝트를 시작하다]]></title>
            <link>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-%EB%93%9C%EB%94%94%EC%96%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4</link>
            <guid>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-%EB%93%9C%EB%94%94%EC%96%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4</guid>
            <pubDate>Fri, 27 Feb 2026 02:40:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 콘텐츠는 구름 서포터즈 활동으로 지원을 받아 작성된 교육생의 실제 경험 후기입니다.</p>
</blockquote>
<p>3개월의 이론 기간이 끝나고 드디어 프로젝트를 시작하게 되었다.
그런데 중간에 설 연휴도 끼어 있어서 실질적으로는 약 1주일 정도 진행을 하지 못했다.</p>
<p>설 연휴 전에 프로젝트명과 도메인을 무엇으로 할지, 그리고 각자 어떤 역할을 맡을지 정도만 정해 놓은 상태였다.
<img src="https://velog.velcdn.com/images/lee_nah/post/e252aa83-8339-493e-abbd-8262d3668de9/image.png" alt=""></p>
<p>이렇게 기본적인 틀을 정한 뒤, 전체적인 기술 스택은 초기에
EKS와 쿠버네티스를 활용한 MSA 구조로 시작하기로 수많은 토론 끝에 결정하였다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/af46980c-b537-4d44-841c-d9266e85f43d/image.png" alt=""></p>
<p>위 사진은 그 열띤 토론의 한 장면이다.</p>
<br>

<br>

<h1 id="첫-번째-기획안-발표">첫 번째 기획안 발표</h1>
<p>발표는 팀장인 내가 맡아 진행을 하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/76df8624-80a0-4cf8-a4d9-7d922ea22701/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/877e1194-5535-463e-8ab6-cefc9f3eb00c/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/63aaf0d1-27de-485a-a21b-bafb50957f2b/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/35bc49cf-ed73-4a55-97e3-723860de8960/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/c212c440-82ad-4068-9bed-cc7d7480e3ef/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/1c010347-82b0-4577-8d64-a096b7fd88c0/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/dfed0b86-e302-46c3-80a2-9bd541e7f582/image.png" alt=""><img src="https://velog.velcdn.com/images/lee_nah/post/e42f1e3a-5019-4ec6-8430-ddd433293221/image.png" alt=""></p>
<p>이렇게 PPT를 제작하여 발표를 진행하였다.
여기까지는 비교적 수월하게 진행되는 것처럼 보였다.</p>
<br>

<h1 id="첫-멘토링-그리고-와장창-깨진-우리의-기획안">첫 멘토링, 그리고 와장창 깨진 우리의 기획안</h1>
<p>우리는 PPT도 만들었고 발표도 무난하게 마쳤기 때문에 기술 선택에 대해 어느 정도 자신감이 있었다.
하지만 실제 여러 회사를 다니며 우리와 같은 과정을 겪어오신 멘토님께서는 꽤 날카로운 질문을 던지셨다.</p>
<p>처음부터 MSA로 시작하는 것이 맞는지,
EC2만으로도 충분히 가능한데 왜 EKS를 사용하려 하는지,
EKS 안에는 어떤 구성요소들이 있고 그래서 왜 비용이 많이 나오는지 알고 있는지 등
생각보다 기본적인 질문들이 이어졌다.</p>
<p>하지만 우리는 그 질문들에 대해 명확하게 답변하지 못했다.</p>
<p>결국 멘토링 이후 프로젝트 방향을 다시 잡게 되었다.
처음부터 거대한 구조를 만드는 것이 아니라,
먼저 모놀리식 구조로 서비스를 구성하고
그 안에서 문제점을 발견한 뒤 점진적으로 확장해 나가는 방식으로
기획을 전면 수정하기로 했다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/5450ff3d-9180-44b4-98b3-a04c27a060fc/image.png" alt="">
-&gt; 이는 멘토님께서 지적한 부분을 정리한 것이다.</p>
<br>

<p>멘토링을 하면서 가장 크게 느낀 점이 있다.
우리는 최신 기술을 적용하는 것 자체에 너무 집중하고 있었고,
기술을 위한 프로젝트를 하려고 했던 것 같다.</p>
<p>하지만 결국 중요한 것은 기술의 화려함이 아니라
왜 이 기술을 사용하는지 이해하고 선택하는 과정이라는 것을 깨달았다.</p>
<p>시간이 조금 부족하더라도 기초부터 차근차근 쌓아 가는 방식이
결국 우리에게 더 많이 남는 프로젝트가 될 것이라는 확신이 들었다.</p>
<p>멘토링을 한 것은 프로젝트 중 가장 잘한 일이라고 생각한다. 우리가 잘못가고 있는 길을, 애써 무시한 것들을 잘 짚어주셔서 감사하다고 생각이 든다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[구름 서포터즈] 딥다이브 3개월차 후기, EXP 미션과 스터디로 성장한 시간]]></title>
            <link>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C-3%EA%B0%9C%EC%9B%94%EC%B0%A8-%ED%9B%84%EA%B8%B0-EXP-%EB%AF%B8%EC%85%98%EA%B3%BC-%EC%8A%A4%ED%84%B0%EB%94%94%EB%A1%9C-%EC%84%B1%EC%9E%A5%ED%95%9C-%EC%8B%9C%EA%B0%84</link>
            <guid>https://velog.io/@lee_nah/%EA%B5%AC%EB%A6%84-%EC%84%9C%ED%8F%AC%ED%84%B0%EC%A6%88-%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C-3%EA%B0%9C%EC%9B%94%EC%B0%A8-%ED%9B%84%EA%B8%B0-EXP-%EB%AF%B8%EC%85%98%EA%B3%BC-%EC%8A%A4%ED%84%B0%EB%94%94%EB%A1%9C-%EC%84%B1%EC%9E%A5%ED%95%9C-%EC%8B%9C%EA%B0%84</guid>
            <pubDate>Fri, 30 Jan 2026 04:41:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 콘텐츠는 구름 서포터즈 활동으로 지원을 받아 작성된 교육생의 실제 경험 후기입니다.</p>
</blockquote>
<br>


<p>오늘은 구름의 딥다이브 과정을 반 정도 지나온 시점에서, 약 3개월 동안 느낀 점을 정리해보려고 한다.
또한 과정이 어떻게 진행되는지 간략하게 소개하고, 그중에서도 가장 마음에 들었던 스터디에 관해 이야기하려고 한다.</p>
<br>

<h1 id="딥다이브-홈페이지-및-exp-미션">딥다이브 홈페이지 및 EXP 미션</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e7efa153-1480-46b2-a611-26b094ddcecd/image.png" alt=""></p>
<p>우선 딥다이브 홈페이지는 위와 같이 구성되어 있으며, EXP로 이동할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/3ed3943d-de8b-4425-8aa0-908686d36a73/image.png" alt=""></p>
<p>EXP로 이동하면 이러한 화면이 나온다.</p>
<br>

<p>그 다음에는 미션 탭으로 이동하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/3ef7d2e9-b8d9-46ed-bf92-c712b043d3dc/image.png" alt=""></p>
<p>미션 탭에서는 다양한 미션을 도전하고 완료할 수 있다.
미션을 수행하는 것은 자유이지만, 커리큘럼에 맞춰 난이도가 점점 올라가는 구조로 되어있다.</p>
<p>수업을 듣고 난 뒤 미션을 하나씩 클리어하는 과정에서, 강의에서 배웠던 내용을 자연스럽게 실습하고 복습할 수 있어 도움이 많이 되는 것 같다.</p>
<p>미션을 완료한 뒤에는 강사님께 결과 제출과 피드백을 받게 된다.
이 과정에서 강사님이 남겨주시는 짧은 응원 한마디가 오래 기억에 남았고, 더 열심히 하게 되는 원동력이 되었다.</p>
<br>

<p>그 다음으로는 칭찬하기 탭도 확인할 수 있다.</p>
<p>공개적으로 타인을 칭찬하는 것이 처음에는 조금 부끄러웠지만, 용기를 내어 강사님 칭찬하기 미션을 수행하며 평소 전하고 싶었던 말을 전달해보았다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d0cb26d6-164c-443c-86c1-32df6b199f7d/image.png" alt=""></p>
<p>강사님께서는 이렇게 진심을 담아 답글을 남겨주셨고, 딥다이브 과정을 시작하는 데 큰 힘이 되었던 것 같다.</p>
<br>

<br>

<h1 id="스터디">스터디</h1>
<p>딥다이브 과정 중에는 스터디가 필수로 진행되었다.</p>
<p>딥다이브 과정중에는 스터디가 필수로 되어있었다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/a1462c29-ce55-49af-a696-e1a7763e27c2/image.png" alt=""></p>
<p>나는 팀빌딩 게시판에 글을 올려 도커와 쿠버네티스를 함께 공부할 팀원들을 모았다.</p>
<p>또한 12월 31일까지 인프런 제공 기간에 맞춰, 팀원들과 쿠버네티스, 프로메테우스, 그라파나 강의를 완강하는 것을 목표로 계획을 세웠다.
강의를 함께 들으며 내용을 공유하고 정리하는 방식으로 스터디를 진행하였다.</p>
<p><a href="https://www.notion.so/goormkdx/1-296c0ff4ce3181068983cc8b23e5b945">https://www.notion.so/goormkdx/1-296c0ff4ce3181068983cc8b23e5b945</a></p>
<p>위 링크는 우리 스터디 팀원들이 함께 사용하는 노션 페이지이다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/c0198e1f-a3a4-43b3-b728-46e0b2fe17d6/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/21524fe5-a0e8-49ae-8578-3c0bd26079d3/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/9b508e6c-7503-493e-a596-5ffc47789ca7/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_nah/post/5656ba0b-d1bb-4e28-9fc6-8a408ba8607d/image.png" alt=""></p>
<p>이렇게 진행 방식과 공통 목표를 설정하고, 구체적인 계획을 세운 뒤 스터디를 이어갔다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/96d6d1d1-f87a-48ce-b884-bcec01844add/image.png" alt=""></p>
<p>그날그날 각자가 공부한 내용과 포스팅한 글을 공유하면서 서로의 학습에 큰 도움을 주고 있다.</p>
<p>이론 기간이 짧게 느껴지기도 했지만, 그 시간 동안 서로 동기부여를 주고받으며 아무것도 몰랐던 쿠버네티스와 프로메테우스, 그라파나를 공부하면서 많은 성장을 이룰 수 있었던 것 같다.</p>
<p>또한 이 스터디는 하루 한 시간 정도만 진행되었지만, 남는 자습 시간에도 서로 공부하고 의견을 나누며 딥다이브 과정 중 개인적으로 가장 만족하고 있는 시간으로 남아 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠키는 어떻게 동작할까? 개발자도구로 직접 확인해보기]]></title>
            <link>https://velog.io/@lee_nah/%EC%BF%A0%ED%82%A4%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8F%84%EA%B5%AC%EB%A1%9C-%EC%A7%81%EC%A0%91-%ED%99%95%EC%9D%B8%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@lee_nah/%EC%BF%A0%ED%82%A4%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8F%84%EA%B5%AC%EB%A1%9C-%EC%A7%81%EC%A0%91-%ED%99%95%EC%9D%B8%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 29 Jan 2026 12:43:44 GMT</pubDate>
            <description><![CDATA[<p>쿠키와 세션에 대해 공부하고 있는데, 실제 쿠키가 어떻게 동작하는지 이해가 잘 안 가서 실습을 진행해보기로 결정했다.</p>
<br>

<h1 id="쿠키-동작-과정-기본-흐름">쿠키 동작 과정 (기본 흐름)</h1>
<ul>
<li><ol>
<li>클라이언트(브라우저)가 페이지를 요청한다.
ex)사용자가 사이트에 접속<pre><code class="language-bash">GET /login</code></pre>
</li>
</ol>
</li>
</ul>
<br>

<ul>
<li><ol start="2">
<li>웹서버는 쿠키를 생성한다.
ex) 로그인 세션 ID 만들기</li>
</ol>
</li>
</ul>
<br>

<ul>
<li><ol start="3">
<li><p>서버는 응답(Response) 헤더에 쿠키를 담아서 보낸다.</p>
<ul>
<li>즉, 서버가 브라우저에게 해당 내용을 쿠키로 저장하라고 하는 것이다. <pre><code>Set-Cookie: sessionId=abc123; Path=/; HttpOnly</code></pre></li>
</ul>
</li>
</ol>
</li>
</ul>
<br>

<ul>
<li><ol start="4">
<li>브라우저는 쿠키를 저장한다. </li>
</ol>
</li>
</ul>
<br>

<ul>
<li><ol start="5">
<li>이후 같은 사이트 요청 시 브라우저가 쿠키를 자동으로 첨부한다. 
ex) 사용자가 다시 요청한다.</li>
</ol>
</li>
<li><blockquote>
<p>브라우저는 자동으로 붙인다.</p>
</blockquote>
<pre><code class="language-bash">   Cookie: sessionId=abc123</code></pre>
</li>
</ul>
<br>

<ul>
<li><ol start="6">
<li>서버는 쿠키 값을 읽고 사용자를 식별한다.
(서버는 sessionID를 보고 로그인 한 사용자라고 판단한다.)</li>
</ol>
</li>
</ul>
<br>

<ul>
<li><ol start="7">
<li>서버는 쿠키를 갱신하거나 삭제할 수도 있다</li>
</ol>
<ul>
<li>만료 연장<pre><code class="language-bash">Set-Cookie: sessionId=abc123; Max-Age=3600</code></pre>
</li>
<li>삭제<pre><code class="language-bash">Set-Cookie: sessionId=; Max-Age=0</code></pre>
</li>
</ul>
</li>
</ul>
<br>


<ul>
<li><ol start="8">
<li>브라우저는 만료되면 쿠키를 자동 삭제한다.</li>
</ol>
<ul>
<li>Max-Age, Expires 지나면 제거된다.</li>
</ul>
</li>
</ul>
<br>

<br>

<br>

<h1 id="실습으로-쿠키-생성과정-확인하기">실습으로 쿠키 생성과정 확인하기</h1>
<p>실습 순서는 다음과 같다. </p>
<h2 id="1-개발자-도구를-연다">1. 개발자 도구를 연다.</h2>
<p>f12를 누르면 개발자 도구를 열 수 있다.</p>
<br>

<h2 id="2-개발자-도구에서-network탭을-누른다">2. 개발자 도구에서 Network탭을 누른다.</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/4a945410-266c-455e-95c2-b3c317fdc287/image.png" alt=""></p>
<br>



<h2 id="3-새로고침을-한-후-요청들에서-type이-document인-줄-하나를-클릭한다">3. 새로고침을 한 후, 요청들에서 type이 document인 줄 하나를 클릭한다.</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/0477e48c-4d8e-49ec-8053-0666d538719e/image.png" alt=""></p>
<br>

<h2 id="4-상세-header-패널을-연다">4. 상세 Header 패널을 연다</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d01a7b49-38d3-475f-9883-e49b91730fa9/image.png" alt=""></p>
<br>


<h2 id="5-response-headers에서-set-cookie-항목을-확인한다">5. Response Headers에서 Set-Cookie 항목을 확인한다</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/602248a2-09a9-4fac-9bb7-ee704c670ee7/image.png" alt=""></p>
<p>그럼 다음과 같이 set-cookies정보들을 볼 수 있다. 이것에 의미는 서버에서 쿠키를 내려줬다는 것이다.</p>
<br>

<h2 id="6-cookies-탭을-본다">6. Cookies 탭을 본다.</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/f15f2182-f6d8-4924-83b4-1415a4fdac09/image.png" alt="">
아까 Header에 들어가서 response Header에 있는 set-cookie에 있는 내용이 Response Cookies에 표 형태로 잘 들어가있는 것을 확인할 수 있다. </p>
<ul>
<li><p>Response Cookies: 서버가 내려준 쿠키</p>
</li>
<li><p>Request Cookies: 브라우저가 다음 요청에 자동으로 첨부한 쿠키</p>
</li>
</ul>
<p>즉, 쿠키는 서버에서 내려온 뒤 브라우저에 저장되고
이후 요청마다 자동으로 포함되어 서버로 전달되는 구조이다</p>
<br>

<br>


<h1 id="정리">정리</h1>
<ul>
<li>Set-Cookie는 서버가 쿠키를 생성해서 내려주는 순간이다.</li>
<li>Cookie는 브라우저가 저장된 쿠키를 자동으로 요청에 포함시키는 것이다.</li>
<li>개발자 도구 Network 탭을 통해 쿠키의 생성과 전달 흐름을 직접 확인할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스 설치를 ‘이해’하기: 사전 조건부터 네트워크 플러그인까지]]></title>
            <link>https://velog.io/@lee_nah/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%84%A4%EC%B9%98%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%82%AC%EC%A0%84-%EC%A1%B0%EA%B1%B4%EB%B6%80%ED%84%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@lee_nah/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%84%A4%EC%B9%98%EB%A5%BC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%82%AC%EC%A0%84-%EC%A1%B0%EA%B1%B4%EB%B6%80%ED%84%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Wed, 07 Jan 2026 07:22:47 GMT</pubDate>
            <description><![CDATA[<p>그동안은 쿠버네티스 환경을 구성할 때 강사님께서 주시는 vagrantfile을 그저 받아서 vagrant up을 하여 
설치환경을 구성하였는데, 이제는 단순한 실습 환경을 넘어서,
인프라 관리자의 관점에서 쿠버네티스를 어떻게 설치해야 하는지를
공식 문서를 기준으로 하나씩 확인하며 정리할 필요가 있다고 느꼈다.</p>
<p>이 포스팅은
쿠버네티스 공식 문서를 따라가며
설치 전 사전 조건부터 컨테이너 런타임, 네트워크 플러그인까지
“왜 이 설정이 필요한지”를 중심으로 정리한 기록이다.</p>
<br>

<p><a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/">쿠버네티스 공식 홈페이지 - kubeadm</a> </p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/65d4ad4e-bdf6-4d50-83ae-d055e75c4c42/image.png" alt=""></p>
<p>이렇게 쿠버네티스 공식 홈페이지에 들어가게되면
<img src="https://velog.velcdn.com/images/lee_nah/post/8f7e72d3-5858-4e19-b6cd-fabfc4d3a095/image.png" alt="">
기초적으로 확인할 , 필수포트와 스왑 구성을 해야한다고 나와있다.</p>
<h2 id="1-필수-포트-확인">1. 필수 포트 확인</h2>
<p>필수 포트 확인에 대한 내용은 아래 공식 문서를 참고하였다.
<a href="https://kubernetes.io/ko/docs/reference/networking/ports-and-protocols/">쿠버네티스 공식 홈페이지 - 포트와 프로토콜</a></p>
<p>쿠버네티스 설치 전에는 필수 포트가 네트워크 상에서 차단되어 있지 않은지를 확인해야 한다.
이는 해당 포트에 서비스가 이미 실행 중이어야 한다는 의미가 아니라,
설치 이후 쿠버네티스 컴포넌트들이 사용할 수 있도록 포트가 열려 있어야 한다는 의미이다.</p>
<p>대표적으로 쿠버네티스 API 서버는 기본적으로 6443/TCP 포트를 사용한다.</p>
<p>다음은 API 서버 포트를 확인할 때 사용할 수 있는 명령어 예시이다.</p>
<pre><code class="language-bash">nc 127.0.0.1 6443 -zv -w 2</code></pre>
<p>이 명령은 다음을 의미한다.</p>
<ul>
<li>127.0.0.1 : 로컬 노드</li>
<li>6443 : 쿠버네티스 API 서버 포트</li>
<li>-z : 실제 데이터 전송 없이 포트 상태만 확인</li>
<li>-v : 상세 출력</li>
<li>-w 2 : 타임아웃 2초</li>
</ul>
<br>

<h3 id="실제-실행-결과와-그-의미">실제 실행 결과와 그 의미</h3>
<p>로컬 환경에서 위 명령을 실행했을 때 다음과 같은 출력이 나타났다.
<img src="https://velog.velcdn.com/images/lee_nah/post/52ec007d-4f0c-4090-af5b-2814e09b86f6/image.png" alt=""></p>
<p>이 결과는 포트가 차단되어 있다는 의미가 아니다.</p>
<p>Connection refused 는
해당 IP와 포트에 현재 리스닝 중인 프로세스가 존재하지 않는다는 의미이다.</p>
<p>즉, 다음과 같은 상황을 의미한다.</p>
<ul>
<li>방화벽에 의해 포트가 막힌 상태 → 아님</li>
<li>해당 노드에 API 서버 프로세스가 실행 중 → 아님</li>
</ul>
<p>본 실습 환경에서는 이 명령을 쿠버네티스 컨트롤 플레인 노드가 아닌 로컬 PC에서 실행했기 때문에,
127.0.0.1:6443 에 kube-apiserver 프로세스가 존재하지 않아 위와 같은 결과가 출력되었다.</p>
<br>

<h3 id="공식-문서의-예제를-어떻게-이해해야-하는가">공식 문서의 예제를 어떻게 이해해야 하는가</h3>
<p>중요한 점은,
공식 문서에 제시된 nc 명령이 쿠버네티스 설치 전에 반드시 실행해야 하는 절차는 아니라는 점이다.
(그래서 현재 시점에서는 중요한게 아니다.)</p>
<br>

<p>해당 예제는 다음을 설명하기 위한 것이다.</p>
<p>쿠버네티스 API 서버는 기본적으로 6443 포트를 사용한다</p>
<p>설치 이후, 또는 운영 중 장애 상황에서</p>
<p>API 서버 포트가 정상적으로 열려 있는지 이런 방식으로 확인할 수 있다</p>
<p>즉, 설치 전 단계에서는 이 명령의 성공 여부를 확인하는 것이 목적이 아니라,
해당 포트가 방화벽·네트워크 정책 상 차단되지 않도록 사전에 설계되어 있어야 한다는 점을 인지하는 것이 목적이다.</p>
<br>


<h2 id="2-스왑-구성">2. 스왑 구성</h2>
<br>

<p><img src="https://velog.velcdn.com/images/lee_nah/post/f8e56b36-71b0-490a-bd75-569806b9adef/image.png" alt=""></p>
<br>

<p>스왑(Swap) 구성에 대해서는 쿠버네티스 공식 문서에서 다음과 같이 명시하고 있다.</p>
<p>쿠버네티스에서 kubelet은 기본적으로 스왑 메모리가 활성화되어 있으면 실행에 실패하도록 설계되어 있다.
이는 쿠버네티스가 노드의 메모리 상태를 정확하게 인지하고,
파드의 리소스 사용량을 안정적으로 제어하기 위함이다.</p>
<h2 id="왜-쿠버네티스는-스왑을-허용하지-않을까">왜 쿠버네티스는 스왑을 허용하지 않을까</h2>
<p>리눅스에서 스왑은
물리 메모리가 부족할 때 디스크 공간을 메모리처럼 사용하는 기능이다.</p>
<p>하지만 쿠버네티스 환경에서는 이 방식이 문제가 된다.</p>
<ul>
<li>스왑은 디스크 I/O를 사용하기 때문에 성능 예측이 어렵다</li>
<li>kubelet은 파드의 메모리 사용량을 기준으로 스케줄링과 eviction을 수행한다</li>
<li>스왑이 활성화되어 있으면 실제 메모리 사용량을 정확히 판단하기 어렵다</li>
</ul>
<p>이로 인해 쿠버네티스는 “메모리가 부족하면 느리게 버티는 것”보다
“명확하게 OOM을 발생시키는 것”을 더 안전한 동작으로 판단한다.</p>
<p>그래서 기본 정책은 다음과 같다.</p>
<blockquote>
<p>스왑이 켜져 있으면 kubelet은 실행되지 않는다.</p>
</blockquote>
<br>

<h2 id="스왑-비활성화-방법">스왑 비활성화 방법</h2>
<p>쿠버네티스를 설치하기 위해서는 노드에서 스왑을 비활성화해야 한다.</p>
<p>우선 현재 활성화된 스왑을 즉시 비활성화한다.</p>
<p>swapoff -a</p>
<p>이 명령은 현재 실행 중인 시스템에서만 스왑을 끄는 임시 설정이다.</p>
<p>재부팅 후에도 스왑이 다시 활성화되지 않도록 하기 위해
/etc/fstab 파일에서 스왑 설정을 주석 처리한다.</p>
<p>sed -i &#39;/ swap / s/^/#/&#39; /etc/fstab</p>
<p>이렇게 설정하면 시스템 재부팅 이후에도 스왑이 비활성화된 상태를 유지한다.</p>
<br>

<h2 id="현재-스왑-활성화-여부-확인">현재 스왑 활성화 여부 확인</h2>
<pre><code class="language-bash">swapon --show</code></pre>
<p>아무 출력도 없으면 → 스왑 비활성화 상태이다</p>
<p>출력이 있으면 → 스왑 활성화 상태이다</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/17e13250-7bad-4f64-b6cb-014ea0df8f6b/image.png" alt=""></p>
<p>나는 현재 이렇게 스왑이 비활성화된 상태라고 출력이 된다.</p>
<p>여기서 더 자세하기 보기 위해서 다음 명령어를 쳐봤다.</p>
<pre><code class="language-bash">free -h</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/1d56d64e-8c57-4f65-b286-69e43769bf73/image.png" alt=""></p>
<p>여기서 Swap: 0B 0B 0B 로 나오면 완전히 비활성화 상태이다.</p>
<br>

<br>

<h1 id="3-컨테이너-런타임-설치">3. 컨테이너 런타임 설치</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/9a941d87-f05d-49ab-9884-76c7ad4cb579/image.png" alt="">
마지막으로 쿠버네티스를 설치하기 위해서는 컨테이너 런타임을 설치해야 한다.
쿠버네티스는 자체적으로 컨테이너를 실행하지 않기 때문에, 외부 컨테이너 런타임이 반드시 필요하다.
본 실습에서는 쿠버네티스 공식 문서에서 권장하는 containerd를 컨테이너 런타임으로 사용한다.</p>
<p>일단 위 글에서 살펴보라고 나온 <a href="https://kubernetes.io/ko/docs/setup/production-environment/container-runtimes/">컨테이너 런타임</a>에 들어가서 보면
다음과 같이 나온다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/8a245834-418e-4254-8f14-9283be95586f/image.png" alt=""></p>
<br>

<p>컨테이너 런타임을 설치하기 전에 리눅스 커널 파라미터를 사전에 설정해야 한다는 내용이 나온다.</p>
<p>위 문서에서는 다음과 같은 sysctl 설정을 적용하도록 안내하고 있다.</p>
<p>이 설정들은 단순히 containerd만을 위한 설정이 아니라,
쿠버네티스 파드 네트워크가 정상적으로 동작하기 위해 필수적인 커널 설정이다.
<img src="https://velog.velcdn.com/images/lee_nah/post/232e4e43-4e34-4a91-87de-9578bf8c89c5/image.png" alt=""></p>
<h2 id="3-1-sysctl-설정">3-1. sysctl 설정</h2>
<h3 id="sysctl-설정이-필요한-이유">sysctl 설정이 필요한 이유.</h3>
<p>쿠버네티스에서 파드는 가상 네트워크를 통해 서로 통신한다.
이 과정에서 리눅스 커널은 다음과 같은 역할을 수행해야 한다.</p>
<ul>
<li>브리지 네트워크를 통해 들어오는 패킷을 iptables에서 처리할 수 있어야 한다</li>
<li>파드 간 트래픽이 노드를 거쳐 포워딩될 수 있어야 한다</li>
<li>컨테이너 네트워크 인터페이스(CNI)가 커널 네트워크 스택과 정상적으로 연동되어야 한다</li>
</ul>
<p>하지만 리눅스 기본 설정에서는
브리지 네트워크를 통과하는 패킷이 iptables 규칙을 거치지 않거나,
IP 포워딩이 비활성화되어 있는 경우가 많다.</p>
<p>이 상태로 쿠버네티스를 설치하면 다음과 같은 문제가 발생할 수 있다.</p>
<ul>
<li>파드 간 통신 실패</li>
<li>서비스(Service) 트래픽이 정상적으로 전달되지 않음</li>
<li>CNI 플러그인 초기화 실패</li>
</ul>
<p>이를 방지하기 위해, 공식 문서에서는 아래와 같은 커널 파라미터 설정을 사전 조건으로 요구한다.</p>
<br>

<h3 id="적용하는-sysctl-파라미터의-의미">적용하는 sysctl 파라미터의 의미</h3>
<pre><code class="language-bash">net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1</code></pre>
<p>위 설정은
브리지 네트워크를 통해 전달되는 패킷이
iptables 및 ip6tables 규칙을 거치도록 설정하는 옵션이다.</p>
<p>즉, 쿠버네티스 네트워크 정책과 서비스 규칙이
브리지 인터페이스에서도 정상적으로 적용되도록 하기 위한 설정이다.</p>
<br>

<pre><code class="language-bash">net.ipv4.ip_forward = 1</code></pre>
<p>이 설정은 IP 포워딩을 활성화하는 옵션이다.
노드가 파드 트래픽을 다른 파드나 노드로 전달하기 위해 반드시 필요하다.</p>
<br>

<h3 id="sysctl-파라미터-적용">sysctl 파라미터 적용</h3>
<p>공식 문서에서 제시한 명령어를 그대로 사용하여
해당 커널 파라미터를 적용한다.</p>
<p>이 설정은 컨테이너 런타임(containerd) 설치 이전에 적용되어야 하며,
이후 CNI 플러그인과 쿠버네티스 네트워크가 정상적으로 동작하기 위한 기반이 된다.</p>
<br>

<h2 id="3-2-cgroup-드라이버">3-2. cgroup 드라이버</h2>
<p>컨테이너 런타임을 설치할 때 반드시 함께 고려해야 하는 요소가 cgroup(control group) 드라이버이다.</p>
<p>리눅스에서 cgroup은
프로세스에 할당된 CPU, 메모리, I/O와 같은 자원을 제한하고 관리하기 위한 기능이다.
쿠버네티스에서는 이 cgroup을 기반으로 파드와 컨테이너의 리소스를 제어한다.</p>
<br>

<h3 id="kubelet과-컨테이너-런타임에서-cgroup이-중요한-이유">kubelet과 컨테이너 런타임에서 cgroup이 중요한 이유</h3>
<p>쿠버네티스에서 실제 컨테이너를 관리하는 주체는 다음 두 컴포넌트이다.</p>
<ul>
<li>kubelet</li>
<li>컨테이너 런타임(containerd)</li>
</ul>
<p>이 두 컴포넌트는 모두 cgroup을 통해 다음과 같은 작업을 수행한다.</p>
<ul>
<li>파드의 CPU / 메모리 요청(request)과 제한(limit) 적용</li>
<li>노드의 리소스 사용량 추적</li>
<li>메모리 부족 상황에서 파드 eviction 판단</li>
</ul>
<p>이 때문에 kubelet과 컨테이너 런타임은 반드시 동일한 cgroup 드라이버를 사용해야 한다.
두 컴포넌트가 서로 다른 방식으로 cgroup을 관리할 경우,
노드의 리소스 상태를 서로 다르게 인식하게 되어 시스템이 불안정해질 수 있다.</p>
<br>

<h3 id="사용-가능한-cgroup-드라이버-종류">사용 가능한 cgroup 드라이버 종류</h3>
<p>쿠버네티스에서 사용할 수 있는 cgroup 드라이버는 크게 두 가지이다.</p>
<ul>
<li>cgroupfs</li>
<li>systemd</li>
</ul>
<br>

<h4 id="cgrupfs-드라이버">cgrupfs 드라이버</h4>
<p>cgroupfs 드라이버는
kubelet이 직접 cgroup 파일 시스템을 제어하는 방식이다.</p>
<p>과거에는 기본값으로 많이 사용되었지만,
init 시스템이 systemd인 환경에서는 권장되지 않는다.</p>
<p>그 이유는 systemd 자체가 이미 cgroup 관리자 역할을 수행하고 있기 때문이다.
이 상태에서 cgroupfs를 사용하면,
하나의 시스템에 두 개의 cgroup 관리자(systemd + cgroupfs) 가 공존하게 되어
리소스 관리 충돌이 발생할 수 있다.</p>
<br>

<h4 id="systemd-cgroup-드라이버">systemd cgroup 드라이버</h4>
<p>systemd cgroup 드라이버는
systemd를 통해 cgroup을 관리하는 방식이다.</p>
<p>systemd 기반 리눅스 배포판에서는
모든 서비스와 프로세스가 systemd 단위(unit)로 관리되며,
각 단위마다 cgroup이 자동으로 할당된다.</p>
<p>따라서 systemd를 init 시스템으로 사용하는 환경에서는
kubelet과 컨테이너 런타임 모두 systemd cgroup 드라이버를 사용하는 것이 권장된다.</p>
<p>공식 문서에서도
systemd 환경에서는 systemd cgroup 드라이버 사용을 권장하고 있으며,
특히 cgroup v2 환경에서는 systemd 사용이 사실상 표준에 가깝다.</p>
<br>

<h3 id="본-실습-환경에서의-선택">본 실습 환경에서의 선택</h3>
<p>앞서 살펴본 cgroup 드라이버 개념을 바탕으로,
본 실습 환경에서는 다음과 같은 구성을 선택하였다.</p>
<p>본 실습은 systemd 기반의 Rocky Linux 환경에서 진행되었으며,
컨테이너 런타임으로는 containerd를 사용하였다.</p>
<p>이에 따라 kubelet과 컨테이너 런타임 간의 cgroup 관리 방식을 일치시키기 위해
systemd cgroup 드라이버를 사용하도록 설정하였다.</p>
<p>실제로 containerd 설정 파일을 생성한 뒤,
기본값으로 설정되어 있는 cgroupfs 방식이 아닌
systemd 방식으로 cgroup 드라이버를 변경하였다.</p>
<br>

<h2 id="3-3-컨테이너-런타임-설치">3-3 컨테이너 런타임 설치</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/bd323c23-da81-49ec-80d7-fdee31c271ff/image.png" alt=""></p>
<p>공식 문서에는 다음과 같은 컨테이너 런타임들이 소개되어 있다.</p>
<ul>
<li>containerd</li>
<li>CRI-O</li>
<li>Docker 엔진 (cri-dockerd 사용)</li>
<li>미란티스 컨테이너 런타임(MCR)</li>
</ul>
<p>이 중에서 본 실습에서는
containerd를 컨테이너 런타임으로 선택하였다.</p>
<br>

<h3 id="containerd를-선택한-이유">containerd를 선택한 이유</h3>
<p>containerd는 CNCF에서 관리되는 오픈소스 컨테이너 런타임으로,
쿠버네티스가 공식적으로 지원하는 CRI 호환 런타임이다.</p>
<p>containerd를 사용할 경우 다음과 같은 특징이 있다.</p>
<ul>
<li>별도의 어댑터 없이 kubelet과 직접 CRI 연동 가능</li>
<li>Docker 엔진에 비해 구조가 단순함</li>
<li>쿠버네티스 공식 문서에서 권장하는 기본 런타임</li>
<li>현재 운영 환경에서 가장 널리 사용되는 런타임 중 하나</li>
</ul>
<p>반면, Docker 엔진은
쿠버네티스 1.24 버전부터 kubelet 내장 지원이 제거되었기 때문에
추가 어댑터인 cri-dockerd를 설치해야만 사용이 가능하다.</p>
<p>본 실습에서는
불필요한 중간 계층을 줄이고, 공식 권장 구조에 맞추기 위해
Docker 엔진 및 cri-dockerd는 사용하지 않았다.</p>
<br>

<h3 id="containerd-설치-및-cri-엔드포인트">containerd 설치 및 CRI 엔드포인트</h3>
<p>containerd를 설치하면,
다음 경로에 CRI 소켓이 생성된다.</p>
<pre><code class="language-bash">/var/run/containerd/containerd.sock</code></pre>
<p>kubeadm은 이 소켓을 통해
containerd가 CRI 런타임으로 동작하고 있음을 자동으로 인식한다.</p>
<p>따라서 본 실습에서는
kubeadm 실행 시 별도로 컨테이너 런타임 엔드포인트를 지정하지 않아도
containerd를 정상적으로 감지하여 클러스터 초기화를 진행할 수 있다.</p>
<br>

<h3 id="containerd-설정-파일과-cgroup-드라이버">containerd 설정 파일과 cgroup 드라이버</h3>
<p>공식 문서에서는
containerd 설치 이후 유효한 설정 파일(config.toml)을 생성하고,
systemd cgroup 드라이버를 사용하도록 설정할 것을 권장한다.</p>
<p>이에 따라 본 실습에서는
기본 설정 파일을 생성한 뒤
cgroup 드라이버를 systemd 방식으로 변경하였다.</p>
<p>이 설정을 통해</p>
<ul>
<li>kubelet</li>
<li>containerd</li>
</ul>
<p>두 컴포넌트가 동일한 cgroup 관리 방식을 사용하도록 구성하였다.</p>
<p>설정 변경 이후에는
containerd 서비스를 재시작하여 변경 사항을 적용하였다.</p>
<br>

<h2 id="3-4-네트워크-플러그인">3-4 네트워크 플러그인</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/ce71853d-8805-4af1-912b-b9cbb195fc50/image.png" alt=""></p>
<p>공식 홈페이지에서는 이렇게, 컨테이너 런타임 뿐만 아니라 클러스터에 동작하는 네트워크 플러그인도 필요하다고 나와있어 네트워크 플러그인도 설치해준다.</p>
<p><a href="https://kubernetes.io/ko/docs/concepts/cluster-administration/networking/#%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95">쿠버네티스 공식 홈페이지 - 네트워크 플러그인</a></p>
<br>

<p>위 링크를 들어가면 다음과 같이 나온다. 
<img src="https://velog.velcdn.com/images/lee_nah/post/b9e8052f-31bb-4c66-95df-8d624a0d308d/image.png" alt=""></p>
<br>

<h3 id="네트워크-플러그인이-필요한-이유">네트워크 플러그인이 필요한 이유</h3>
<p>쿠버네티스에서 파드는 노드 안에서 생성되고 삭제되며,
클러스터 상태에 따라 언제든지 다른 노드로 이동할 수 있다.</p>
<p>이때 다음과 같은 작업이 필요하다.</p>
<ul>
<li>파드 생성 시 IP 할당</li>
<li>파드 삭제 시 IP 회수</li>
<li>파드 간 통신 경로 구성</li>
<li>노드 간 파드 트래픽 라우팅</li>
<li>서비스(Service) 트래픽 처리</li>
</ul>
<p>이 모든 네트워크 관련 작업을 담당하는 것이 네트워크 플러그인이다.</p>
<p>따라서 네트워크 플러그인이 설치되지 않은 상태에서는 다음과 같은 현상이 발생한다.</p>
<ul>
<li>파드는 생성되지만 NotReady 상태에 머무른다</li>
<li>파드 간 통신이 불가능하다</li>
<li>CoreDNS가 정상적으로 동작하지 않는다</li>
<li>서비스(Service)를 통한 접근이 불가능하다</li>
</ul>
<blockquote>
<p>즉, 네트워크 플러그인이 없으면
쿠버네티스 클러스터는 사실상 정상 동작할 수 없다.</p>
</blockquote>
<br>

<h3 id="cnicontainer-network-interface">CNI(Container Network Interface)</h3>
<p>쿠버네티스는 네트워크 플러그인과 연동하기 위해
CNI(Container Network Interface) 라는 표준 인터페이스를 사용한다.</p>
<p>CNI는 다음을 정의한다.</p>
<ul>
<li>컨테이너 네트워크를 설정하는 방식</li>
<li>네트워크 플러그인이 kubelet과 통신하는 방법</li>
<li>파드 생성/삭제 시 네트워크를 설정·정리하는 규칙</li>
</ul>
<p>쿠버네티스는 CNI 규격을 만족하는 플러그인이라면
어떤 구현체라도 사용할 수 있도록 설계되어 있다.</p>
<br>

<h3 id="네트워크-플러그인의-종류">네트워크 플러그인의 종류</h3>
<p>공식 문서에는 다양한 네트워크 플러그인이 소개되어 있다.</p>
<p>예를 들면 다음과 같다.</p>
<ul>
<li>Calico</li>
<li>Flannel</li>
<li>Weave Net</li>
<li>Cilium</li>
</ul>
<p>각 플러그인은
네트워크 구현 방식, 성능 특성, 보안 기능(NetworkPolicy 지원 여부) 등이 서로 다르다.</p>
<p>따라서 실제 운영 환경에서는
클러스터 규모, 보안 요구사항, 네트워크 정책 적용 여부 등을 고려하여
적절한 네트워크 플러그인을 선택해야 한다.</p>
<br>

<h3 id="본-실습에서의-네트워크-플러그인-선택">본 실습에서의 네트워크 플러그인 선택</h3>
<p>본 실습 환경에서는
네트워크 플러그인으로 Calico를 사용한다.</p>
<p>Calico는</p>
<ul>
<li>쿠버네티스 NetworkPolicy를 기본적으로 지원하며</li>
<li>노드 간 파드 통신을 안정적으로 처리하고</li>
<li>공식 문서와 실습 자료에서 널리 사용되는 플러그인이다</li>
</ul>
<p>다음 단계에서는
kubeadm을 통해 클러스터를 초기화한 이후,
Calico 네트워크 플러그인을 설치하여
파드 네트워크를 실제로 구성한다.</p>
<br>

<br>

<br>

<h1 id="이제는-이해할-수-있는-쿠버네티스-설치-명령어full">이제는 이해할 수 있는 쿠버네티스 설치 명령어(Full)</h1>
<pre><code class="language-bash">echo &#39;======== [4] Rocky Linux 기본 설정 ========&#39;
echo &#39;======== [4-1] 패키지 업데이트 ========&#39;
# 강의와 동일한 실습 환경을 유지하기 위해 Linux Update는 하지 마세요!
# yum -y update # (x)

echo &#39;======== [4-2] 타임존 설정 ========&#39;
timedatectl set-timezone Asia/Seoul
timedatectl set-ntp true
chronyc makestep

echo &#39;======== [4-3] [WARNING FileExisting-tc]: tc not found in system path 로그 관련 업데이트 ========&#39;
yum install -y yum-utils iproute-tc
echo &#39;======== [4-3] [WARNING OpenSSL version mismatch 로그 관련 업데이트 ========&#39;
yum update openssl openssh-server -y

echo &#39;======= [4-4] hosts 설정 ==========&#39;
cat &lt;&lt; EOF &gt;&gt; /etc/hosts
192.168.56.30 k8s-master
EOF

echo &#39;======== [5] kubeadm 설치 전 사전작업 ========&#39;
echo &#39;======== [5] 방화벽 해제 ========&#39;
systemctl stop firewalld &amp;&amp; systemctl disable firewalld

echo &#39;======== [5] Swap 비활성화 ========&#39;
swapoff -a &amp;&amp; sed -i &#39;/ swap / s/^/#/&#39; /etc/fstab

echo &#39;======== [6] 컨테이너 런타임 설치 ========&#39;
echo &#39;======== [6-1] 컨테이너 런타임 설치 전 사전작업 ========&#39;
echo &#39;======== [6-1] iptable 세팅 ========&#39;
cat &lt;&lt;EOF |tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

cat &lt;&lt;EOF |tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sysctl --system

echo &#39;======== [6-2] 컨테이너 런타임 (containerd 설치) ========&#39;
echo &#39;======== [6-2-1] containerd 패키지 설치 (option2) ========&#39;
echo &#39;======== [6-2-1-1] docker engine 설치 ========&#39;
echo &#39;======== [6-2-1-1] repo 설정 ========&#39;
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

echo &#39;======== [6-2-1-1] containerd 설치 ========&#39;
yum install -y containerd.io-1.6.21-3.1.el9.aarch64
systemctl daemon-reload
systemctl enable --now containerd

echo &#39;======== [6-3] 컨테이너 런타임 : cri 활성화 ========&#39;
containerd config default &gt; /etc/containerd/config.toml
sed -i &#39;s/ SystemdCgroup = false/ SystemdCgroup = true/&#39; /etc/containerd/config.toml
systemctl restart containerd

echo &#39;======== [7] kubeadm 설치 ========&#39;
echo &#39;======== [7] repo 설정 ========&#39;
cat &lt;&lt;EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.27/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.27/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF

echo &#39;======== [7] SELinux 설정 ========&#39;
setenforce 0
sed -i &#39;s/^SELINUX=enforcing$/SELINUX=permissive/&#39; /etc/selinux/config

echo &#39;======== [7] kubelet, kubeadm, kubectl 패키지 설치 ========&#39;
yum install -y kubelet-1.27.2-150500.1.1.aarch64 kubeadm-1.27.2-150500.1.1.aarch64 kubectl-1.27.2-150500.1.1.aarch64 --disableexcludes=kubernetes
systemctl enable --now kubelet

echo &#39;======== [8] kubeadm으로 클러스터 생성  ========&#39;
echo &#39;======== [8-1] 클러스터 초기화 (Pod Network 세팅) ========&#39;
kubeadm init --pod-network-cidr=20.96.0.0/12 --apiserver-advertise-address 192.168.56.30

echo &#39;======== [8-2] kubectl 사용 설정 ========&#39;
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

echo &#39;======== [8-3] Pod Network 설치 (calico) ========&#39;
kubectl create -f https://raw.githubusercontent.com/k8s-1pro/install/main/ground/k8s-1.27/calico-3.26.4/calico.yaml
kubectl create -f https://raw.githubusercontent.com/k8s-1pro/install/main/ground/k8s-1.27/calico-3.26.4/calico-custom.yaml

echo &#39;======== [8-4] Master에 Pod를 생성 할수 있도록 설정 ========&#39;
kubectl taint nodes k8s-master node-role.kubernetes.io/control-plane-

echo &#39;======== [9] 쿠버네티스 편의기능 설치 ========&#39;
echo &#39;======== [9-1] kubectl 자동완성 기능 ========&#39;
yum -y install bash-completion
echo &quot;source &lt;(kubectl completion bash)&quot; &gt;&gt; ~/.bashrc
echo &#39;alias k=kubectl&#39; &gt;&gt;~/.bashrc
echo &#39;complete -o default -F __start_kubectl k&#39; &gt;&gt;~/.bashrc
source ~/.bashrc

echo &#39;======== [9-2] Dashboard 설치 ========&#39;
kubectl create -f https://raw.githubusercontent.com/k8s-1pro/install/main/ground/k8s-1.27/dashboard-2.7.0/dashboard.yaml

echo &#39;======== [9-3] Metrics Server 설치 ========&#39;
kubectl create -f https://raw.githubusercontent.com/k8s-1pro/install/main/ground/k8s-1.27/metrics-server-0.6.3/metrics-server.yaml</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/394dd1da-69ab-47fa-895b-ea0a5dc2a689/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/d5b0e9d2-73de-467c-b6ce-982eb851a17b/image.png" alt=""></p>
<br>

<br>

<blockquote>
<p>위 포스팅은 인프런 강의 : <a href="https://www.inflearn.com/course/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%96%B4%EB%82%98%EB%8D%94-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%A7%80%EC%83%81%ED%8E%B8-sprint1">쿠버네티스 어나더 클래스-Sprint 1, 2</a>
및 <a href="https://cafe.naver.com/kubeops/91">쿠버네티스 무게감 있게 설치하기</a>를 참고하여 작성한 포스팅입니다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[컨테이너가 등장하며 바뀐 애플리케이션 배포 방식 정리]]></title>
            <link>https://velog.io/@lee_nah/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EA%B0%80-%EB%93%B1%EC%9E%A5%ED%95%98%EB%A9%B0-%EB%B0%94%EB%80%90-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EC%8B%9D-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@lee_nah/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EA%B0%80-%EB%93%B1%EC%9E%A5%ED%95%98%EB%A9%B0-%EB%B0%94%EB%80%90-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EC%8B%9D-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 05 Jan 2026 12:08:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lee_nah/post/f27e04c6-4d21-4183-b537-980cce58ca67/image.png" alt=""></p>
<h1 id="컨테이너-이전-배포방식">컨테이너 이전 배포방식</h1>
<p>컨테이너가 등장하기 전의 배포 방식은 비교적 단순한 구조였다.
애플리케이션을 빌드해서 나온 실행 파일을 서버에 복사하고 실행하는 방식이다.</p>
<br>

<h2 id="개발-환경">개발 환경</h2>
<ul>
<li>개발자는 IntelliJ 같은 IDE를 사용해 코드를 작성한다.</li>
<li>개발이 완료되면 Gradle 같은 빌드 도구를 통해 빌드를 수행한다.
빌드는 컴파일 후, 실행 가능한 형태로 패키징하는 과정이다.</li>
<li>ex) Java로 개발한 경우, 빌드를 하면 .jar 파일이 생성된다.</li>
<li>이 .jar 파일은 사전에 설치된 JVM(Java Virtual Machine) 위에서 실행된다</li>
</ul>
<p>많은 개발자들은 이러한 환경을 자신의 컴퓨터에 설치하고, 코딩하고 테스트하면서 개발을 진행한다. </p>
<ul>
<li>각자 개발이 완료되면 소스를 GitHub에 커밋한다.</li>
<li>GitHub에 코드가 통합된 이후,
Jenkins 같은 CI 도구를 통해 빌드를 수행한다</li>
</ul>
<br>

<h2 id="cicd-환경">CI/CD 환경</h2>
<ul>
<li>Jenkins는 GitHub에서 소스를 내려받는다.</li>
<li>Jenkins 서버에도 Gradle이 설치되어 있어 필요한 라이브러리를 내려받는다.</li>
<li>빌드를 통해 .jar 파일을 생성한다.</li>
<li>생성된 .jar 파일을 인프라 서버에 배포하면 배포가 완료된다. </li>
</ul>
<br>

<p>(이 구조에서는 “서버에 JVM이 깔려 있고, 거기에 jar만 올리면 된다”
라는 전제가 항상 필요하다.)</p>
<br>


<h1 id="컨테이너가-나온-후-배포방식">컨테이너가 나온 후 배포방식</h1>
<p>컨테이너 이전에는 .jar 파일을 서버에 복사하면 배포가 끝이었다.</p>
<p>하지만 컨테이너가 등장하면서 배포 방식에 중요한 변화가 생겼다.</p>
<br>

<blockquote>
<p>가장 큰 변화는, 컨테이너 빌드 과정이 추가되었다는 점이다.</p>
</blockquote>
<h2 id="cicd-환경-컨테이너-기반">CI/CD 환경 (컨테이너 기반)</h2>
<p>Jenkins에서 빌드 버튼을 눌렀을 때
다음과 같은 일이 순서대로 발생한다.</p>
<ul>
<li>먼저 .jar 파일을 실행할 수 있는 OpenJDK 이미지를 DockerHub에서 가져온다.</li>
<li>이 OpenJDK 이미지는 애플리케이션을 실행하기 위한 베이스 이미지이다.</li>
<li>그 위에 빌드된 .jar 파일을 올린다.</li>
<li>이 과정을 통해 컨테이너 이미지가 생성된다.
→ MyApp 컨테이너 이미지</li>
<li>생성된 이미지를 다시 DockerHub 같은 이미지 레지스트리에 업로드한다.</li>
</ul>
<blockquote>
<p>즉, 이제 배포 대상은 jar 파일이 아니라 컨테이너 이미지가 된다.</p>
</blockquote>
<br>

<h2 id="배포-과정">배포 과정</h2>
<p>컨테이너 이미지가 준비되면 배포가 진행된다.
Jenkins에서 쿠버네티스에 Pod 생성 명령을 전달한다.</p>
<h2 id="인프라-환경-kubernetes">인프라 환경 (Kubernetes)</h2>
<p>이 명령을 받은 쿠버네티스는 다음과 같이 동작한다.</p>
<ul>
<li>Pod 정의 안에는 컨테이너 이미지 주소가 들어 있다.</li>
<li>쿠버네티스는 해당 주소를 보고 DockerHub에서 이미지를 다운로드한다.</li>
<li>그 다음, containerd에게 해당 이미지로 컨테이너를 생성하라고 요청한다.</li>
<li>containerd는 이미지를 기반으로 실제 컨테이너를 실행한다.</li>
</ul>
<p>이렇게 해서 애플리케이션이 실행된다.</p>
<br>

<h1 id="핵심-차이">핵심 차이</h1>
<p>컨테이너 이전 : 
-&gt; jar 파일을 서버에 직접 배포
컨테이너 이후 :
-&gt; jar 파일을 포함한 컨테이너 이미지를 빌드하고 배포.</p>
<br>

<p>컨테이너가 나온 덕분에 </p>
<ul>
<li>실행 환경이 이미지로 고정되고</li>
<li>서버마다 환경 차이로 인한 문제가 줄어들며</li>
<li>배포와 롤백이 훨씬 쉬워졌다.</li>
</ul>
<br>

<blockquote>
<p>위 포스팅은 인프런 강의 중 <a href="https://www.inflearn.com/course/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%96%B4%EB%82%98%EB%8D%94-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%A7%80%EC%83%81%ED%8E%B8-sprint1/dashboard">쿠버네티스 어나더 클래스-Sprint 1, 2</a>를 참고하여 작성하였습니다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커, 쿠버네티스 및 기초 개념 정리]]></title>
            <link>https://velog.io/@lee_nah/%EB%8F%84%EC%BB%A4-%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EB%B0%8F-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@lee_nah/%EB%8F%84%EC%BB%A4-%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EB%B0%8F-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 05 Jan 2026 07:18:12 GMT</pubDate>
            <description><![CDATA[<h1 id="리눅스-계열-정리">리눅스 계열 정리</h1>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/2df526f1-c390-48e0-ae44-cfd60695bebc/image.png" alt="">
리눅스는 크게 Debian 계열과 Red Hat 계열로 나눌 수 있다.
이 구분은 무료/유료의 문제가 아니라, 패키지 관리 방식과 배포 정책의 계열 차이이다.</p>
<ul>
<li>Debian 계열은 apt, dpkg 기반의 패키지 관리 방식을 사용한다.</li>
<li>Red Hat 계열은 rpm, dnf(yum) 기반의 패키지 관리 방식을 사용한다.</li>
</ul>
<br>

<p>Debian 계열에서 가장 대중적으로 사용되는 배포판은 Ubuntu이다.
Ubuntu는 Debian을 기반으로 하되, 설치 편의성, 드라이버 지원, 빠른 릴리스 주기 등을 강화한 배포판이다.
이 때문에 개인 개발 환경, 서버 테스트, 컨테이너 베이스 이미지 등에서 매우 많이 사용된다.</p>
<br>

<p>Red Hat 계열의 대표적인 배포판은 <strong>RHEL(Red Hat Enterprise Linux)</strong>이다.
RHEL은 기업 환경을 대상으로 한 상용 리눅스로, 긴 지원 기간(LTS), 안정적인 업데이트, 공식 기술 지원을 제공한다.
다만 라이센스 비용이 많이 발생한다. </p>
<br>

<p>RHEL은 소스가 공개되기 때문에, 이를 기반으로 한 호환 배포판들이 존재한다.
대표적으로 Rocky Linux, AlmaLinux가 있다.</p>
<p>과거에는 CentOS가 이 역할을 했으나, 현재는 사용하지 않는다. </p>
<p>그 대안으로 Rocky Linux와 AlmaLinux가 등장했으며 현재 점유율은 Rocky Linux가 높다. </p>
<p>기업 환경에서는 RHEL과 바이너리 호환을 유지하면서 무료로 사용할 수 있는 Rocky Linux를 표준처럼 사용하는 경우가 많다.</p>
<br>

<p>나는 리눅스의 역사를 공부하면서 무료인 Ubuntu를 쓰면 되는 것 아닌가라는 생각이 들 수 있지만,
기업 환경에서는 단순한 무료 여부보다 운영 안정성, 업데이트 정책, 표준화된 관리 방식이 훨씬 중요하다는 것을 깨닫게 되었다. </p>
<p>Ubuntu는 릴리스 주기가 빠르고 변화가 잦지만, 
Red Hat 계열은 변경 폭이 작고 장기간 동일한 환경을 유지하기 때문에 Red Hat을 선호한다는 것을 알게 되었다. </p>
<p>따라서 OS를 직접 관리하는 비용과 리스크를 줄이기 위해
기업에서는 Rocky Linux와 같은 Red Hat 계열 배포판을 선호하는 경우가 많다는 것을 알게 되었다. </p>
<br>


<h1 id="컨테이너와-컨테이너-런타임-컨테이너-오케스트레이션">컨테이너와 컨테이너 런타임, 컨테이너 오케스트레이션</h1>
<h2 id="컨테이너-컨테이너-런타임">컨테이너, 컨테이너 런타임</h2>
<p><strong>컨테이너</strong>는 애플리케이션과 실행 환경을 함께 묶은 실행 단위이다.
이 컨테이너를 실제로 생성하고 실행하는 역할을 하는 것이 <strong>컨테이너 런타임</strong>이다.</p>
<br>

<p>우리가 잘 아는 Docker는
단순한 도구 하나가 아니라, 내부에 컨테이너 런타임 기능을 포함한 플랫폼이다.</p>
<ul>
<li>이미지를 빌드하고</li>
<li>컨테이너를 생성하고</li>
<li>컨테이너를 실행하고 관리한다</li>
</ul>
<p>즉, 컨테이너가 결과물이라면, 컨테이너 런타임은 컨테이너를 실행시키는 엔진이다.</p>
<br>

<p>컨테이너 런타임의 예시는 다음과 같다.</p>
<p>ex) Docker, containerd, CRI-O</p>
<p>이 중 containerd는 Docker에서 분리되어 나온 핵심 런타임으로,
현재 쿠버네티스 환경에서 가장 널리 사용되고 있다. </p>
<br>

<h2 id="컨테이너-오케스트레이션">컨테이너 오케스트레이션</h2>
<p><strong>컨테이너 오케스트레이션</strong>이란 이러한 컨테이너 런타임 위에서 실행되는 수많은 컨테이너들을 자동으로 관리·조정하는 시스템이다.</p>
<ul>
<li>컨테이너 배포</li>
<li>스케일 아웃/인</li>
<li>장애 발생 시 재시작</li>
<li>로드 밸런싱</li>
</ul>
<p>이 역할을 수행하는 대표적인 도구가 쿠버네티스이다.
쿠버네티스는 컨테이너 런타임을 직접 대체하는 것이 아니라,
컨테이너 런타임을 제어하는 상위 관리자 역할을 수행한다.</p>
<br>

<h2 id="컨테이너-런타임을-도커---containerd로-바꾼다면">컨테이너 런타임을 도커 &lt;-&gt; containerD로 바꾼다면</h2>
<p>쿠버네티스 환경에서는 과거에 Docker를 컨테이너 런타임으로 사용했지만,
현재는 containerd나 CRI-O를 직접 사용하는 방식이 표준이 되었다.</p>
<p>이때 흔히 드는 의문이 있었다.</p>
<p>&quot;컨테이너 런타임을 바꾸면 이미지도 다시 만들어야 하지 않을까?&quot;</p>
<p>결론부터 말하면 그럴 필요가 없다.</p>
<br>

<h2 id="ociopen-container-initiative">OCI(Open Container Initiative)</h2>
<p>그 이유는 OCI(Open Container Initiative) 표준 때문이다.
OCI는 컨테이너 이미지 형식과 런타임 동작 방식에 대한 표준을 정의하는 단체이다.</p>
<ul>
<li>Docker 이미지</li>
<li>containerd 이미지</li>
<li>CRI-O 이미지</li>
</ul>
<p>이들은 모두 OCI 표준을 따르기 때문에 서로 호환된다.</p>
<br>

<p>실제로 containerd 내부에서도
Docker에서 사용하던 runC를 그대로 사용한다.</p>
<blockquote>
<p>즉, 컨테이너 런타임을 Docker에서 containerd로 변경하더라도
기존에 만들어 둔 이미지 변경 없이 그대로 사용 가능하다.</p>
</blockquote>
<p>이 구조 덕분에 쿠버네티스는
특정 런타임에 종속되지 않고 유연하게 런타임을 교체할 수 있는 것이다.</p>
<br>

<h1 id="요약">요약</h1>
<ul>
<li>리눅스는 Debian 계열과 Red Hat 계열로 나뉜다.</li>
<li>기업 환경에서는 안정성과 표준화 때문에 Red Hat 계열(Rocky Linux)을 선호한다.</li>
<li>Docker는 컨테이너 런타임을 포함한 플랫폼이다.</li>
<li>쿠버네티스는 컨테이너 런타임을 지휘하는 오케스트레이터이다.</li>
<li>OCI 표준 덕분에 런타임을 바꿔도 이미지는 그대로 사용 가능하다.</li>
</ul>
<br>

<br>

<blockquote>
<p>위 포스팅은 인프런 강의 중 <a href="https://www.inflearn.com/course/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%96%B4%EB%82%98%EB%8D%94-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%A7%80%EC%83%81%ED%8E%B8-sprint1/dashboard">쿠버네티스 어나더 클래스-Sprint 1, 2</a>를 참고하여 작성하였습니다.  </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[그라파나 #4] 다양한 대시보드 만들기 3 - Histogram ]]></title>
            <link>https://velog.io/@lee_nah/%EA%B7%B8%EB%9D%BC%ED%8C%8C%EB%82%98-4-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-Histogram</link>
            <guid>https://velog.io/@lee_nah/%EA%B7%B8%EB%9D%BC%ED%8C%8C%EB%82%98-4-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-Histogram</guid>
            <pubDate>Thu, 01 Jan 2026 06:55:13 GMT</pubDate>
            <description><![CDATA[<p>이번 게시물에서는 Grafana 대시보드에서 Dashboard &gt; New panel을 통해 Histogram 시각화를 직접 만들어보는 과정을 정리해본다.
목표는 Kubernetes API Server 요청 처리 시간의 분포를 히스토그램으로 확인하는 것이다.</p>
<br>

<h1 id="api-server-요청-지표-확인">API Server 요청 지표 확인</h1>
<p>우선, 데이터가 정상적으로 수집되고 있는지를 확인하기 위해 Prometheus 쿼리를 먼저 실행한다.</p>
<pre><code class="language-bash">apiserver_request_duration_seconds_bucket</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/cc194f62-1871-4449-a950-74cb1ce45c75/image.png" alt=""></p>
<p>이 쿼리는 Kubernetes API Server가 요청을 처리하는 데 걸린 시간을 버킷(bucket) 단위로 누적 카운트한 히스토그램 메트릭이다.
그래프가 정상적으로 출력된다면, Prometheus에서 해당 메트릭을 문제없이 수집 중이라는 의미다.</p>
<br>

<h2 id="histogram_quantile-사용">histogram_quantile 사용</h2>
<p>Grafana의 suggestion에 표시된 쿼리를 클릭해 다음 쿼리를 실행한다.</p>
<pre><code class="language-bash">histogram_quantile(0.95, sum by(le) (rate(apiserver_request_duration_seconds_bucket[$__rate_interval])))</code></pre>
<p>이 쿼리의 의미는 다음과 같다.</p>
<ul>
<li><p>rate(...)
→ 히스토그램 버킷 값의 초당 증가율을 계산한다.</p>
</li>
<li><p>sum by(le)
→ 여러 시계열을 le(less than or equal) 버킷 기준으로 합친다.</p>
</li>
<li><p>histogram_quantile(0.95, ...)
→ 95퍼센타일(95% 요청이 이 값 이하로 처리됨) 을 계산한다.</p>
</li>
</ul>
<blockquote>
<p>즉,
“최근 구간에서 API 요청의 95%가 이 시간 이내로 처리된다” 라는 값을 시계열로 보여주는 쿼리다.</p>
</blockquote>
<p>실행하면 다음과 같이 표 형태의 결과가 출력된다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e1860e5e-b3c3-4268-bdc5-323cec1224bc/image.png" alt=""></p>
<br>

<h2 id="조회-시간을-1분-단위로-변경">조회 시간을 1분 단위로 변경</h2>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/b5a0d585-2b8c-4659-a5c2-1e9e437f86a4/image.png" alt="">
기본 $__rate_interval 대신, 고정된 1분 기준으로 보고 싶어서 다음과 같이 수정한다.</p>
<pre><code class="language-bash">histogram_quantile(0.95, sum by(le) (rate(apiserver_request_duration_seconds_bucket[1m])))</code></pre>
<p>이렇게 하면 1분 단위로 계산된 API Server 요청 지연 시간의 95퍼센타일 값을 확인할 수 있다.</p>
<br>

<h2 id="특정-요청-타입get만-필터링">특정 요청 타입(GET)만 필터링</h2>
<p>이번에는 모든 요청이 아니라, GET 요청만 대상으로 분석해본다. </p>
<pre><code class="language-bash">histogram_quantile(0.95, rate(apiserver_request_duration_seconds_bucket{verb=&quot;GET&quot;}[1m]))</code></pre>
<p>여기서 {verb=&quot;GET&quot;} 은
아래와 같이 apiserver_request_duration_seconds_bucket 메트릭에 포함된 레이블을 기반으로 필터링한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/96734364-79f1-428b-b4fa-5458f399cc6c/image.png" alt=""></p>
<pre><code class="language-bash">apiserver_request_duration_seconds_bucket</code></pre>
<p>즉,
API Server로 들어오는 GET 요청의 처리 시간 분포만 따로 분석하게 된다.</p>
<br>

<h2 id="그래프가-직관적이지-않은-이유">그래프가 직관적이지 않은 이유</h2>
<br>

<p><img src="https://velog.velcdn.com/images/lee_nah/post/0ee9bedc-e3f5-460c-be1d-ecd0c33c8a7f/image.png" alt=""> 
이 상태로 Grafana에서 조회하면 다음과 같이 그래프가 출력된다.</p>
<p>하지만 이 그래프는</p>
<p>시간에 따른 값 변화는 보이지만, 요청 지연 시간 분포를 한눈에 파악하기는 어렵다</p>
<p>그래서 여기서 Visualization을 Histogram으로 변경한다.</p>
<br>

<h2 id="histogram-시각화-적용">Histogram 시각화 적용</h2>
<p>Visualization에서 Histogram을 선택하면 다음과 같이 변경된다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/e59420fd-657e-4535-a4ca-006aebd949e3/image.png" alt="">
그러면 이와 같이 히스토그램으로 변경된 것을 확인할 수 있으며, API 서버의 요청 시간에 따른 각 구간의 빈도수를 시각적으로 효과적으로 확인할 수 있게 된다.</p>
<br>

<h2 id="단위-및-임계값threshold-설정">단위 및 임계값(Threshold) 설정</h2>
<br>

<p>마지막으로 가독성을 높이기 위해 단위와 임계값을 설정한다.</p>
<ul>
<li>Unit → time / seconds 로 설정
→ 요청 처리 시간이 초 단위라는 것을 명확히 표시</li>
</ul>
<p>그리고 SRI / SLO 관점에서 임계값을 보기 위해 Threshold를 추가한다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/9d53dd03-f912-473f-9ce1-22a4cef2013a/image.png" alt=""></p>
<ul>
<li>Threshold 값: 0.05</li>
<li>색상: 빨간색</li>
</ul>
<p>이렇게 설정하면
0.05초 이하 구간에 있는 요청들을 “문제가 있는 영역”으로 직관적으로 인지할 수 있다.</p>
<br>


<blockquote>
<p>해당 게시물은 인프런 강의 중 <a href="https://www.inflearn.com/courses/lecture?courseId=329716&amp;tab=curriculum&amp;type=LECTURE&amp;unitId=148939&amp;subtitleLanguage=ko">&quot;실습으로 배우는 그라파나&quot;</a>를 참고하여 작성하였습니다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[그라파나 #3] 다양한 대시보드 만들기 2 - Heatmap]]></title>
            <link>https://velog.io/@lee_nah/%EA%B7%B8%EB%9D%BC%ED%8C%8C%EB%82%98-3-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-Heatmap</link>
            <guid>https://velog.io/@lee_nah/%EA%B7%B8%EB%9D%BC%ED%8C%8C%EB%82%98-3-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-Heatmap</guid>
            <pubDate>Tue, 30 Dec 2025 12:25:16 GMT</pubDate>
            <description><![CDATA[<p>이번 게시물에서는 Grafana 대시보드에서 Kubernetes apiserver 요청에 대한 응답시간을 시간 흐름에 따라 Heatmap으로 시각화하는 방법을 정리해본다.</p>
<br>

<h1 id="apiserver-요청에-대한-응답시간-시간-흐름을-heatmap으로-나타내기">apiserver 요청에 대한 응답시간 (시간 흐름)을 Heatmap으로 나타내기</h1>
<p>먼저 apiserver 요청 응답시간에 대한 히스토그램 메트릭이 수집되고 있는지 확인한다.</p>
<pre><code class="language-bash">apiserver_request_duration_seconds_bucket</code></pre>
<p>해당 메트릭을 쿼리하면 다음과 같이 응답시간 구간(bucket)별 데이터가 출력되는 것을 확인할 수 있다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/lee_nah/post/de86e843-a74a-4549-aa2d-7b61d0aee9fe/image.png" alt=""></p>
<br>

<p>다음으로, 이 데이터를 Heatmap으로 표현하기 위해 가공한다.
히스토그램 메트릭은 누적 카운터 형태이기 때문에, 일정 시간 단위의 변화량을 보기 위해 rate 함수를 사용한다.
또한 응답시간 구간별 분포를 보기 위해 le 라벨 기준으로 그룹화하고, 나머지 라벨은 모두 합산한다.</p>
<p>최종적으로 사용하는 쿼리는 다음과 같다.</p>
<pre><code class="language-bash">sum by(le) (rate(apiserver_request_duration_seconds_bucket[1m]))</code></pre>
<p>이 쿼리는 최근 1분 동안 apiserver 요청이 각 응답시간 구간(le)에 얼마나 많이 발생했는지를 나타낸다.</p>
<p>해당 쿼리를 실행하면 아래와 같은 그래프가 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/17b91967-3b57-4885-99eb-fe8d55f71174/image.png" alt=""></p>
<br>

<p>하지만 이 상태에서는 시간대별로 어느 응답시간 구간에 요청이 몰렸는지를 직관적으로 파악하기 어렵다.
이를 개선하기 위해 패널 타입을 Heatmap으로 변경한다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/17aab442-4ee8-4145-b95b-7c43f9b1b3a9/image.png" alt=""></p>
<p>Heatmap으로 변경하면,
시간 흐름에 따라 특정 응답시간 구간에 요청이 집중되는 패턴을 색상으로 한눈에 확인할 수 있다.</p>
<br>

<p>추가로, Y축이 bucket 값(le) 그대로 표시되면 해석이 다소 어려울 수 있다.
이를 응답시간 단위로 명확히 표현하기 위해 Y축 설정에서 Time → Second로 변경한다.</p>
<p><img src="https://velog.velcdn.com/images/lee_nah/post/98364021-2580-4307-9eda-86070270b936/image.png" alt=""></p>
<p>이렇게 설정하면 Y축이 초 단위 응답시간으로 표시되어,
느린 요청이 어느 시간대에 발생했는지를 더욱 직관적으로 파악할 수 있다.</p>
<br>

<br>

<blockquote>
<p>해당 게시물은 인프런 강의 중 <a href="https://www.inflearn.com/courses/lecture?courseId=329716&amp;tab=curriculum&amp;type=LECTURE&amp;unitId=148939&amp;subtitleLanguage=ko">&quot;실습으로 배우는 그라파나&quot;</a>를 참고하여 작성하였습니다. </p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>