<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>장성필 기술블로그</title>
    <link>https://hackjsp.tistory.com/</link>
    <description>피드백은 언제나 환영입니다:)</description>
    <language>ko</language>
    <pubDate>Sat, 4 Jul 2026 07:53:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>장성필(hackjap)</managingEditor>
    <image>
      <title>장성필 기술블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/2740303/attach/81525585b603432597583809ce9fe39a</url>
      <link>https://hackjsp.tistory.com</link>
    </image>
    <item>
      <title>[AEWS4] EKS Auto Mode 살펴보기</title>
      <link>https://hackjsp.tistory.com/111</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. EKS Auto Mode란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS Auto Mode는 컨트롤 플레인뿐 아니라 데이터 플레인까지 AWS가 관리하는 EKS 운영 모드다. 2024년 12월 1일 re:Invent 2024에서 GA로 발표됐고, 쿠버네티스 1.29 이상 클러스터에서 쓸 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b184PS/dJMcaf7FCkE/ao3fbPPiLXxbKhnKnl4bK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b184PS/dJMcaf7FCkE/ao3fbPPiLXxbKhnKnl4bK0/img.png&quot; data-alt=&quot;https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/automode.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b184PS/dJMcaf7FCkE/ao3fbPPiLXxbKhnKnl4bK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb184PS%2FdJMcaf7FCkE%2Fao3fbPPiLXxbKhnKnl4bK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1403&quot; height=&quot;397&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/automode.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS를 운영해 본 사람이라면 &quot;컨트롤 플레인만 매니지드&quot;라는 표현이 충분하지 않다고 느낀 경험이 있을 것이다. 컨트롤 플레인 아래에는 운영자가 직접 챙겨야 할 영역이 두텁게 쌓여 있다. 노드 그룹 운영, OS 패치, Karpenter Helm 차트 관리, VPC CNI와 CoreDNS와 EBS CSI 데몬셋, AWS Load Balancer Controller 설치와 IRSA 매핑, 보안 패치 주기마다의 노드 교체 같은 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode는 이 영역까지 AWS가 들고 간다. AWS 공식 한국 블로그가 한 줄로 요약했다. &quot;EKS 컨트롤 플레인 외에도 AWS는 애플리케이션을 실행하는 데 필요한 EKS 클러스터의 AWS 인프라를 구성, 관리 및 보호합니다.&quot; 쉽게 말하면 운영자가 챙기던 데몬셋과 컨트롤러를 AWS가 가져가서, 사용자는 Pod과 워크로드 정의에만 집중하게 만든 모드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화되는 항목을 풀어서 보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 라이프사이클 (생성, 패치, 교체)&lt;/li&gt;
&lt;li&gt;컴퓨트 오토스케일링 (Karpenter)&lt;/li&gt;
&lt;li&gt;Pod과 서비스 네트워킹 (VPC CNI)&lt;/li&gt;
&lt;li&gt;클러스터 DNS (CoreDNS)&lt;/li&gt;
&lt;li&gt;블록 스토리지 (EBS CSI)&lt;/li&gt;
&lt;li&gt;애플리케이션 로드 밸런싱 (ALB Controller)&lt;/li&gt;
&lt;li&gt;Pod 단위 IAM 자격 (Pod Identity Agent)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 EKS에서 위 항목을 각각 Helm 차트나 EKS Addon으로 운영했던 사람이라면, Auto Mode가 무엇을 가져갔는지 바로 감이 잡힌다. 본문은 이 항목들 가운데 변화가 큰 네 컴포넌트(노드, CNI/CSI, 로드밸런서, IAM)를 차례로 살펴본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 일반 EKS와의 차이 한눈에 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 들어가기 전에 일반 EKS와 Auto Mode가 어디서 갈리는지를 표 하나로 정리해 둔다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;일반 EKS&lt;/th&gt;
&lt;th&gt;EKS Auto Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;컨트롤 플레인&lt;/td&gt;
&lt;td&gt;AWS 매니지드&lt;/td&gt;
&lt;td&gt;AWS 매니지드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노드 OS와 패치&lt;/td&gt;
&lt;td&gt;사용자 (NodeGroup, AMI 선택)&lt;/td&gt;
&lt;td&gt;AWS 매니지드 (Bottlerocket variant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Karpenter&lt;/td&gt;
&lt;td&gt;사용자 (Helm 설치)&lt;/td&gt;
&lt;td&gt;AWS 매니지드 (클러스터 밖에서 실행)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC CNI&lt;/td&gt;
&lt;td&gt;사용자 (DaemonSet)&lt;/td&gt;
&lt;td&gt;AWS 매니지드 (AMI 빌트인)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster DNS (CoreDNS)&lt;/td&gt;
&lt;td&gt;사용자 (Deployment)&lt;/td&gt;
&lt;td&gt;AWS 매니지드 (노드 systemd 서비스)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EBS CSI Driver&lt;/td&gt;
&lt;td&gt;사용자 (Addon)&lt;/td&gt;
&lt;td&gt;AWS 매니지드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Load Balancer Controller&lt;/td&gt;
&lt;td&gt;사용자 (Helm + IRSA)&lt;/td&gt;
&lt;td&gt;AWS 매니지드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EKS Pod Identity Agent&lt;/td&gt;
&lt;td&gt;사용자 (Addon)&lt;/td&gt;
&lt;td&gt;AWS 매니지드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod 워크로드&lt;/td&gt;
&lt;td&gt;사용자&lt;/td&gt;
&lt;td&gt;사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노드 SSH/SSM 접근&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM 매핑&lt;/td&gt;
&lt;td&gt;aws-auth ConfigMap&lt;/td&gt;
&lt;td&gt;EKS access entries 자동 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표에서 굵게 시선이 가는 행이 사용자가 챙기던 영역이 AWS 매니지드로 옮겨간 줄이다. 3장에서 그 가운데 가장 변화가 큰 네 영역을 풀어서 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 흥미로운 점은, Auto Mode 클러스터를 만들고 &lt;code&gt;kubectl get pods -A&lt;/code&gt;를 치면 결과가 거의 비어 있다는 사실이다. AWS Best Practices Guide의 설명을 보면 새로 만든 Auto Mode 클러스터에 떠 있는 Pod은 Metrics Server뿐이다. 일반 EKS에서 익숙했던 aws-node, coredns, ebs-csi-controller, aws-load-balancer-controller가 모두 클러스터 밖에서 AWS가 돌리는 형태로 바뀌었기 때문이다. 처음 보면 &quot;어, 진짜 비어 있네&quot;라는 인상을 받게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 자동화 영역 살펴보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 노드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 EKS에서 노드는 사실상 EC2 인스턴스였다. NodeGroup이든 Karpenter든 운영자가 챙길 항목이 많다. AMI 선택(Amazon Linux 2, Bottlerocket, Ubuntu, Windows), OS 패치 주기, 노드 그룹 인스턴스 타입, ASG 설정, Karpenter Helm 차트 버전, NodePool과 NodeClass YAML, SSH 키나 SSM 권한, 커널 튜닝과 보안 에이전트 같은 것들이다. 노드 자체를 사용자가 들고 있었으니 그 위에 얹히는 거의 모든 것에 사용자의 손이 닿았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode에서 노드는 EC2 Managed Instance라는 새로운 인스턴스 종류로 동작한다. 표준 EC2와 어떻게 다른지 정리하면 이렇다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표준 EC2&lt;/th&gt;
&lt;th&gt;Auto Mode Managed Instance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;사용자가 패치/업데이트 책임&lt;/td&gt;
&lt;td&gt;AWS가 자동 패치&amp;middot;업데이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 API로 종료 가능&lt;/td&gt;
&lt;td&gt;EKS가 워크로드에 맞춰 인스턴스 수 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH 접근 가능&lt;/td&gt;
&lt;td&gt;Pod와 컨테이너만 배포 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자가 OS/AMI 선택&lt;/td&gt;
&lt;td&gt;AWS가 OS/AMI 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows/Ubuntu 가능&lt;/td&gt;
&lt;td&gt;Linux 컨테이너만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자가 인스턴스 타입 선택&lt;/td&gt;
&lt;td&gt;AWS가 결정 (NodePool로 제한 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 Auto Mode의 노드는 &quot;어플라이언스&quot;처럼 다뤄진다. 사용자가 직접 만지지 않고 AWS가 알아서 관리하는 가전제품 같은 존재가 된다. AWS 공식 문서도 &quot;nodes are designed to be treated like appliances&quot;라는 표현을 쓴다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Bottlerocket variant&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode가 쓰는 OS는 표준 Bottlerocket이 아니라 그 위에 잠금을 더 건 variant다. 루트 파일시스템에 무결성 검사가 강제되고 SELinux Enforcing 모드가 동작한다. SSH와 SSM 같은 원격 접근 서비스는 아예 노드에 들어 있지 않다. 사용자가 노드에 진입할 길이 막혀 있다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 잠금이 얼마나 단단한지는 보안 백서의 한 줄에서 드러난다. &quot;Even the AWS account root user is unable to circumvent these constraints.&quot; AWS 계정 root user조차 우회하지 못한다. 보안은 강해지지만 사용자의 노드 접근권은 같이 사라진다. 이 부분은 4장에서 한 번 더 언급한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;노드는 주기적으로 교체된다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode의 노드는 패치하지 않고 교체한다. 새 AMI가 나오면 새 인스턴스를 띄우고 기존 인스턴스를 종료하는 방식이다. 두 개의 수치를 알아두면 좋다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 NodePool(&lt;code&gt;general-purpose&lt;/code&gt;)의 노드 만료 주기: 14일&lt;/li&gt;
&lt;li&gt;Auto Mode 전체의 강제 상한: 21일 (PDB가 막아도 강제 교체)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 2주에 한 번, 늦어도 3주에 한 번 노드가 새 AMI로 갈아엎어진다. AMI 자체는 일주일에 한 번 정도 새 버전이 나오고, 출시 전에 CVE 스캔과 노드 컨포먼스 테스트, Pod Identity 자격 획득 검증, GPU 호환성 테스트가 끝난 뒤에야 배포된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모델 덕분에 사용자는 OS 패치 주기와 CVE 대응 일정을 따로 잡지 않아도 된다. 사용자가 챙기던 일을 AWS가 가져간 대표적인 사례다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Karpenter는 클러스터 밖에서 돈다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode의 Karpenter는 클러스터 안에 Pod로 떠 있지 않다. AWS가 클러스터 외부에서 직접 운영한다. 사용자는 NodePool과 NodeClass 객체로 &quot;어떤 워크로드를 어떤 종류의 노드에 띄울지&quot;만 표현하면 된다. Karpenter Helm 차트 버전을 따라가거나 controller Pod의 리소스 limit을 조정하는 일이 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spot 인스턴스 중단 알림과 EC2 health event 처리도 AWS가 자동으로 처리한다. Spot 인스턴스를 적극 활용하던 팀이라면 이쪽의 자동 처리가 꽤 편하게 다가올 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 CNI와 CSI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 EKS에서 &lt;code&gt;kubectl get ds -n kube-system&lt;/code&gt;을 치면 데이터 플레인 컴포넌트가 줄지어 떴다. &lt;code&gt;aws-node&lt;/code&gt;(VPC CNI), &lt;code&gt;ebs-csi-node&lt;/code&gt;, &lt;code&gt;kube-proxy&lt;/code&gt; 같은 DaemonSet과 &lt;code&gt;coredns&lt;/code&gt; Deployment가 모두 클러스터 안에서 동작했다. 사용자가 버전과 권한과 리소스 limit을 관리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode에서는 같은 컴포넌트가 두 가지 방식으로 자리를 옮겼다. 노드 측 컴포넌트는 AMI 안에 systemd 서비스로 들어간다. 컨트롤러 측 컴포넌트는 클러스터 밖에서 AWS가 돌린다. 데몬셋도 컨트롤러 Pod도 사용자 클러스터에 보이지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;VPC CNI&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 변화는 설정 방식이다. 기존에 &lt;code&gt;aws-node&lt;/code&gt; DaemonSet 환경 변수로 조정하던 설정(&lt;code&gt;WARM_IP_TARGET&lt;/code&gt;, &lt;code&gt;ENABLE_PREFIX_DELEGATION&lt;/code&gt; 같은 것)은 더 이상 동작하지 않는다. AWS 공식 문서가 &quot;Configuration options for the previous AWS VPC CNI will not apply to EKS Auto Mode&quot;라고 못 박는다. 대신 NodeClass 객체에서 통합 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값도 바뀌었다. Prefix Delegation이 기본이 됐고, /28 prefix를 쓴다. Pod 수에 따라 IP 워밍 풀이 자동으로 늘었다 줄었다 한다. ENIConfig 같은 커스텀 네트워킹과 Security Groups per Pod 옛 방식, Warm IP/Warm prefix 직접 설정 등은 모두 미지원이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CoreDNS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreDNS는 더 이상 &lt;code&gt;kube-system/coredns&lt;/code&gt; Deployment로 떠 있지 않다. 대신 각 노드의 systemd 서비스로 동작한다. DNS 응답이 노드 안에서 처리되니 네트워크 홉이 줄고 안정성이 좋아진다. CoreDNS Pod을 HPA로 스케일하던 작업이 사라진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EBS CSI&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EBS CSI에서 알아둘 점은 두 가지다. 첫째, provisioner 이름이 바뀌었다. 기존 &lt;code&gt;ebs.csi.aws.com&lt;/code&gt;이 아니라 &lt;code&gt;ebs.csi.eks.amazonaws.com&lt;/code&gt;이다. 한 글자가 아니라 도메인 자체가 다르다. 둘째, Auto Mode는 StorageClass를 자동으로 만들어 주지 않는다. 사용자가 직접 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 동작은 더 안전한 쪽으로 옮겨갔다. 루트와 데이터 볼륨이 기본 암호화되고 KMS CMK 옵션도 지원한다. 단, EBS Detailed Performance Metrics를 Prometheus로 직접 수집하던 모니터링 스택은 Auto Mode에서 접근이 막혀 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;짧은 비교표&lt;/h4&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;일반 EKS&lt;/th&gt;
&lt;th&gt;EKS Auto Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VPC CNI 배포&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws-node&lt;/code&gt; DaemonSet&lt;/td&gt;
&lt;td&gt;AMI 빌트인 (systemd)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC CNI 설정&lt;/td&gt;
&lt;td&gt;DaemonSet env var / ENIConfig&lt;/td&gt;
&lt;td&gt;NodeClass 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix Delegation&lt;/td&gt;
&lt;td&gt;옵션&lt;/td&gt;
&lt;td&gt;기본값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CoreDNS&lt;/td&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;노드 systemd 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EBS CSI provisioner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ebs.csi.aws.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ebs.csi.eks.amazonaws.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EBS 기본 암호화&lt;/td&gt;
&lt;td&gt;옵션&lt;/td&gt;
&lt;td&gt;기본값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 로드밸런서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 EKS에서 Ingress로 ALB를 띄우는 과정은 단계가 길었다. &lt;code&gt;aws-load-balancer-controller&lt;/code&gt; Helm 차트를 설치한 뒤 IRSA로 controller에 ALB&amp;middot;EC2&amp;middot;WAF 권한을 부여한다. webhook 인증서를 관리하고 controller 버전도 따라가야 한다. Ingress YAML에는 &lt;code&gt;kubernetes.io/ingress.class: alb&lt;/code&gt;와 다수의 &lt;code&gt;alb.ingress.kubernetes.io/*&lt;/code&gt; 어노테이션을 붙였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode에서는 controller 자체가 매니지드다. Ingress 매니페스트만 들어오면 AWS가 ALB를 만들어 준다. 단, 매니페스트가 새 IngressClass를 가리켜야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IngressClass controller: &lt;code&gt;eks.amazonaws.com/alb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;API version: &lt;code&gt;eks.amazonaws.com/v1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 주의할 부분은 어노테이션 호환성이다. 기존에 자주 쓰던 어노테이션 가운데 일부는 더 이상 동작하지 않거나 다른 방식으로 대체됐다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기존 어노테이션&lt;/th&gt;
&lt;th&gt;Auto Mode 대응&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubernetes.io/ingress.class: alb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;미지원, &lt;code&gt;spec.ingressClassName&lt;/code&gt; 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alb.ingress.kubernetes.io/group.name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IngressClass 안에서 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alb.ingress.kubernetes.io/waf-acl-id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;미지원, WAF v2로 대체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alb.ingress.kubernetes.io/auth-type: oidc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 타겟팅 모드도 Instance Mode가 아니라 IP Mode다. 기본값으로 IP Mode를 쓰니까 Instance Mode에 의존하던 워크로드는 어노테이션으로 명시해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 마이그레이션 함정이 있다. 기존 ALB Controller가 만들어 둔 ALB를 Auto Mode 관리로 옮기는 경로는 지원되지 않는다. 새 IngressClass로 새 Ingress를 만들어 트래픽을 옮기는 방식만 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 IAM과 Pod Identity&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주차 글(&lt;a href=&quot;../week5/blog.md&quot;&gt;EKS IRSA 트러블슈팅 가이드&lt;/a&gt;)에서 IRSA가 깨질 수 있는 6가지 패턴을 정리했다. IRSA는 OIDC Provider 등록, ServiceAccount annotation, MutatingWebhook, SDK 호출, IAM Trust Policy 검증 5단계 위에서 동작한다. 그 5단계 가운데 하나라도 깨지면 &lt;code&gt;AccessDenied&lt;/code&gt;가 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode는 Pod Identity를 기본으로 쓴다. AWS 공식 문서에 &quot;You do not have to install the EKS Pod Identity Agent on EKS Auto Mode clusters&quot;라고 적혀 있다. Pod Identity Agent를 Addon으로 따로 깔지 않아도 자동으로 들어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 큰 변화는 IAM 매핑이다. 일반 EKS에서는 IAM Role을 쿠버네티스 권한과 연결하려면 &lt;code&gt;aws-auth&lt;/code&gt; ConfigMap을 수정해야 했다. Auto Mode에서는 EKS access entries로 자동 매핑된다. ConfigMap을 만지는 일이 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode는 두 개의 핵심 IAM Role을 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cluster IAM Role: AmazonEKSComputePolicy, AmazonEKSBlockStoragePolicy, AmazonEKSLoadBalancingPolicy, AmazonEKSNetworkingPolicy, AmazonEKSClusterPolicy 5종 자동 부착&lt;/li&gt;
&lt;li&gt;Node IAM Role: AmazonEKSWorkerNodeMinimalPolicy + AmazonEC2ContainerRegistryPullOnly만 부여&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 자체가 IAM 권한을 폭넓게 가지고 있던 기존 모델과 다르게, Node Role은 &quot;Pod Identity Role을 가져올 수 있는 권한&quot;과 &quot;ECR pull 권한&quot;만 남긴다. 사용자 워크로드가 필요로 하는 권한은 PodIdentityAssociation 객체로 SA에 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Auto Mode 쓰기 전에 알아두면 좋은 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode가 가져간 책임이 많아진 만큼 사용자가 양보한 부분도 있다. 학습 정리를 위해 알아두면 좋은 제약사항을 정리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;노드에 직접 들어갈 수 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.1에서 본 것처럼 SSH도 SSM도 막혀 있다. 노드에 진입해서 &lt;code&gt;journalctl&lt;/code&gt;을 보거나 &lt;code&gt;crictl ps&lt;/code&gt;로 컨테이너 상태를 확인하던 디버깅 방식이 통째로 사라진다. AWS는 그 빈자리를 네 가지 도구로 대체한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;NodeDiagnostic&lt;/code&gt; CRD: 노드의 시스템 로그와 네트워크 트래픽을 S3로 비동기 업로드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws ec2 get-console-output&lt;/code&gt;: 부팅&amp;middot;커널 단계의 콘솔 출력 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl debug node/&amp;lt;id&amp;gt;&lt;/code&gt;: debug Pod을 노드 namespace에 띄워 라이브 디버깅&lt;/li&gt;
&lt;li&gt;VPC Reachability Analyzer: 노드가 클러스터에 조인 못 할 때 경로 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브 디버깅이 필요할 때는 &lt;code&gt;kubectl debug node&lt;/code&gt;가 가장 자주 쓰인다. debug Pod이 같은 노드의 namespace를 공유하므로 &lt;code&gt;nsenter -t 1 -m journalctl -f -u kubelet&lt;/code&gt; 같은 명령으로 호스트의 kubelet 로그를 라이브로 따라갈 수 있다. SSH 대체 작업의 거의 모든 경우를 이 패턴으로 처리하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Karpenter 이벤트 로그도 평소처럼 &lt;code&gt;kubectl logs -n karpenter&lt;/code&gt;로 볼 수 없다. Karpenter가 클러스터 밖에 있기 때문이다. CloudWatch Logs Insights에서 &lt;code&gt;kube-apiserver-audit&lt;/code&gt; 로그를 쿼리해야 한다. 자주 등장하는 이벤트 키워드는 &lt;code&gt;DisruptionBlocked&lt;/code&gt;, &lt;code&gt;Unconsolidatable&lt;/code&gt;, &lt;code&gt;FailedScheduling&lt;/code&gt;, &lt;code&gt;InsufficientCapacityError&lt;/code&gt; 같은 것들이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IRSA의 trust policy 길이 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 메모에는 &quot;IRSA 512KB&quot; 같은 표현이 있었는데 1차 자료로 다시 확인해 보니 정확한 단위가 다르다. AWS IAM trust policy의 길이 한계는 기본 2,048자, 상한 4,096자(증가 요청 가능)다. KB가 아니라 글자 단위다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA는 한 IAM Role을 여러 EKS 클러스터에서 공유하려면 그 Role의 trust policy에 클러스터마다 OIDC Provider URL을 모두 적어야 한다. 각 항목이 글자 수를 잡아먹다 보니 멀티클러스터 환경에서 8~12개쯤에서 4,096자 한계에 부딪힌다는 보고가 자주 보인다. Rafay의 분석이 12개 클러스터를 한계로 명시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod Identity는 이 문제를 비켜 간다. trust policy를 만지지 않고 PodIdentityAssociation 객체로 어느 클러스터의 어느 SA가 어느 Role을 쓰는지 표현하기 때문이다. Auto Mode는 Pod Identity가 기본이라 멀티클러스터 운영에서 trust policy 길이 압박이 자연스럽게 풀린다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSK 같은 일부 워크로드는 Pod Identity가 안 통한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 메모에 &quot;Kafka 4시간 키 로테이션&quot;이라고 적어 둔 부분도 1차 자료로 보면 정확하지 않다. 실제 동작은 &quot;MSK IAM 인증이 Pod Identity의 동적 STS session name과 호환되지 않아 재인증에 실패&quot;하는 쪽이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원리는 이렇다. MSK IAM 인증은 클라이언트가 보낸 STS session name을 토대로 권한을 확인한다. Pod Identity는 매 호출마다 새로운 무작위 session name을 만든다. 처음 인증할 때 쓰던 session name과 15분 뒤 자격 갱신 시점의 session name이 달라서 MSK가 &quot;다른 신원&quot;으로 보고 재인증에 실패한다. aws-msk-iam-auth 라이브러리의 &lt;a href=&quot;https://github.com/aws/aws-msk-iam-auth/issues/143&quot;&gt;Issue #143&lt;/a&gt;이 같은 현상을 그대로 적어 두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer가 15분 동안 잘 동작하다가 자격 만료 시점에 떨어진다는 보고다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA는 고정된 session name을 쓰기 때문에 같은 문제가 발생하지 않는다. Auto Mode 클러스터에서도 IRSA는 함께 사용 가능하니, MSK 워크로드만 IRSA로 별도 설정하는 패턴을 쓰면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AMI 커스터마이징은 불가능하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode 노드의 AMI는 AWS가 제공하는 Bottlerocket variant로 고정되어 있다. 사용자가 자기 AMI로 바꿀 수 없다. AWS Best Practices Guide의 FAQ도 &quot;currently the only supported AMIs are for Amazon-provided Bottlerocket&quot;이라고 답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트 레벨에서 동작해야 하는 보안 스캐너나 EDR 같은 에이전트는 AMI에 굽거나 노드에 직접 설치할 수 없다. AWS의 권장은 같은 기능을 쿠버네티스 DaemonSet으로 옮기는 방식이다. Pod 경계 안에서 동작 가능한 에이전트라면 큰 문제가 없지만, 노드 OS에 직접 깔려야만 동작하는 에이전트는 Auto Mode에 들어올 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IMDS 설정도 잠겨 있다. IMDSv2와 hop limit 1이 강제고 변경 불가다. IMDSv1을 가정한 구버전 SDK는 Auto Mode 환경에서 자격을 가져올 수 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 학습 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Auto Mode는 운영 부담을 크게 덜어 주는 매력적인 모드라고 느꼈다. 다만 노드 접근 차단과 AMI 고정처럼 사용자가 볼 수 있는 영역이 좁아지는 트레이드오프는 분명하다. 본인 워크로드가 호스트 레벨 접근이나 커스터마이징을 얼마나 요구하느냐가 결국 도입 가능 여부를 가른다고 본다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 참고 자료&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS 공식 문서&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/automode.html&quot;&gt;Automate cluster infrastructure with EKS Auto Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/automode-learn-instances.html&quot;&gt;Learn about EKS Auto Mode Managed instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/auto-learn-iam.html&quot;&gt;Learn about identity and access in EKS Auto Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/auto-networking.html&quot;&gt;Learn about VPC Networking and Load Balancing in EKS Auto Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/auto-configure-alb.html&quot;&gt;Create an IngressClass to configure an Application Load Balancer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/create-storage-class.html&quot;&gt;Create a storage class&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/auto-troubleshoot.html&quot;&gt;Troubleshoot EKS Auto Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/security-overview-amazon-eks-auto-mode/eks-auto-mode-data-plane.html&quot;&gt;Security Overview of EKS Auto Mode (Whitepaper)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/best-practices/automode.html&quot;&gt;EKS Auto Mode Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS 공식 블로그&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/blogs/aws/streamline-kubernetes-cluster-management-with-new-amazon-eks-auto-mode/&quot;&gt;Streamline Kubernetes cluster management with the new Amazon EKS Auto Mode (AWS News)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/korea/streamline-kubernetes-cluster-management-with-new-amazon-eks-auto-mode/&quot;&gt;Amazon EKS Auto Mode 출시 (한국 블로그)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/blogs/containers/amazon-eks-pod-identity-a-new-way-for-applications-on-eks-to-obtain-iam-credentials/&quot;&gt;Amazon EKS Pod Identity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AWS/EKS</category>
      <category>EKS</category>
      <category>EKS AutoMode</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/111</guid>
      <comments>https://hackjsp.tistory.com/111#entry111comment</comments>
      <pubDate>Sat, 16 May 2026 19:38:09 +0900</pubDate>
    </item>
    <item>
      <title>AWS EKS Upgrade 워크샵(1.30 &amp;rarr; 1.31)</title>
      <link>https://hackjsp.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 워크샵 &lt;code&gt;EKS Upgrades&lt;/code&gt;(&lt;a href=&quot;https://catalog.workshops.aws/eks-upgrades/en-US&quot;&gt;catalog.workshops.aws/eks-upgrades&lt;/a&gt;) 환경을 제공해주셔서 그대로 따라가며 정리한다. 워크샵은 1.30 &amp;rarr; 1.31 한 단계 인플레이스 업그레이드를 컨트롤 플레인 &amp;rarr; 애드온 &amp;rarr; 4가지 노드 타입 순으로 수행한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 EKS 업그레이드의 책임 분담&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 업그레이드에서 가장 먼저 잡고 가야 하는 개념은 &lt;b&gt;공동 책임 모델&lt;/b&gt;이다. 컨트롤 플레인 컴포넌트(API Server &amp;middot; etcd &amp;middot; Scheduler &amp;middot; Controller Manager)는 AWS의 관리 VPC에서 동작하며, 그 가용성&amp;middot;백업&amp;middot;패치&amp;middot;고가용성 토폴로지는 모두 AWS가 보장한다. 반면 그 위에서 동작하는 &lt;b&gt;데이터 플레인(워커 노드, kubelet, kube-proxy, CNI, 사용자 워크로드)&lt;/b&gt; 의 호환성과 가용성은 전적으로 사용자 몫이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업그레이드 관점에서 이 분담은 매우 구체적인 의미를 갖는다. AWS는 컨트롤 플레인 버전을 1.30 &amp;rarr; 1.31로 올려주지만, 그 다음 노드 위에서 돌고 있는 kubelet과 add-on 컴포넌트가 새 API Server와 호환되는지, 사용자 워크로드가 deprecated된 API를 호출하지 않는지는 사용자가 책임진다. 클러스터 버전을 한 줄 바꾸면 끝나는 게 아니라, &lt;b&gt;컨트롤 플레인 &amp;rarr; 애드온 &amp;rarr; 노드 &amp;rarr; 워크로드 순서로 호환성을 한 단계씩 끌어올리는 캐스케이드&lt;/b&gt;로 이해해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 왜 어려운가&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;컨트롤 플레인은 한 단계씩만&lt;/b&gt;: 1.30 &amp;rarr; 1.32 같은 점프 업그레이드는 불가능하다. EKS는 Kubernetes 자체의 버전 스큐 정책을 그대로 따르며, n+1만 허용한다. 1.30에서 1.32로 가려면 1.31을 거쳐야 하고, 각 단계마다 데이터 플레인 동기화 &amp;rarr; 사전 점검 &amp;rarr; apply 사이클을 새로 돌려야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노드 타입마다 절차가 다르다&lt;/b&gt;: Managed Node Group은 EKS가 롤링을 대행하지만, Self-Managed는 ASG Instance Refresh를 직접 트리거해야 하고, Karpenter는 컨트롤러가 NodeClaim을 새로 만드는 모델이며, Fargate는 노드라는 개념 자체가 없어 파드 재시작으로 대체된다. 한 클러스터에 4종이 공존하면 업그레이드 절차도 4가지를 모두 알고 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 플레인 가용성&lt;/b&gt;: 노드를 갈아 끼우는 동안에도 트래픽은 흐른다. PodDisruptionBudget &amp;middot; TopologySpreadConstraints &amp;middot; Managed Node Group의 Surge 정책 &amp;middot; ASG Health Check가 모두 맞물려 있어야 무중단이 보장된다. 한 군데라도 헐거우면 그 워크로드만 정확히 트래픽 손실이 난다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Deprecated API&lt;/b&gt;: K8s는 마이너 버전마다 일부 API를 deprecate한다. PodSecurityPolicy(1.25에서 제거), &lt;code&gt;flowcontrol.apiserver.k8s.io/v1beta2&lt;/code&gt;(1.29), &lt;code&gt;node.k8s.io/v1beta1&lt;/code&gt; 등 이전 버전에서 멀쩡히 쓰던 매니페스트가 새 컨트롤 플레인에서는 거부될 수 있다. EKS Upgrade Insights가 이를 사전에 잡아주는 이유다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 워크샵 환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵은 us-west-2(오레곤) 리전 위에서 동작한다. AWS CloudFormation으로 띄운 &lt;code&gt;IDE-Server&lt;/code&gt;(EC2 + code-server)에 접속해 작업하고, EKS 클러스터(&lt;code&gt;eksworkshop-eksctl&lt;/code&gt;) 자체는 &lt;a href=&quot;https://github.com/aws-ia/terraform-aws-eks-blueprints&quot;&gt;terraform-aws-eks-blueprints&lt;/a&gt; 기반 Terraform으로 이미 배포되어 있다. 모든 업그레이드 작업은 이 Terraform 코드를 수정하고 &lt;code&gt;terraform apply&lt;/code&gt;를 돌리는 방식으로 수행한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;컴포넌트&lt;/th&gt;
&lt;th&gt;초기 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control Plane&lt;/td&gt;
&lt;td&gt;1.30 (platformVersion &lt;code&gt;eks.65&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Node group&lt;/td&gt;
&lt;td&gt;&lt;code&gt;initial&lt;/code&gt;, &lt;code&gt;blue-mng&lt;/code&gt; (AL2023, 1.30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-Managed Node group&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default-selfmng&lt;/code&gt; (AL2023, 1.30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Karpenter NodePool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt; (Spot)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fargate Profile&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fp-profile&lt;/code&gt; (namespace: &lt;code&gt;assets&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;애드온&lt;/td&gt;
&lt;td&gt;vpc-cni, coredns, kube-proxy, aws-ebs-csi-driver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitOps&lt;/td&gt;
&lt;td&gt;ArgoCD (apps, assets, carts, catalog, checkout, karpenter, orders, ui &amp;hellip;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 앱(retail-store-sample)은 ArgoCD가 GitOps로 동기화한 상태고, UI는 NLB로 노출해 두었다. 업그레이드 진행 중 다음 두 루프를 따로 띄워두면 영향도를 즉시 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# UI 헬스체크 반복
while true; do curl -s $UI_WEB; echo; date; sleep 1; done

# 클러스터 메타 반복
while true; do date; aws eks describe-cluster --name $EKS_CLUSTER_NAME \
  | egrep 'version|endpoint&quot;|issuer|platformVersion'; echo; sleep 2; done&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 업그레이드 사전 점검&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 EKS Upgrade Insights&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS는 &lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/cluster-insights.html&quot;&gt;Upgrade Insights&lt;/a&gt; API를 통해 &lt;b&gt;현재 클러스터를 다음 버전으로 올렸을 때 발생할 호환성 문제를 사전에 진단&lt;/b&gt;한다. 클러스터 audit log와 컴포넌트 메타데이터를 분석해 다음과 같은 항목을 자동 점검한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;kube-proxy / CoreDNS / VPC CNI 버전 스큐&lt;/b&gt; &amp;mdash; 컨트롤 플레인과 일치하는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Deprecated API 호출&lt;/b&gt; &amp;mdash; audit log를 스캔해 v1beta1 등 사라질 API를 누가 호출하는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Add-on 호환성&lt;/b&gt; &amp;mdash; 다음 버전에서 사용 가능한지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노드 OS / 커널 / containerd 버전&lt;/b&gt; &amp;mdash; 새 K8s 버전 요구사항 충족 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Insight는 &lt;code&gt;PASSING / WARNING / ERROR&lt;/code&gt; 중 하나의 상태와, 통과하지 못했을 때 어디를 손봐야 하는지에 대한 &lt;code&gt;recommendation&lt;/code&gt;을 함께 반환한다. &lt;b&gt;업그레이드 전 ERROR가 하나라도 남아 있으면 진행을 멈추는 게 원칙&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;aws eks list-insights --filter kubernetesVersions=1.31 \
  --cluster-name $CLUSTER_NAME | jq .&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;676cc9cf-...&quot;,
  &quot;name&quot;: &quot;kube-proxy version skew&quot;,
  &quot;category&quot;: &quot;UPGRADE_READINESS&quot;,
  &quot;kubernetesVersion&quot;: &quot;1.31&quot;,
  &quot;insightStatus&quot;: {
    &quot;status&quot;: &quot;PASSING&quot;,
    &quot;reason&quot;: &quot;kube-proxy versions match the cluster control plane version.&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233028.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3226&quot; data-origin-height=&quot;1278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caadGF/dJMcaffrtAF/TsQeK9XuhKoAJJlF8GVOZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caadGF/dJMcaffrtAF/TsQeK9XuhKoAJJlF8GVOZK/img.png&quot; data-alt=&quot;그림 1 &amp;amp;mdash; EKS Upgrade Insights 콘솔 화면. PASSING / WARNING / ERROR 카테고리로 한눈에 본다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caadGF/dJMcaffrtAF/TsQeK9XuhKoAJJlF8GVOZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaadGF%2FdJMcaffrtAF%2FTsQeK9XuhKoAJJlF8GVOZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3226&quot; height=&quot;1278&quot; data-origin-width=&quot;3226&quot; data-origin-height=&quot;1278&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1 &amp;mdash; EKS Upgrade Insights 콘솔 화면. PASSING / WARNING / ERROR 카테고리로 한눈에 본다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 PodDisruptionBudget 사전 점검&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 플레인 업그레이드는 노드 드레인을 동반한다. &lt;code&gt;kubectl drain&lt;/code&gt;은 내부적으로 &lt;b&gt;eviction subresource API&lt;/b&gt;(&lt;code&gt;POST /api/v1/namespaces/{ns}/pods/{name}/eviction&lt;/code&gt;)를 호출하는데, 이 API는 삭제 전에 해당 파드가 속한 PDB를 평가한다. PDB가 정의한 최소 가용성을 위반하게 되면 API Server가 &lt;code&gt;429 Too Many Requests&lt;/code&gt;를 반환하고 eviction은 거부된다. 즉 &lt;b&gt;PDB는 단순한 어노테이션이 아니라 K8s API Server가 강제하는 admission 정책&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 관점에서 핵심은 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PDB가 없는 워크로드는 노드와 함께 그냥 사라진다&lt;/b&gt; &amp;mdash; drain 명령은 무방비 상태로 evict를 강행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PDB만 있고 replicas가 1이면 노드 드레인이 영원히 막힌다&lt;/b&gt; &amp;mdash; &lt;code&gt;minAvailable: 1&lt;/code&gt; + replicas 1 조합은 흔한 함정이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵은 &lt;code&gt;orders&lt;/code&gt; 앱에 PDB가 없다는 점을 짚고 추가 &amp;rarr; 검증 시나리오를 따라간다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# apps/orders/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: orders-pdb
  namespace: orders
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: orders&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArgoCD로 sync한 뒤 &lt;code&gt;kubectl drain&lt;/code&gt;을 시도해 보면, PDB 위반으로 evict가 거부된다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;kubectl drain &quot;$nodeName&quot; --ignore-daemonsets --force --delete-emptydir-data
# error when evicting pods/&quot;orders-...&quot; -n &quot;orders&quot;: Cannot evict pod as it would violate
# the pod's disruption budget.&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; drain은 cordon까지 같이 한다&lt;br /&gt;drain이 PDB로 막혀도 노드는 이미 SchedulingDisabled 상태가 되어 새 파드가 잡히지 않는다. Ctrl+C로 멈추면 kubectl uncordon으로 명시적으로 풀어줘야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 버전 스큐 정책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K8s는 컴포넌트 간 통신 호환성을 보장하기 위해 &lt;a href=&quot;https://kubernetes.io/releases/version-skew-policy/&quot;&gt;Version Skew Policy&lt;/a&gt;를 GA 정책으로 명시한다. EKS는 이 정책을 그대로 따른다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;컴포넌트&lt;/th&gt;
&lt;th&gt;컨트롤 플레인과 허용 차이&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;td&gt;최대 2 마이너 (1.30 컨트롤 &amp;rarr; 1.28 kubelet 까지)&lt;/td&gt;
&lt;td&gt;노드 업그레이드를 며칠 미뤄도 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kube-proxy&lt;/td&gt;
&lt;td&gt;최대 2 마이너&lt;/td&gt;
&lt;td&gt;보통 kubelet과 함께 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;controller-manager / scheduler&lt;/td&gt;
&lt;td&gt;API Server와 동일 마이너&lt;/td&gt;
&lt;td&gt;EKS 관리 영역&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;client (kubectl, client-go)&lt;/td&gt;
&lt;td&gt;컨트롤 플레인 &amp;plusmn;1 마이너&lt;/td&gt;
&lt;td&gt;CI 파이프라인 점검 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CoreDNS / VPC CNI&lt;/td&gt;
&lt;td&gt;EKS 호환성 표 별도 확인&lt;/td&gt;
&lt;td&gt;add-on은 별도 매트릭스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정책의 의도는 단순하다. &lt;b&gt;API Server는 항상 N 버전이고, 그 외 컴포넌트는 N-1 또는 N-2까지 허용함으로써 점진적 업그레이드(rolling upgrade)를 가능하게 한다&lt;/b&gt;. 만약 동시에 모두 같은 버전이어야 한다면 무중단 업그레이드 자체가 불가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 적용은 다음과 같다. 컨트롤 플레인을 1.30 &amp;rarr; 1.31로 올린 뒤 노드는 1.30 그대로 며칠 운영해도 무방하다. 다만 다음 업그레이드(1.31 &amp;rarr; 1.32)를 시작하기 전에는 반드시 노드를 1.31로 맞춰야 한다. 그렇지 않으면 컨트롤 1.32 + 노드 1.30 조합이 되어 스큐 한계를 넘어선다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 standard vs extended support&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 버전은 출시 후 &lt;b&gt;14개월간 standard 지원&lt;/b&gt;, 이후 &lt;b&gt;12개월간 extended 지원&lt;/b&gt;으로 운영된다. standard 기간에는 추가 비용이 없지만, extended 기간으로 진입하면 시간당 추가 요금($0.60/hr 수준)이 부과된다. 비용 영향이 있으니 클러스터 단위로 의식적으로 정책을 잡아야 한다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;aws eks describe-cluster --name $EKS_CLUSTER_NAME \
  --query 'cluster.upgradePolicy'
# { &quot;supportType&quot;: &quot;EXTENDED&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 워크샵 클러스터는 &lt;code&gt;EXTENDED&lt;/code&gt;로 설정되어 있다. 운영 환경에서는 &lt;code&gt;STANDARD&lt;/code&gt;로 두면 standard 기간이 끝나는 시점에 자동으로 EKS가 다음 버전으로 강제 업그레이드를 수행하므로, 업그레이드 일정을 사용자가 통제하고 싶다면 &lt;code&gt;EXTENDED&lt;/code&gt;로 명시하고 직접 사이클을 돌리는 패턴이 일반적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 전략 비교 &amp;mdash; In-place vs Blue/Green&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전략&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;th&gt;적합한 환경&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;In-place&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;비용 추가 없음, 절차 단순, Terraform 한 줄&lt;/td&gt;
&lt;td&gt;롤백 사실상 불가 (control plane downgrade 불허), 같은 클러스터 ID 위에서 진행&lt;/td&gt;
&lt;td&gt;개발/스테이징, 무상태 서비스 위주 프로덕션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Blue/Green (cluster)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;검증 후 트래픽 전환, 즉시 롤백 가능, 클러스터 ID 자체 교체 가능&lt;/td&gt;
&lt;td&gt;클러스터 1세트 비용 추가, PV/StatefulSet 데이터 마이그레이션, DNS 전환 설계 필요&lt;/td&gt;
&lt;td&gt;금융&amp;middot;의료&amp;middot;결제 등 다운타임 비용이 큰 프로덕션&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 워크샵은 In-place로 컨트롤 플레인을 올리고, &lt;b&gt;노드 그룹 단위에서만 Blue/Green을 일부 섞어 쓴다&lt;/b&gt;. 즉 컨트롤 플레인 자체는 한 번만 올리지만, 데이터 플레인의 일부 워크로드(EBS PV를 쓰는 &lt;code&gt;orders&lt;/code&gt; 등)는 신규 노드그룹으로 옮기는 하이브리드 전략이다. 이는 실무에서도 흔한 패턴으로, &lt;b&gt;컨트롤 플레인은 in-place, 위험도가 높은 데이터 플레인 일부는 blue/green&lt;/b&gt;이라는 두 단의 조합을 통해 비용과 안전성을 모두 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 변경은 &lt;code&gt;~/environment/terraform/&lt;/code&gt;의 코드를 고친 뒤 &lt;code&gt;terraform apply&lt;/code&gt; 한 번으로 수행한다. 콘솔 클릭 / &lt;code&gt;eksctl&lt;/code&gt; / &lt;code&gt;aws cli&lt;/code&gt; 옵션도 있지만 &lt;b&gt;드리프트(Terraform state와 실제 리소스 불일치) 발생을 막기 위해 단일 진실 원천을 Terraform으로 통일&lt;/b&gt;한다. 콘솔에서 한 번이라도 직접 변경하면 다음 &lt;code&gt;terraform plan&lt;/code&gt;이 그 차이를 되돌리려 시도하면서 사고로 이어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Control Plane Upgrade&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 4가지 방법&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;한 줄 요약&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eksctl upgrade cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CLI 한 줄&lt;/td&gt;
&lt;td&gt;한 단계만 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS 관리 콘솔&lt;/td&gt;
&lt;td&gt;클릭&lt;/td&gt;
&lt;td&gt;드리프트 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws eks update-cluster-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API 직접 호출&lt;/td&gt;
&lt;td&gt;노드 그룹 버전 일치 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Terraform&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cluster_version&lt;/code&gt; 변수 변경&lt;/td&gt;
&lt;td&gt;✅ 채택&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Terraform으로 컨트롤 플레인 올리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;variables.tf&lt;/code&gt;의 &lt;code&gt;cluster_version&lt;/code&gt; 변수를 &lt;code&gt;1.30&lt;/code&gt; &amp;rarr; &lt;code&gt;1.31&lt;/code&gt;로 바꾼다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233030.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3248&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SZeqz/dJMcagk3Nnf/KXGo6cCIOJ3mEkp4Ss8kPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SZeqz/dJMcagk3Nnf/KXGo6cCIOJ3mEkp4Ss8kPK/img.png&quot; data-alt=&quot;그림 2 &amp;amp;mdash;&amp;amp;nbsp;variables.tf의 한 줄만 바꾸면 컨트롤 플레인 업그레이드 plan이 잡힌다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SZeqz/dJMcagk3Nnf/KXGo6cCIOJ3mEkp4Ss8kPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSZeqz%2FdJMcagk3Nnf%2FKXGo6cCIOJ3mEkp4Ss8kPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3248&quot; height=&quot;490&quot; data-origin-width=&quot;3248&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2 &amp;mdash;&amp;nbsp;variables.tf의 한 줄만 바꾸면 컨트롤 플레인 업그레이드 plan이 잡힌다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 현재 이미지 스냅샷 (이후 비교용)
kubectl get pods -A -o jsonpath=&quot;{.items[*].spec.containers[*].image}&quot; \
  | tr -s '[[:space:]]' '\n' | sort | uniq -c &amp;gt; 1.30.txt

terraform plan -no-color &amp;gt; plan-output.txt   # IDE에서 검토
terraform apply -auto-approve&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 결과 &amp;mdash; 파드는 단 한 개도 재생성되지 않는다&lt;/h3&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;aws eks describe-cluster --name $EKS_CLUSTER_NAME \
  | egrep 'version|endpoint&quot;|issuer|platformVersion'
#   &quot;version&quot;: &quot;1.31&quot;,
#   &quot;endpoint&quot;: &quot;https://...sk1.us-west-2.eks.amazonaws.com&quot;,   # 동일
#   &quot;issuer&quot;:   &quot;https://oidc.eks.us-west-2.amazonaws.com/...&quot;, # 동일
#   &quot;platformVersion&quot;: &quot;eks.X&quot;,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;endpoint&lt;/code&gt;와 OIDC &lt;code&gt;issuer&lt;/code&gt;가 그대로 유지되는 게 핵심 포인트다. EKS는 컨트롤 플레인을 업그레이드할 때 새로운 클러스터 인스턴스를 만들지 않고 &lt;b&gt;기존 클러스터의 마스터 컴포넌트만 롤링 교체한다&lt;/b&gt;. 클러스터 식별자가 같으니 OIDC issuer URL(&lt;code&gt;oidc.eks.{region}.amazonaws.com/id/{clusterId}&lt;/code&gt;)도 동일하다. 즉 &lt;b&gt;IRSA를 쓰는 워크로드(LBC, EBS CSI, Karpenter, &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ArgoCD 등)는 IAM Role의 Trust Policy를 다시 잡을 필요가 없고, 파드 재시작이나 토큰 재발급도 발생하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Blue/Green Cluster 업그레이드와 가장 큰 차이점이다. 신규 클러스터를 띄우면 OIDC issuer URL이 바뀌므로, IRSA를 쓰는 모든 IAM Role의 Trust Policy를 새 issuer로 갱신해야 한다. 운영 클러스터의 IAM Role 수십~수백 개를 한 번에 갱신해야 하는 부담은 Blue/Green Cluster 전략의 숨은 비용이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;diff 1.30.txt 1.31.txt
# (출력 없음 &amp;mdash; 컨테이너 이미지 동일)

kubectl get pod -A   # AGE가 모두 업그레이드 이전 그대로&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233032.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3262&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmMVhv/dJMcagMaBDD/XPR6cfsEEPMR17KzkT1gd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmMVhv/dJMcagMaBDD/XPR6cfsEEPMR17KzkT1gd1/img.png&quot; data-alt=&quot;그림 3 &amp;amp;mdash; 컨트롤 플레인이 1.31로 올라간 직후 상태. 모든 데이터 플레인은 아직 1.30 그대로 동작한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmMVhv/dJMcagMaBDD/XPR6cfsEEPMR17KzkT1gd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmMVhv%2FdJMcagMaBDD%2FXPR6cfsEEPMR17KzkT1gd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3262&quot; height=&quot;886&quot; data-origin-width=&quot;3262&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 3 &amp;mdash; 컨트롤 플레인이 1.31로 올라간 직후 상태. 모든 데이터 플레인은 아직 1.30 그대로 동작한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; Control Plane만 올라간 클러스터는 정상이다&lt;br /&gt;컨트롤 플레인이 1.31, kubelet은 1.30인 상태로 며칠씩 굴러가도 무방하다. 버전 스큐가 2 마이너 이내면 EKS는 이를 지원되는 운영 상태로 본다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Add-on Upgrade&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreDNS, kube-proxy, VPC CNI, EBS CSI Driver 4개 애드온을 컨트롤 플레인 버전에 맞춰 끌어올린다. 이 4개는 모두 EKS의 &lt;b&gt;Managed Add-on&lt;/b&gt;으로 등록할 수 있는 컴포넌트로, helm으로 직접 띄우는 self-deployed 방식과 비교했을 때 다음 이점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EKS API로 버전 관리&lt;/b&gt; &amp;mdash; &lt;code&gt;aws eks describe-addon-versions&lt;/code&gt;로 K8s 버전별 호환 매트릭스를 자동 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CVE 패치 자동 적용&lt;/b&gt; &amp;mdash; 보안 패치를 EKS가 직접 푸시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IRSA 자동 연동&lt;/b&gt; &amp;mdash; &lt;code&gt;service_account_role_arn&lt;/code&gt; 한 줄로 ServiceAccount까지 묶어줌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Configuration Schema 검증&lt;/b&gt; &amp;mdash; 잘못된 config는 apply 단계에서 거부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자체 관리(helm) 방식은 유연성은 높지만 위 모든 것을 사용자가 직접 챙겨야 한다. 컨트롤 플레인과 강결합된 핵심 컴포넌트는 EKS Managed Add-on을 쓰는 게 정석이다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;eksctl get addon --cluster $CLUSTER_NAME
# UPDATE AVAILABLE 컬럼에서 후보 버전 확인

aws eks describe-addon-versions --addon-name coredns \
  --kubernetes-version 1.31 --output table \
  --query &quot;addons[].addonVersions[:5].{Version:addonVersion,Default:compatibilities[0].defaultVersion}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 버전을 &lt;code&gt;addons.tf&lt;/code&gt;의 각 addon 블록에 박아 넣고 &lt;code&gt;terraform apply&lt;/code&gt; 한 번이면 끝난다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233031.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3262&quot; data-origin-height=&quot;1756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/urZRx/dJMcaaFbSMG/dxG5U1zMHNIzXJDqV6Yka0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/urZRx/dJMcaaFbSMG/dxG5U1zMHNIzXJDqV6Yka0/img.png&quot; data-alt=&quot;그림 4 &amp;amp;mdash;&amp;amp;nbsp;addons.tf&amp;amp;nbsp;업데이트. 각 애드온 버전을 명시하고 apply 한 번.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/urZRx/dJMcaaFbSMG/dxG5U1zMHNIzXJDqV6Yka0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FurZRx%2FdJMcaaFbSMG%2FdxG5U1zMHNIzXJDqV6Yka0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3262&quot; height=&quot;1756&quot; data-origin-width=&quot;3262&quot; data-origin-height=&quot;1756&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 4 &amp;mdash;&amp;nbsp;addons.tf&amp;nbsp;업데이트. 각 애드온 버전을 명시하고 apply 한 번.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# addons.tf 발췌
coredns = {
  addon_version = &quot;v1.11.4-eksbuild.33&quot;
}
kube-proxy = {
  addon_version = &quot;v1.31.14-eksbuild.9&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; VPC CNI는 호환성 표를 따로 본다&lt;br /&gt;VPC CNI는 컨트롤 플레인 버전과 1:1 매핑이 아니다. &quot;1.31에서 동작하는 VPC CNI 최신 버전&quot;을 &lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html&quot;&gt;VPC CNI 호환성 표&lt;/a&gt;에서 별도로 확인하고 박아야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Node Upgrade &amp;mdash; 4가지 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터가 진짜다. 노드 타입별로 갱신 메커니즘이 모두 다르다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패턴&lt;/th&gt;
&lt;th&gt;메커니즘&lt;/th&gt;
&lt;th&gt;책임 주체&lt;/th&gt;
&lt;th&gt;핵심 변수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Managed In-Place&lt;/td&gt;
&lt;td&gt;EKS 자체 롤링 (Surge)&lt;/td&gt;
&lt;td&gt;EKS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cluster_version&lt;/code&gt;, &lt;code&gt;ami_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Blue/Green&lt;/td&gt;
&lt;td&gt;신규 노드그룹 생성 + 마이그레이션&lt;/td&gt;
&lt;td&gt;사용자 + EKS&lt;/td&gt;
&lt;td&gt;신규 nodegroup block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Karpenter&lt;/td&gt;
&lt;td&gt;NodeClass의 amiSelectorTerms 변경&lt;/td&gt;
&lt;td&gt;Karpenter 컨트롤러&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EC2NodeClass&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-Managed&lt;/td&gt;
&lt;td&gt;ASG Instance Refresh&lt;/td&gt;
&lt;td&gt;사용자 + ASG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ami_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fargate&lt;/td&gt;
&lt;td&gt;Deployment rollout restart&lt;/td&gt;
&lt;td&gt;Fargate scheduler&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 Managed Node Group &amp;mdash; In-Place&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 동작은 단순하다. &lt;code&gt;cluster_version&lt;/code&gt;을 1.31로 올린 뒤 &lt;code&gt;terraform apply&lt;/code&gt;를 돌리면, EKS가 자체적으로 새 AMI 노드를 surge로 띄우고 기존 노드를 cordon &amp;rarr; drain &amp;rarr; terminate 한다. 내부 동작은 &lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/APIReference/API_UpdateNodegroupVersion.html&quot;&gt;EKS Managed Node Group Update API&lt;/a&gt;에 정의된 4단계 phase로 진행된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Setup phase&lt;/b&gt; &amp;mdash; 새 Launch Template 버전 생성, ASG 스케일링 정책 갱신&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Scale up phase&lt;/b&gt; &amp;mdash; &lt;code&gt;update_config.max_unavailable&lt;/code&gt;을 고려해 surge 노드를 새 AMI로 띄움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Upgrade phase&lt;/b&gt; &amp;mdash; 기존 노드를 하나씩 cordon &amp;rarr; drain (PDB 평가) &amp;rarr; terminate&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Scale down phase&lt;/b&gt; &amp;mdash; desired capacity를 원래 값으로 복귀&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵은 한 가지 변종을 추가한다. &lt;b&gt;Custom AMI를 사용하는 노드그룹은 &lt;code&gt;ami_id&lt;/code&gt;를 직접 박아두는데, 이 경우 자동 갱신 대상에서 빠진다.&lt;/b&gt; 따라서 사전 준비로 custom AMI를 쓰는 신규 노드그룹(&lt;code&gt;custom-Y&lt;/code&gt;)을 하나 추가해 두고, 컨트롤 플레인 업그레이드와 함께 어떻게 처리되는지 관찰한다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# eks.tf 발췌 &amp;mdash; initial은 자동, custom-Y는 ami_id 명시
eks_managed_node_groups = {
  initial   = { ... }                      # ami_id 미지정 &amp;rarr; 자동
  &quot;custom-Y&quot; = {
    ami_id = data.aws_ami.eks_1_30.id      # 직접 지정
    ami_type = &quot;AL2023_x86_64_STANDARD&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cluster_version&lt;/code&gt;을 1.31로 바꾸고 apply 하면, &lt;code&gt;initial&lt;/code&gt;은 1.31 AMI로 롤링되고 &lt;code&gt;custom-Y&lt;/code&gt;는 1.30 AMI 그대로 남는다. 즉 &lt;b&gt;custom AMI 노드그룹은 사용자가 따로 &lt;code&gt;ami_id&lt;/code&gt;를 1.31용으로 갱신해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233033.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3242&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dedNvn/dJMcafsVbHf/UnAL0fDvkeWzafG4PFRGcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dedNvn/dJMcafsVbHf/UnAL0fDvkeWzafG4PFRGcK/img.png&quot; data-alt=&quot;그림 5 &amp;amp;mdash; Managed In-Place 업그레이드 결과. initial은 1.31, custom-Y는 직접 갱신 필요.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dedNvn/dJMcafsVbHf/UnAL0fDvkeWzafG4PFRGcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdedNvn%2FdJMcafsVbHf%2FUnAL0fDvkeWzafG4PFRGcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3242&quot; height=&quot;998&quot; data-origin-width=&quot;3242&quot; data-origin-height=&quot;998&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 5 &amp;mdash; Managed In-Place 업그레이드 결과. initial은 1.31, custom-Y는 직접 갱신 필요.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; Surge 정책의 단위&lt;br /&gt;Managed Node group의 update_config.max_unavailable_percentage는 동시에 빠질 수 있는 비율이고, surge는 동시에 새로 뜰 수 있는 노드 수다. 둘이 함께 동작하므로 PDB가 빡빡하면 surge를 키우고, 그래도 안 되면 max_unavailable을 더 낮추는 식으로 조정한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Managed Node Group &amp;mdash; Blue/Green&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EBS PV를 쓰는 워크로드처럼 &lt;b&gt;롤링이 안전하지 않은 케이스&lt;/b&gt;는 Blue/Green이 정공법이다. EBS Volume은 단일 노드에만 attach되므로(&lt;code&gt;ReadWriteOnce&lt;/code&gt;), 새 노드가 같은 PV를 attach하려면 기존 노드에서 detach가 먼저 끝나야 한다. In-place 롤링이 빠르게 surge하면 detach 타이밍이 어긋나 파드가 Pending에 갇히기 쉽다. 노드그룹을 통째로 새로 띄우고 워크로드를 명시적으로 옮기면 이 race condition이 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵은 &lt;code&gt;blue-mng&lt;/code&gt;(1.30, EBS PVC를 쓰는 앱이 떠 있음)에서 &lt;code&gt;green-mng&lt;/code&gt;(1.31)로 옮기는 시나리오를 따라간다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# eks.tf &amp;mdash; green-mng 신규 추가
&quot;green-mng&quot; = {
  cluster_version = &quot;1.31&quot;
  taints = { dedicated = { key = &quot;dedicated&quot;, value = &quot;OrdersApp&quot;, effect = &quot;NO_SCHEDULE&quot; } }
  labels = { type = &quot;OrdersMng&quot; }
  subnet_ids = module.vpc.private_subnets
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apply로 &lt;code&gt;green-mng&lt;/code&gt;가 올라오면 다음 순서로 옮긴다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;워크로드의 nodeSelector / toleration을 green 쪽 라벨에 맞춘다 (Argo로 sync).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl drain blue-mng-node&lt;/code&gt; 로 트래픽을 옮긴다 &amp;mdash; PDB가 막아주면 그게 정상이다.&lt;/li&gt;
&lt;li&gt;옮겨진 게 확인되면 &lt;code&gt;blue-mng&lt;/code&gt; 블록을 삭제하고 apply.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233034.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3272&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCbhlm/dJMcafT060a/wlaNHjCLDXFtmgopkwEDAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCbhlm/dJMcafT060a/wlaNHjCLDXFtmgopkwEDAk/img.png&quot; data-alt=&quot;그림 6 &amp;amp;mdash; Blue/Green 마이그레이션 후 상태. blue-mng는 사라지고 green-mng가 1.31로 동작 중.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCbhlm/dJMcafT060a/wlaNHjCLDXFtmgopkwEDAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCbhlm%2FdJMcafT060a%2FwlaNHjCLDXFtmgopkwEDAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3272&quot; height=&quot;1260&quot; data-origin-width=&quot;3272&quot; data-origin-height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 6 &amp;mdash; Blue/Green 마이그레이션 후 상태. blue-mng는 사라지고 green-mng가 1.31로 동작 중.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; EBS PV의 AZ 제약&lt;br /&gt;EBS Volume은 AZ에 묶여 있다. blue 노드가 떠 있던 AZ에 green 노드가 없으면 PVC가 새 노드에 바인딩되지 못해 파드가 Pending에 갇힌다. green 노드그룹의 subnet_ids를 blue와 동일한 AZ 세트로 잡아주는 게 핵심이다.&lt;br /&gt;&lt;br /&gt;사전 예방으로는 EBS storageClass의 volumeBindingMode를 WaitForFirstConsumer로 설정해 PV 생성을 파드 스케줄링 시점까지 미루는 패턴이 일반적이다. 이러면 파드가 스케줄될 노드의 AZ에 맞춰 EBS가 생성되어 AZ 미스매치가 원천 차단된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 Karpenter Nodes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Karpenter는 노드를 직접 만들고 회수하는 컨트롤러이므로, 업그레이드 메커니즘도 컨트롤러에게 맡긴다. ASG/노드그룹이라는 중간 레이어 없이 EC2 API를 직접 호출하는 모델이라, 업그레이드 트리거는 &lt;b&gt;AMI 선택 정책 변경 한 줄&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Karpenter v1의 &lt;code&gt;EC2NodeClass.spec.amiSelectorTerms&lt;/code&gt;는 EC2 AMI 검색 조건을 명시한다. &lt;code&gt;alias&lt;/code&gt; 필드가 가장 직관적인데, &lt;code&gt;al2023@latest&lt;/code&gt;는 &lt;b&gt;AWS가 발행한 최신 AL2023 EKS-optimized AMI를 클러스터의 K8s 버전에 맞춰 자동 선택&lt;/b&gt;한다는 뜻이다. 즉 컨트롤 플레인이 1.31로 올라간 시점에 &lt;code&gt;al2023@latest&lt;/code&gt;로 두면 Karpenter는 자동으로 1.31용 AMI를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# EC2NodeClass &amp;mdash; amiSelectorTerms 변경 전/후
amiSelectorTerms:
  - alias: al2023@v20260301   # 1.30 시점에 핀 고정한 AMI
# 변경 후:
  - alias: al2023@latest       # 컨트롤 플레인 버전(1.31)에 맞춰 자동 선택&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변경 후 새 파드가 들어오면 Karpenter는 &lt;b&gt;신규 NodeClaim을 1.31 AMI로 띄우고&lt;/b&gt;, 기존 1.30 노드는 &lt;a href=&quot;https://karpenter.sh/docs/concepts/disruption/&quot;&gt;disruption controller&lt;/a&gt;가 &lt;code&gt;consolidation&lt;/code&gt; / &lt;code&gt;drift&lt;/code&gt; 정책에 맞춰 점진적으로 정리한다. 특히 &lt;b&gt;drift 감지&lt;/b&gt;는 현재 노드의 AMI가 NodeClass 명세와 다를 때 자동으로 감지되어 노드를 교체 대상으로 올리는 기능으로, 이번 업그레이드의 핵심 트리거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵은 효과를 빠르게 보기 위해 &lt;code&gt;checkout&lt;/code&gt; 디플로이먼트를 10개로 증설한다. 기존 노드 용량으로는 부족하니 Karpenter가 즉시 신규 NodeClaim을 만들어 1.31 AMI로 노드를 띄운다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl scale deploy checkout -n checkout --replicas=10
# 신규 NodeClaim이 떠서 1.31 AMI로 노드가 잡힘
kubectl get nodeclaims
# default-xxxxx   r4.large   spot   us-west-2a   ip-...   True   2m&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233035.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3188&quot; data-origin-height=&quot;1918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vCXOc/dJMcaiQGYg4/0Y9Xs4kgOQ4KPQxG5OsVY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vCXOc/dJMcaiQGYg4/0Y9Xs4kgOQ4KPQxG5OsVY0/img.png&quot; data-alt=&quot;그림 7 &amp;amp;mdash; Karpenter가 새 1.31 AMI 노드를 자동 프로비저닝한 상태.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vCXOc/dJMcaiQGYg4/0Y9Xs4kgOQ4KPQxG5OsVY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvCXOc%2FdJMcaiQGYg4%2F0Y9Xs4kgOQ4KPQxG5OsVY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3188&quot; height=&quot;1918&quot; data-origin-width=&quot;3188&quot; data-origin-height=&quot;1918&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 7 &amp;mdash; Karpenter가 새 1.31 AMI 노드를 자동 프로비저닝한 상태.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; Karpenter는 &quot;당장&quot; 다 갈아엎지 않는다&lt;br /&gt;AMI를 바꿔도 기존 1.30 노드가 즉시 사라지진 않는다. disruption.consolidationPolicy와 expireAfter 설정이 도달해야 정리가 시작된다. 빨리 보고 싶으면 노드를 직접 drain 하거나 NodeClaim을 삭제한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 Self-Managed Node Group&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-Managed Node Group은 사용자가 직접 만든 ASG에 EKS-optimized AMI를 얹어 워커 노드를 운영하는 형태다. EKS는 ASG의 존재를 알지 못하며, 노드 갱신은 사용자가 ASG의 Launch Template과 Instance Refresh 메커니즘으로 직접 처리한다. 즉 &lt;b&gt;AMI ID 한 줄을 갱신하고 Instance Refresh를 트리거하는 게 전부&lt;/b&gt;지만, 그 AMI ID를 어디서 어떻게 가져오느냐가 실무 포인트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS는 EKS-optimized AMI의 최신 ID를 &lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html#ssm-parameter&quot;&gt;SSM Parameter Store&lt;/a&gt;에 K8s 버전 / OS / 아키텍처 / 프로파일별로 발행한다. 이 경로를 그대로 Terraform &lt;code&gt;data&lt;/code&gt; source로 참조하면 매번 최신 AMI를 자동 추적할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 1.31용 최신 AL2023 AMI ID 조회
aws ssm get-parameter \
  --name /aws/service/eks/optimized-ami/1.31/amazon-linux-2023/x86_64/standard/recommended/image_id \
  --region $AWS_REGION --query &quot;Parameter.Value&quot; --output text
# ami-00e0cfd6e5895fe3a&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# base.tf
self_managed_node_groups = {
  self-managed-group = {
    instance_type = &quot;m5.large&quot;
    ami_id        = &quot;ami-00e0cfd6e5895fe3a&quot;  # 1.31
    subnet_ids    = module.vpc.private_subnets
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;terraform apply&lt;/code&gt; 하면 ASG가 Instance Refresh로 들어간다. ASG의 Instance Refresh는 &lt;b&gt;MinHealthyPercentage&lt;/b&gt; 정책에 따라 새 EC2를 먼저 띄워 Health 통과를 확인한 뒤, &lt;b&gt;InstanceWarmup&lt;/b&gt; 시간만큼 기다린 뒤 기존 인스턴스를 종료한다. 이 두 파라미터가 PDB와 함께 무중단 보장의 마지막 안전장치다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../assets/images/IMG-20260503233036.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;i&gt;그림 8 &amp;mdash; ASG Instance Refresh 진행. 새 노드가 먼저 뜬 뒤 기존 노드가 빠진다.&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl get nodes -l node.kubernetes.io/lifecycle=self-managed
# ip-10-0-14-215...   Ready   2m21s   v1.31.14-eks-bbe087e
# ip-10-0-21-221...   Ready   2m36s   v1.31.14-eks-bbe087e&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; Self-Managed는 EKS 콘솔에서 안 보인다&lt;br /&gt;EKS 콘솔의 Node groups 탭에서는 Managed만 노출된다. Self-Managed는 EC2 콘솔의 ASG에서 직접 봐야 한다. Instance Refresh 진행 상태도 ASG &amp;rarr; Activity 탭이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.5 Fargate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fargate는 &lt;a href=&quot;https://firecracker-microvm.github.io/&quot;&gt;Firecracker&lt;/a&gt; 기반의 microVM에 파드 한 개를 격리해서 띄우는 서버리스 컨테이너 런타임이다. 클러스터에 정의된 &lt;code&gt;FargateProfile&lt;/code&gt;의 selector(namespace + labels)에 매칭된 파드는 일반 노드가 아닌 fargate-scheduler가 처리하며, &lt;b&gt;파드 하나당 microVM 하나가 즉석에서 프로비저닝&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업그레이드 관점에서 핵심은 &lt;b&gt;Fargate에는 노드라는 개념이 없다&lt;/b&gt;는 점이다. AMI도 ASG도 Launch Template도 없으며, microVM의 OS&amp;middot;kubelet&amp;middot;containerd는 AWS가 컨트롤 플레인 버전과 함께 자동으로 갱신한다. 사용자가 할 일은 &lt;b&gt;새 fargate 노드(=microVM)를 만들도록 파드를 재생성하는 것뿐&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl rollout restart deployment assets -n assets
kubectl wait --for=condition=Ready pods --all -n assets --timeout=180s
kubectl get node $(kubectl get pods -n assets -o jsonpath='{.items[0].spec.nodeName}')
# fargate-ip-10-0-35-224...   Ready   60s   v1.31.14-eks-f69f56f&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점 &amp;mdash; Fargate는 노드:파드가 1:1&lt;br /&gt;Fargate에서는 파드 하나당 fargate 노드 하나가 뜬다. 즉 디플로이먼트를 재시작하면 노드도 통째로 갈린다. 별도의 AMI/ASG 갱신 절차가 없는 이유다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 모든 노드 1.31 확인&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl get node
# NAME                                                 VERSION
# fargate-ip-10-0-35-224....compute.internal           v1.31.14-eks-f69f56f
# ip-10-0-0-159....compute.internal                    v1.31.14-eks-bbe087e
# ip-10-0-14-215....compute.internal                   v1.31.14-eks-bbe087e
# ip-10-0-2-146....compute.internal                    v1.31.14-eks-bbe087e
# ip-10-0-2-216....compute.internal                    v1.31.14-eks-bbe087e
# ip-10-0-21-221....compute.internal                   v1.31.14-eks-bbe087e
# ip-10-0-4-136....compute.internal                    v1.31.14-eks-bbe087e
# ip-10-0-44-71....compute.internal                    v1.31.14-eks-bbe087e&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계별 절차를 표로 다시 한번 정리해 보면, 동일한 컨트롤 플레인 위에서 노드 타입마다 갱신 주체와 트리거가 명확히 다르다는 점이 한눈에 들어온다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;트리거&lt;/th&gt;
&lt;th&gt;갱신 단위&lt;/th&gt;
&lt;th&gt;사용자 개입&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control Plane&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클러스터&lt;/td&gt;
&lt;td&gt;Terraform 변수 1개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add-ons&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EKS Managed Add-on&lt;/td&gt;
&lt;td&gt;&lt;code&gt;addons.tf&lt;/code&gt; 버전 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed In-Place&lt;/td&gt;
&lt;td&gt;EKS 자동&lt;/td&gt;
&lt;td&gt;노드그룹 단위 롤링&lt;/td&gt;
&lt;td&gt;(Custom AMI는 사용자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Blue/Green&lt;/td&gt;
&lt;td&gt;사용자 마이그레이션&lt;/td&gt;
&lt;td&gt;노드그룹 신규 + 삭제&lt;/td&gt;
&lt;td&gt;nodeSelector / drain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Karpenter&lt;/td&gt;
&lt;td&gt;신규 파드 진입 시&lt;/td&gt;
&lt;td&gt;NodeClaim 단위&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EC2NodeClass&lt;/code&gt; 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-Managed&lt;/td&gt;
&lt;td&gt;ASG Instance Refresh&lt;/td&gt;
&lt;td&gt;EC2 단위&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ami_id&lt;/code&gt; 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fargate&lt;/td&gt;
&lt;td&gt;Deployment rollout&lt;/td&gt;
&lt;td&gt;Pod = 노드&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl rollout restart&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실습 인사이트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 가시화의 가치 &amp;mdash; kube-ops-view + UI 헬스체크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업그레이드는 시간이 길고 동작이 동시에 여러 곳에서 일어난다. 그래서 워크샵 가이드에 없지만 두 가지를 추가로 띄워두는 게 권장된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;kube-ops-view&lt;/b&gt;: 노드 단위로 어느 파드가 어디 떠 있는지를 실시간 시각화. 새 노드가 뜨고 기존 노드가 빠지는 흐름이 한눈에 보인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI 헬스체크 루프&lt;/b&gt;: &lt;code&gt;while true; do curl ...; done&lt;/code&gt; 한 줄. PDB 없는 워크로드(=&lt;code&gt;assets&lt;/code&gt; rollout 등)에서 응답 끊김이 보이는지 즉시 확인 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 IRSA가 끊기지 않는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤 플레인을 한 단계 올렸을 때 OIDC issuer URL이 그대로 유지된다는 것은 운영 관점에서 큰 의미가 있다. &lt;b&gt;IAM Role Trust Policy의 &lt;code&gt;oidc.eks.../id/...&lt;/code&gt; 값을 다시 잡지 않아도 된다는 뜻&lt;/b&gt;이며, 이는 LBC, EBS CSI, Karpenter, ArgoCD 등 IRSA를 쓰는 모든 컴포넌트가 업그레이드 동안 끊기지 않게 만드는 전제다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3 노드 그룹 인벤토리 먼저&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 워크샵의 노드 인벤토리는 Managed 2개(&lt;code&gt;initial&lt;/code&gt;, &lt;code&gt;blue-mng&lt;/code&gt;) + Self-Managed 1개(&lt;code&gt;default-selfmng&lt;/code&gt;) + Karpenter NodePool 1개 + Fargate Profile 1개 + 실습 중 추가되는 &lt;code&gt;custom-Y&lt;/code&gt;/&lt;code&gt;green-mng&lt;/code&gt; 까지 6~7가지 노드 형태가 한 클러스터에 공존한다. &lt;b&gt;업그레이드 전에 어떤 노드그룹이 있는지, 각각이 어떤 워크로드를 들고 있는지 정리해 두지 않으면 어디까지 끝났는지 추적이 어렵다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;kubectl get node -L eks.amazonaws.com/nodegroup,karpenter.sh/nodepool
kubectl get node --label-columns=eks.amazonaws.com/capacityType,node.kubernetes.io/lifecycle&lt;/code&gt;&lt;/pre&gt;</description>
      <category>AWS/EKS</category>
      <category>EKS</category>
      <category>Upgrade</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/110</guid>
      <comments>https://hackjsp.tistory.com/110#entry110comment</comments>
      <pubDate>Mon, 4 May 2026 00:04:45 +0900</pubDate>
    </item>
    <item>
      <title>EKS Multi-tenant SaaS GitOps 워크샵(Flux v2 + Argo Workflows)</title>
      <link>https://hackjsp.tistory.com/109</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 AEWS4 6주차는 감사하게도, &lt;b&gt;Amazon EKS 기반 확장 가능한 멀티 테넌트 SaaS 플랫폼 구축&lt;/b&gt;이라는 AWS 워크샵 환경을 제공해주셔서 워크샵 내용을 정리해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps 기반으로 SaaS 플랫폼을 구축하는 워크샵이며, 기존에는 ArgoCD 도구만 사용해봤던 입장에서 이번에는 FluxCD를 다뤄본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 워크샵 실습 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mmgXN/dJMcagkYPOF/eZCKCX2oHmLJbga4jzVPPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mmgXN/dJMcagkYPOF/eZCKCX2oHmLJbga4jzVPPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mmgXN/dJMcagkYPOF/eZCKCX2oHmLJbga4jzVPPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmmgXN%2FdJMcagkYPOF%2FeZCKCX2oHmLJbga4jzVPPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;1330&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에 사용되는 대표적인 도구들이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;카테고리&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Gitea&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;self-hosted Git&lt;/td&gt;
&lt;td&gt;GitOps의 단일 진실 원천(Single Source of Truth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Flux v2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GitOps 컨트롤러&lt;/td&gt;
&lt;td&gt;Git &amp;rarr; cluster reconciliation. 본 워크샵의 메인 GitOps 엔진&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TF-Controller&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Flux 생태계 IaC 컨트롤러&lt;/td&gt;
&lt;td&gt;Terraform CRD를 K8s reconcile loop에 묶음. AWS 리소스(SQS/DDB/IAM) 프로비저닝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Argo Workflows&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;argoproj.io의 워크플로 엔진&lt;/td&gt;
&lt;td&gt;템플릿 복사 &amp;rarr; 변수 치환 &amp;rarr; Git 커밋 &amp;rarr; 푸시 자동화 (ArgoCD와 &lt;b&gt;별개 도구&lt;/b&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Argo Events&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;argoproj.io의 이벤트 트리거&lt;/td&gt;
&lt;td&gt;SQS 등 외부 이벤트 &amp;rarr; Sensor &amp;rarr; Argo Workflows 트리거&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;1. SQS 메시지 (tenant_id, tier)
   &amp;darr;
2. Argo Events Sensor가 메시지를 감지
   &amp;darr;
3. Argo Workflows가 tier-template 렌더 + Git push
   &amp;darr;
4. Gitea에 commit
   &amp;darr;
5. Flux v2가 GitRepository를 watch &amp;rarr; reconcile
   &amp;darr;
6. helm-tenant-chart가 Workload + Terraform CR을 함께 렌더
   &amp;darr;
7. TF-Controller가 AWS 리소스(SQS/DDB/IAM) 프로비저닝
   &amp;darr;
8. Pod이 SSM에서 endpoint를 읽어 동작 시작&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Flux v2 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux v2는 단일 도구가 아니라 &lt;b&gt;컨트롤러 패밀리&lt;/b&gt;다. 워크샵에서 만나는 주요 리소스 타입은 7개.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;리소스 유형&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GitRepository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flux가 변경 사항을 감시하는 Git 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HelmRepository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Helm 차트가 저장된 저장소 위치(ECR 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HelmChart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;각 소스에서 가져온 Helm 차트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HelmRelease&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 배포 단위. 하나의 차트를 여러 테넌트에 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Kustomization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitRepository를 가리키는 포인터, Kubernetes 구성 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImageRepository&lt;/code&gt; / &lt;code&gt;ImagePolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 컨테이너 이미지 태그 자동 감지 및 정책&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImageUpdateAutomation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;새 이미지 감지 시 Git에 자동 커밋&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz2Veb/dJMcah5h769/4d7HTD5bNZ22fCy5dEI6Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz2Veb/dJMcah5h769/4d7HTD5bNZ22fCy5dEI6Ck/img.png&quot; data-alt=&quot;fluxcd/flux2 공식 GitHub&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz2Veb/dJMcah5h769/4d7HTD5bNZ22fCy5dEI6Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz2Veb%2FdJMcah5h769%2F4d7HTD5bNZ22fCy5dEI6Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1824&quot; height=&quot;1136&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1136&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fluxcd/flux2 공식 GitHub&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux 클러스터 진입점은 &lt;code&gt;gitops-gitea-repo/clusters/production/&lt;/code&gt; 아래 6개의 &lt;code&gt;Kustomization&lt;/code&gt;이고, &lt;b&gt;&lt;code&gt;dependsOn&lt;/code&gt;으로 DAG&lt;/b&gt;를 형성한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;sources &amp;rarr; infrastructure &amp;rarr; dependencies
                       └── control-plane

tenants       (독립 leaf)
pooled-envs   (독립 leaf)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운 지점: Kustomization이라는 이름은 두 가지를 가리킨다&lt;br /&gt;ArgoCD에서 넘어오면 거의 모두 한 번씩 막힌다. 같은 단어가 완전히 다른 두 개념을 가리키기 때문이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;Kustomize (도구)&lt;/th&gt;
&lt;th&gt;Flux Kustomization (CRD)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apiVersion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kustomize.config.k8s.io/v1beta1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kustomize.toolkit.fluxcd.io/v1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일명&lt;/td&gt;
&lt;td&gt;반드시 &lt;code&gt;kustomization.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;자유 (예: &lt;code&gt;infrastructure.yaml&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;&quot;이 디렉토리의 yaml을 한 묶음으로 묶어줘&quot;&lt;/td&gt;
&lt;td&gt;&quot;git의 이 path를 cluster에 reconcile해라&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 6개 진입점은 모두 Flux의 CRD고, 그 진입점이 가리키는 디렉토리 안에 또 들어 있는 &lt;code&gt;kustomization.yaml&lt;/code&gt;은 Kustomize 도구 설정이다. 같은 폴더 트리에 두 종류가 공존하므로, &lt;b&gt;&lt;code&gt;kind&lt;/code&gt;만 보지 말고 &lt;code&gt;apiVersion&lt;/code&gt;을 같이 보는 습관&lt;/b&gt;이 필요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 DAG에서 모든 노드는 &lt;code&gt;postBuild.substituteFrom&lt;/code&gt;으로 &lt;code&gt;saas-infra-outputs&lt;/code&gt; ConfigMap을 참조한다. 즉 yaml에서 보이는 &lt;code&gt;${gitea_url}&lt;/code&gt;, &lt;code&gt;${aws_region}&lt;/code&gt;, &lt;code&gt;${argo_workflows_irsa}&lt;/code&gt; 같은 변수들은 &lt;b&gt;Flux가 apply 시점에 resolve&lt;/b&gt;한다. yaml에 절대 hardcode하지 않는다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 명령어:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flux get kustomizations -A
flux get helmreleases -A
flux get sources git -A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 클러스터에서 &lt;code&gt;flux-system&lt;/code&gt; 네임스페이스를 보면, 위에서 정리한 7개 리소스 타입을 분담하는 컨트롤러들이 한꺼번에 떠 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ kubectl get po -n flux-system
NAME                                           READY   STATUS      RESTARTS      AGE
capacitor-dc778678d-psz8f                      1/1     Running     5 (45m ago)   26h
ecr-credentials-sync-29618365-nhcht            0/1     Completed   0             4m1s
flux-operator-6d6f8cbc94-6fx8q                 1/1     Running     0             26h
helm-controller-b7bbcf854-k4gmn                1/1     Running     0             26h
image-automation-controller-5c5fc5487b-hvg9v   1/1     Running     0             26h
image-reflector-controller-547c8dbffc-pktzf    1/1     Running     0             26h
kustomize-controller-77c78b7f4d-b4jrk          1/1     Running     0             26h
notification-controller-58cfb55954-8sdjl       1/1     Running     0             26h
pool-1-tf-runner                               1/1     Running     0             15s
source-controller-6c64896f47-nw96h             1/1     Running     0             26h
tf-controller-7b8cb5d4-s4tgs                   1/1     Running     0             26h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;source-controller&lt;/code&gt; / &lt;code&gt;kustomize-controller&lt;/code&gt; / &lt;code&gt;helm-controller&lt;/code&gt; / &lt;code&gt;image-reflector-controller&lt;/code&gt; / &lt;code&gt;image-automation-controller&lt;/code&gt; / &lt;code&gt;notification-controller&lt;/code&gt;가 위 표의 리소스 타입을 분담하고, 추가로 &lt;code&gt;tf-controller&lt;/code&gt;(TF-Controller 절에서 별도로 다룬다)와 경량 UI인 &lt;code&gt;capacitor&lt;/code&gt;도 같은 네임스페이스에 함께 있다. &quot;컨트롤러 패밀리&quot;라는 표현이 추상이 아니라 그대로 Pod 목록으로 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;테넌트 한 명을 올리면 &lt;code&gt;example-tenant-tf-runner&lt;/code&gt; Pod이 같은 네임스페이스에 잠깐 추가된다 (빨간 박스). TF-Controller가 모듈을 apply하는 동안만 살고 끝나면 사라진다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;postBuild.substituteFrom&lt;/code&gt;은 일반 Kustomize에는 없는 기능이다. Flux가 후처리(postBuild) 단계에서 ConfigMap이나 Secret 값으로 변수를 치환해주는데, 멀티테넌트 환경에서 리전&amp;middot;IRSA ARN&amp;middot;ECR URL 같은 값을 한 yaml로 관리할 수 있게 해주는 메커니즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux는 7개 리소스 타입을 6개 이상의 독립 컨트롤러가 분담하는 구조다. ArgoCD가 단일 컨트롤러로 동작하는 방식과 대비되며, 컨트롤러를 독립적으로 교체&amp;middot;확장할 수 있는 대신 진입 시점에 학습할 객체 수가 많다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ArgoCD에서 넘어오는 분들을 위한 짧은 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArgoCD를 먼저 배운 사람이라면 *&quot;왜 객체가 이렇게 잘게 쪼개져 있지?&quot;* 가 가장 큰 진입 장벽이다. 핵심 매핑만 정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ArgoCD&lt;/th&gt;
&lt;th&gt;Flux&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Application&lt;/code&gt; (Kustomize source)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Kustomization&lt;/code&gt; CRD&lt;/td&gt;
&lt;td&gt;&quot;git path를 reconcile&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Application&lt;/code&gt; (Helm source)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HelmRelease&lt;/code&gt; CRD&lt;/td&gt;
&lt;td&gt;&quot;Helm chart를 install&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spec.source.repoURL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;GitRepository&lt;/code&gt;&lt;/b&gt; (별도)&lt;/td&gt;
&lt;td&gt;URL+credential을 한 번 정의하고 재사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spec.source.chart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;HelmRepository&lt;/code&gt;&lt;/b&gt; (별도)&lt;/td&gt;
&lt;td&gt;마찬가지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spec.syncPolicy.automated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spec.interval: 1m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flux는 자동 reconcile이 기본&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;App of Apps 패턴&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Kustomization이 다른 Kustomization을 품음&lt;/td&gt;
&lt;td&gt;별도 패턴 없이 자연스럽게 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ApplicationSet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Helm &lt;code&gt;range&lt;/code&gt; 또는 별도 manifest&lt;/td&gt;
&lt;td&gt;Flux에 정확한 등가물은 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;argocd-server&lt;/code&gt; (UI)&lt;/td&gt;
&lt;td&gt;Capacitor / Weave GitOps (옵션)&lt;/td&gt;
&lt;td&gt;UI는 주역이 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 구조적 차이는 &lt;b&gt;&lt;code&gt;spec.source.repoURL&lt;/code&gt; 한 줄이 별도 객체(&lt;code&gt;GitRepository&lt;/code&gt;)로 분리&lt;/b&gt;된다는 점이다. 같은 git을 여러 Kustomization이 참조할 때 credential을 한 번만 정의해 재사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArgoCD의 App of Apps 패턴은 Flux에서는 별도 패턴 없이 Kustomization이 다른 Kustomization을 참조하는 형태로 구현된다. 6개 진입점 자체가 이 구조의 결과물이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Image Automation &amp;mdash; ECR push를 Git에 자동 반영하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps 환경에서 가장 자주 일어나는 변경은 &lt;b&gt;새 컨테이너 이미지의 배포&lt;/b&gt;다. 매번 사람이 yaml의 &lt;code&gt;tag:&lt;/code&gt; 필드를 수정해서 PR을 만드는 건 잦은 릴리스 요구사항과 맞지 않는다. Flux는 이 자동화를 &lt;i&gt;별도 도구 설치 없이&lt;/i&gt; 3개 CRD로 제공한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;ImageRepository  ─&amp;rarr;  ECR을 주기적으로 스캔
       &amp;darr;
ImagePolicy      ─&amp;rarr;  태그 정규식 정책에 맞는 새 태그 선택
       &amp;darr;
ImageUpdateAutomation ─&amp;rarr; 매칭된 새 태그로 Git을 자동 commit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵에서는 microservice 3개(producer / consumer / payments) 각각에 한 세트씩 있다. 핵심 매니페스트는 &lt;code&gt;gitops-gitea-repo/infrastructure/base/sources/producer-image-automation.yaml&lt;/code&gt;(consumer/payments도 동일 패턴).&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flux get image repository -A
flux get image policy -A
flux get image update -A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매니페스트에서 주의가 필요한 부분이 &lt;b&gt;매직 주석&lt;/b&gt;이다. &lt;code&gt;application-plane/production/tenants/&lt;/code&gt; 또는 &lt;code&gt;pooled-envs/pool-1.yaml&lt;/code&gt;을 열면 다음과 같은 줄이 보인다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spec:
  apps:
    producer:
      image: 656796372676.dkr.ecr.us-west-2.amazonaws.com/producer
      tag: prd-20260425T103000Z  # {&quot;$imagepolicy&quot;: &quot;flux-system:producer-image-policy:tag&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;# {&quot;$imagepolicy&quot;: ...}&lt;/code&gt; 부분이 &lt;b&gt;&lt;code&gt;ImageUpdateAutomation&lt;/code&gt;이 다시 쓰는 줄을 식별하는 마커&lt;/b&gt;다. 실수로 지우면 자동화가 멈춘다. 코드 리뷰에서 놓치기 쉽다. 포맷터나 yaml 정렬 도구가 이 줄을 건드리지 않도록 README에 미리 적어두는 게 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECR 태그 컨벤션은 &lt;code&gt;^prd-&amp;lt;UTC-timestamp&amp;gt;$&lt;/code&gt; 정규식이다. 즉 microservice를 빌드할 때 다음과 같이 push하면 자동으로 감지된다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;TAG=&quot;prd-$(date -u +%Y%m%dT%H%M%SZ)&quot;
docker build -t ${ECR_URL}/producer:${TAG} .
docker push ${ECR_URL}/producer:${TAG}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;push 직후 짧으면 1분, 길면 몇 분 안에 &lt;code&gt;gitops-gitea-repo&lt;/code&gt;에 자동 commit이 올라오고, Flux가 reconcile해서 Pod이 rollout된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 ArgoCD에서 같은 자동화를 적용하려면 Argo CD Image Updater라는 별도 프로젝트를 추가로 설치해야 한다. Flux는 이 기능을 &lt;code&gt;image-reflector-controller&lt;/code&gt; / &lt;code&gt;image-automation-controller&lt;/code&gt; 두 컨트롤러로 표준 제공한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. TF-Controller &amp;mdash; Terraform을 Kubernetes reconcile loop에 묶기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 Kubernetes 안의 이야기다. 그런데 SaaS 테넌트 한 명이 들어오면 K8s 리소스만 필요한 게 아니다. &lt;b&gt;테넌트 전용 SQS 큐, DynamoDB 테이블, IRSA 역할, SSM 파라미터까지&lt;/b&gt; 함께 만들어져야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 Terraform을 사람이 따로 돌린다. 워크샵의 접근은 한 단계 더 들어간다. &lt;b&gt;Terraform 자체를 Kubernetes CRD로 만들어서 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Flux의 reconcile loop 안에 넣는다.&lt;/b&gt; 이게 &lt;a href=&quot;https://github.com/flux-iac/tofu-controller&quot;&gt;TF-Controller (구 Tofu Controller)&lt;/a&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;개발자가 Terraform CRD yaml을 Git에 push
        &amp;darr;
Flux가 변경 감지 &amp;rarr; Terraform CR 생성
        &amp;darr;
TF-Controller가 CR을 watch &amp;rarr; TF Runner Pod 실행
        &amp;darr;
TF Runner가 Git에서 Terraform 모듈을 pull &amp;rarr; terraform apply
        &amp;darr;
실행 결과:
  - tfplan &amp;rarr; Kubernetes Secret으로 저장
  - tfstate &amp;rarr; Kubernetes Secret으로 저장
  - outputs &amp;rarr; &amp;lt;tenantId&amp;gt;-infra-output Secret으로 저장
        &amp;darr;
Pod이 SSM에서 endpoint를 읽어 동작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;helm-tenant-chart&lt;/code&gt;는 &lt;b&gt;Workload(&lt;code&gt;templates/deployment.yaml&lt;/code&gt;)와 Terraform CR(&lt;code&gt;templates/terraform.yaml&lt;/code&gt;)을 동일 chart에서 함께 렌더&lt;/b&gt;한다. 테넌트를 한 명 올리면 Flux &amp;rarr; HelmRelease &amp;rarr; (K8s Deployment + Terraform CR) &amp;rarr; TF-Controller &amp;rarr; AWS 리소스 순서로 단일 reconcile 사이클이 완결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테넌트 Terraform CR 예시 (&lt;code&gt;gitops-gitea-repo/application-plane/production/tenants/example-tenant-terraform-crd.yaml&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
  name: example-tenant
  namespace: flux-system
spec:
  path: ./terraform/modules/tenant-apps
  interval: 1m
  approvePlan: auto
  destroyResourcesOnDeletion: true
  sourceRef:
    kind: GitRepository
    name: terraform-v0-0-1   # 모듈은 git tag로 pin
  vars:
    - name: tenant_id
      value: example-tenant
    - name: enable_producer
      value: true
    - name: enable_consumer
      value: true
  writeOutputsToSecret:
    name: example-tenant-infra-output&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get terraforms -A
kubectl get secret -n flux-system | grep -E 'tfplan|tfstate|infra-output'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 &lt;code&gt;example-tenant&lt;/code&gt;을 한 명 올려보면 &lt;code&gt;tf-runner&lt;/code&gt; Pod이 생성되어 모듈을 apply하고, AWS 리소스 11개가 한 번에 만들어진다. 마지막에 outputs로 IRSA 역할 ARN이 떨어지고, 같은 값이 &lt;code&gt;&amp;lt;tenantId&amp;gt;-infra-output&lt;/code&gt; Secret에 기록된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEzePA/dJMcabDZAUN/mB8w4u02QF9eNEQAfeL940/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEzePA/dJMcabDZAUN/mB8w4u02QF9eNEQAfeL940/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEzePA/dJMcabDZAUN/mB8w4u02QF9eNEQAfeL940/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEzePA%2FdJMcabDZAUN%2FmB8w4u02QF9eNEQAfeL940%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1202&quot; height=&quot;404&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ kubectl logs po/example-tenant-tf-runner -n flux-system -f
...
aws_sqs_queue.consumer_sqs[0]: Creation complete after 25s [id=https://sqs.us-west-2.amazonaws.com/.../consumer-example-tenant-08r]
aws_ssm_parameter.dedicated_consumer_sqs[0]: Creation complete after 0s [id=/example-tenant/consumer_sqs]
aws_iam_policy.producer-iampolicy[0]: Creation complete after 1s [id=arn:aws:iam::...:policy/producer-policy-example-tenant]
aws_iam_policy.consumer-iampolicy[0]: Creation complete after 1s [id=arn:aws:iam::...:policy/consumer-policy-example-tenant]

Apply complete! Resources: 11 added, 0 changed, 0 destroyed.

Outputs:
consumer = { &quot;irsa_role&quot; = &quot;arn:aws:iam::...:role/consumer-role-example-tenant&quot; }
producer = { &quot;irsa_role&quot; = &quot;arn:aws:iam::...:role/producer-role-example-tenant&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 AWS 콘솔에서 보면 &lt;code&gt;consumer-example-tenant-gj9&lt;/code&gt; 같은 테넌트 전용 DynamoDB 테이블이 같이 만들어져 있는 게 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHiObU/dJMb990vwOy/m0CXszp6Vi2rci4lzrCZJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHiObU/dJMb990vwOy/m0CXszp6Vi2rci4lzrCZJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHiObU/dJMb990vwOy/m0CXszp6Vi2rci4lzrCZJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHiObU%2FdJMb990vwOy%2Fm0CXszp6Vi2rci4lzrCZJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1660&quot; height=&quot;868&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;i&gt;기본 pool용 &lt;code&gt;consumer-pool-1-2g2&lt;/code&gt;와 새로 생긴 &lt;code&gt;consumer-example-tenant-gj9&lt;/code&gt;가 함께 보인다. yaml commit 한 번으로 K8s Deployment와 AWS DynamoDB가 같은 사이클에 떨어진다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apply가 끝나면 helm-tenant-chart가 같은 사이클에 렌더한 K8s Deployment가 곧바로 떠서 SSM/IRSA로 그 정보를 읽어 동작한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ kubectl get all -n example-tenant
NAME                                           READY   STATUS    RESTARTS   AGE
pod/example-tenant-consumer-7d45cd96f4-jcms7   1/1     Running   0          3m25s
pod/example-tenant-consumer-7d45cd96f4-nl7p9   1/1     Running   0          3m25s
pod/example-tenant-consumer-7d45cd96f4-r4svb   1/1     Running   0          3m25s
pod/example-tenant-producer-648f57fbdb-8ngcf   1/1     Running   0          3m25s
pod/example-tenant-producer-648f57fbdb-d8t2j   1/1     Running   0          3m25s
pod/example-tenant-producer-648f57fbdb-rqtzn   1/1     Running   0          3m25s

deployment.apps/example-tenant-consumer   3/3     3            3           3m25s
deployment.apps/example-tenant-producer   3/3     3            3           3m25s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 tenant yaml을 지우고 commit하면 같은 흐름이 역순으로 돈다. &lt;code&gt;tf-runner&lt;/code&gt;가 다시 떠서 리소스 11개를 차례로 destroy한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Apply complete! Resources: 0 added, 0 changed, 11 destroyed.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름에서 K8s 워크로드와 AWS 인프라는 동일한 GitOps 사이클로 생성&amp;middot;회수된다. 운영 관점에서 두 가지 결과가 도출된다. 첫째, &lt;b&gt;Git이 애플리케이션과 인프라의 단일 진실 원천이 된다.&lt;/b&gt; 별도 &lt;code&gt;terraform apply&lt;/code&gt; 없이 PR 흐름만으로 인프라 변경이 적용된다. 둘째, &lt;b&gt;모듈 버전 관리가 git tag로 일관된다.&lt;/b&gt; &lt;code&gt;terraform-v0-0-1&lt;/code&gt; GitRepository가 tag에 pin되어 있고, 모듈을 bump하려면 새 git tag를 cut한 뒤 &lt;code&gt;infra.tfVersion&lt;/code&gt;을 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArgoCD 생태계에서 같은 역할을 하는 도구로는 Crossplane(자체 provider DSL 사용)과 Atlantis(PR 코멘트 기반 별개 워크플로)가 있다. TF-Controller는 기존 Terraform 모듈을 그대로 재사용하면서 Flux GitOps loop에 통합되는 형태로 설계됐다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Argo Workflows + Argo Events &amp;mdash; 테넌트 온보딩 자동화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 *&quot;yaml이 Git에 들어오면 cluster까지 잘 흘러간다&quot;&lt;i&gt;는 이야기였다면, 남은 질문은 *&lt;/i&gt;&quot;그 yaml을 누가 만드느냐&quot;**다. 사람이 매번 손으로 작성한다면 SaaS 자동 온보딩이라 부르기 어렵다. 이 자리를 채우는 도구가 Argo Workflows + Argo Events다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 비슷해 자주 혼동되지만 &lt;b&gt;Argo Workflows는 ArgoCD가 아니다.&lt;/b&gt; 둘 다 argoproj.io 패밀리에 속하지만 역할이 다르다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;카테고리&lt;/th&gt;
&lt;th&gt;Flux와의 관계&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ArgoCD&lt;/td&gt;
&lt;td&gt;GitOps 컨트롤러&lt;/td&gt;
&lt;td&gt;&lt;b&gt;경쟁&lt;/b&gt; (Flux의 대안)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argo Workflows&lt;/td&gt;
&lt;td&gt;K8s 워크플로 엔진&lt;/td&gt;
&lt;td&gt;&lt;b&gt;보완&lt;/b&gt; (Git에 commit을 &lt;i&gt;만들어&lt;/i&gt; Flux에게 넘김)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argo Events&lt;/td&gt;
&lt;td&gt;이벤트 트리거&lt;/td&gt;
&lt;td&gt;&lt;b&gt;보완&lt;/b&gt; (외부 이벤트 &amp;rarr; Workflows 시작)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argo Rollouts&lt;/td&gt;
&lt;td&gt;점진적 배포&lt;/td&gt;
&lt;td&gt;별개 사용처 (canary/blue-green)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵의 온보딩 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;SQS 메시지: {&quot;tenant_id&quot;:&quot;t01&quot;,&quot;tenant_tier&quot;:&quot;basic&quot;,&quot;release_version&quot;:&quot;0.0.1&quot;}
        &amp;darr;
Argo Events Sensor: SQS 큐 watch &amp;rarr; WorkflowTemplate 트리거
        &amp;darr;
Argo Workflows: tenant-onboarding-template 실행
   1) 00-validate-tenant.sh
   2) 01-tenant-clone-repo.sh
   3) 02-tenant-onboarding.sh   &amp;larr; tier-template 렌더 + Git push
   4) 03-tenant-deployment.sh
        &amp;darr;
gitops-gitea-repo의 application-plane/production/tenants/{tier}/ 에 새 파일 commit
        &amp;darr;
Flux v2가 변경 감지 &amp;rarr; Kustomization reconcile &amp;rarr; HelmRelease 배포
        &amp;darr;
TF-Controller가 AWS 리소스 프로비저닝
        &amp;darr;
테넌트 사용 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵 노트는 이 분업을 한 줄로 짚는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&quot;Argo Workflows의 역할: 실습1에서 수동으로 진행한 템플릿 복사 &amp;rarr; 변수 치환 &amp;rarr; Git 커밋 &amp;rarr; 푸시 과정을 자동화하는 것이 전부임. Git에 변경이 발생하면 그 이후는 여전히 Flux v2가 담당하여 EKS에 리소스를 배포함.&quot;*&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 트리거:&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;# SQS URL을 ConfigMap에서 가져옴
export ONBOARDING_SQS_URL=$(kubectl get configmap saas-infra-outputs \
  -n flux-system \
  -o jsonpath='{.data.argoworkflows_onboarding_queue_url}')

# 메시지 발송
aws sqs send-message \
  --queue-url ${ONBOARDING_SQS_URL} \
  --message-body '{&quot;tenant_id&quot;:&quot;t01&quot;,&quot;tenant_tier&quot;:&quot;basic&quot;,&quot;release_version&quot;:&quot;0.0.1&quot;}'

# 워크플로우 진행 확인
argo list -n argo-workflows
argo logs &amp;lt;workflow-name&amp;gt; -n argo-workflows&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크샵에는 미리 세 종류의 workflow template이 깔려 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;$ kubectl get workflowtemplates -n argo-workflows
NAME                          AGE
tenant-deployment-template    28h
tenant-offboarding-template   28h
tenant-onboarding-template    28h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 deployment / offboarding / onboarding 큐에 대응되는 sensor가 SQS 메시지를 받아 해당 template을 트리거한다. SQS에 한 번 메시지를 넣으면, sensor &amp;rarr; workflow &amp;rarr; git push &amp;rarr; Flux reconcile &amp;rarr; TF-Controller까지 자동으로 이어지면서 새 테넌트가 통째로 올라온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0wT94/dJMcaaLRqdU/rxVBKTTKokXSJPS5C6wIG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0wT94/dJMcaaLRqdU/rxVBKTTKokXSJPS5C6wIG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0wT94/dJMcaaLRqdU/rxVBKTTKokXSJPS5C6wIG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0wT94%2FdJMcaaLRqdU%2FrxVBKTTKokXSJPS5C6wIG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1700&quot; height=&quot;908&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;i&gt;&lt;code&gt;argoworkflows-deployment-queue&lt;/code&gt; / &lt;code&gt;argoworkflows-offboarding-queue&lt;/code&gt; / &lt;code&gt;argoworkflows-onboarding-queue&lt;/code&gt; 세 개가 Argo Events sensor의 진입점이다. 아래 &lt;code&gt;consumer-pool-1-2g2&lt;/code&gt;, &lt;code&gt;consumer-tenant-t1d6c-56j&lt;/code&gt;는 TF-Controller가 만든 테넌트 워크로드용 큐.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tier-template&lt;/code&gt;은 단순한 텍스트 치환이다. &lt;code&gt;{TENANT_ID}&lt;/code&gt;, &lt;code&gt;{RELEASE_VERSION}&lt;/code&gt;, &lt;code&gt;{ENVIRONMENT_ID}&lt;/code&gt; 같은 single-brace placeholder를 onboarding 스크립트가 plain text로 치환한다. &lt;b&gt;Flux의 &lt;code&gt;${var}&lt;/code&gt; 치환과는 다른 메커니즘&lt;/b&gt;임에 주의(같은 yaml에 두 종류가 공존한다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티어별 격리 전략은 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;producer&lt;/th&gt;
&lt;th&gt;consumer&lt;/th&gt;
&lt;th&gt;namespace&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;basic&lt;/td&gt;
&lt;td&gt;pooled (&lt;code&gt;pool-1&lt;/code&gt; 공유)&lt;/td&gt;
&lt;td&gt;pooled (&lt;code&gt;pool-1&lt;/code&gt; 공유)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pool-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;advanced&lt;/td&gt;
&lt;td&gt;pooled (&lt;code&gt;envId: pool-1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;silo (자체 deployment)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;tenantId&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;premium&lt;/td&gt;
&lt;td&gt;silo&lt;/td&gt;
&lt;td&gt;silo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;tenantId&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer가 보내는 HTTP &lt;code&gt;tier&lt;/code&gt; 헤더에 따라 pooled SQS로 라우팅할지 tenant 전용 SQS로 라우팅할지 결정된다. 코드는 &lt;code&gt;microservice-repos/producer/producer.py&lt;/code&gt;의 &lt;code&gt;get_queue_url()&lt;/code&gt;에서 한 줄로 표현된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;def get_queue_url(tenant_id, tier):
    # basic tier uses pool environment parameters, otherwise tenant specific
    env_id = environment if tier == &quot;basic&quot; else tenant_id
    response = ssm_client.get_parameter(Name=f&quot;/{env_id}/consumer_sqs&quot;)
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALB 라우팅 규칙을 보면 같은 분기가 L7 레벨에서도 그대로 드러난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2936&quot; data-origin-height=&quot;1152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5H2ft/dJMb99THmeV/KWUAVoZQvMSCydsRDRcZDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5H2ft/dJMb99THmeV/KWUAVoZQvMSCydsRDRcZDK/img.png&quot; data-alt=&quot;ALB 리스너 규칙 &amp;amp;mdash; HTTP 헤더 TenantID로 pool-1 vs tenant1-1 분기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5H2ft/dJMb99THmeV/KWUAVoZQvMSCydsRDRcZDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5H2ft%2FdJMb99THmeV%2FKWUAVoZQvMSCydsRDRcZDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2936&quot; height=&quot;1152&quot; data-origin-width=&quot;2936&quot; data-origin-height=&quot;1152&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ALB 리스너 규칙 &amp;mdash; HTTP 헤더 TenantID로 pool-1 vs tenant1-1 분기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;i&gt;&lt;code&gt;HTTP 헤더 TenantID&lt;/code&gt;가 &lt;code&gt;tenant1-1&lt;/code&gt;이면 silo 대상 그룹(&lt;code&gt;k8s-tenantt1-tenantt1-1-...&lt;/code&gt;)으로, 그 외에는 pooled 대상 그룹(&lt;code&gt;k8s-pool1-tenantt1-...&lt;/code&gt;)으로 라우팅된다. 코드(producer.py)와 인프라(ALB 규칙) 양쪽에서 동일한 분기 규칙이 적용된다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 Argo Workflows는 cluster에 직접 &lt;code&gt;kubectl apply&lt;/code&gt;하지 않고 Git에 commit하는 방식으로 동작한다. 모든 변경이 Git을 거치므로 GitOps 4원칙(&lt;b&gt;선언적&amp;middot;버전관리&amp;middot;자동반영&amp;middot;지속조정&lt;/b&gt;)이 유지된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 운영 시 고려사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조합을 도입할 때 고려해야 할 운영 요소는 다음 네 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Flux의 통합 UI는 ArgoCD에 비해 제한적이다.&lt;/b&gt; Flux 진영의 &lt;a href=&quot;https://github.com/gimlet-io/capacitor&quot;&gt;Capacitor&lt;/a&gt;나 Weave GitOps 같은 경량 UI는 클러스터 단위 healthy 여부를 한 화면으로 보여주는 수준이다. ArgoCD UI의 Application &amp;rarr; Deployment &amp;rarr; Pod 트리 토폴로지 형태의 워크로드 시각화는 제공되지 않는다. 실전 운영에서는 &lt;code&gt;flux&lt;/code&gt; CLI + Slack 알림 + Grafana 조합이 일반적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. TF-Controller는 추가 운영 부담을 동반한다.&lt;/b&gt; Terraform state가 Kubernetes Secret에 저장되므로 etcd 백업&amp;middot;복구 정책이 state DR 정책에 포함된다. AWS credential은 controller IRSA를 통해 전달되며, 권한 분리 설계가 더 복잡해진다. TF-Controller는 &lt;b&gt;CNCF Sandbox 단계의 비교적 신생 프로젝트&lt;/b&gt;이므로 프로덕션 채택 전 검증이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Argo Workflows + Argo Events는 컨트롤러 수를 늘린다.&lt;/b&gt; Flux + TF-Controller에 추가되어 트러블슈팅 동선이 길어진다. Pod이 뜨지 않을 때 SQS &amp;rarr; Sensor &amp;rarr; Workflow &amp;rarr; Git &amp;rarr; Flux &amp;rarr; Helm &amp;rarr; K8s 단계 중 어느 지점에서 멈췄는지 추적이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Image Automation은 &quot;암묵적 컨벤션&quot;에 의존한다.&lt;/b&gt; &lt;code&gt;# {&quot;$imagepolicy&quot;: ...}&lt;/code&gt; 마커는 yaml만 봐서는 의미를 알 수 없다. 코드 리뷰에서 삭제되거나 정렬 도구가 다시 쓰면 자동화가 멈춘다. README나 PR 템플릿에 보호 규칙을 명시하는 운영 패턴이 권장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구별 역할을 표로 정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;빠지면 어떻게 되나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Gitea&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Git의 단일 진실 원천&lt;/td&gt;
&lt;td&gt;GitHub/GitLab으로 대체 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Flux v2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;reconciliation 본체&lt;/td&gt;
&lt;td&gt;ArgoCD로 대체 시 Image Automation&amp;middot;TF-Controller와의 결합이 흔들림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Image Automation&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;이미지 push &amp;rarr; Git commit 자동화&lt;/td&gt;
&lt;td&gt;매번 사람이 yaml의 tag 수정. &lt;i&gt;잦은 릴리스&lt;/i&gt; 요구 깨짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TF-Controller&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Terraform CRD 기반 IaC&lt;/td&gt;
&lt;td&gt;사람이 따로 terraform apply. &lt;i&gt;자동화된 온보딩&lt;/i&gt; 깨짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Argo Events&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SQS &amp;rarr; K8s 이벤트 변환&lt;/td&gt;
&lt;td&gt;API Gateway나 Lambda로 우회 가능하지만 K8s 네이티브 트리거가 사라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Argo Workflows&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;템플릿 렌더 + Git push 자동화&lt;/td&gt;
&lt;td&gt;사람이 매번 yaml 작성&amp;middot;commit. &lt;i&gt;운영 효율성&lt;/i&gt; 깨짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;용어 주의: argoproj.io &amp;ne; ArgoCD.&lt;/b&gt; Argo Workflows / Argo Events / Argo Rollouts는 모두 argoproj.io 패밀리지만 ArgoCD와는 별개 프로젝트다. 이름 유사성 때문에 묶어서 인식하기 쉬우나, 본 워크샵에서 Argo Workflows는 Flux와 &lt;b&gt;보완 관계&lt;/b&gt;(Git에 commit을 만들어 Flux에게 전달)로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kustomize의 동작 특성: 누락 파일 참조 무시.&lt;/b&gt; &lt;code&gt;resources&lt;/code&gt; 항목이 가리키는 파일이 실제로 존재하지 않으면 Kustomize는 해당 항목을 무시하고 진행한다. 즉 GitOps 흐름에서 매니페스트 파일을 삭제할 때 같은 디렉토리의 &lt;code&gt;kustomization.yaml&lt;/code&gt; 참조를 동시에 정리하지 않더라도 reconcile은 실패하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PULDi/dJMcadV4giY/rGx47UtLK6uuSKkW5Yi1wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PULDi/dJMcadV4giY/rGx47UtLK6uuSKkW5Yi1wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PULDi/dJMcadV4giY/rGx47UtLK6uuSKkW5Yi1wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPULDi%2FdJMcadV4giY%2FrGx47UtLK6uuSKkW5Yi1wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1436&quot; height=&quot;746&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;i&gt;7번 줄 &lt;code&gt;- example-tenant-terraform-crd.yaml&lt;/code&gt;이 가리키는 실제 파일은 직전 commit에서 이미 삭제된 상태이지만 reconcile은 정상 동작한다.&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps의 기본 정의는 *&quot;yaml을 Git에 commit하면 cluster에 반영된다&quot;* 한 줄이다. 멀티테넌트 SaaS 환경에서 이를 자동화하려면 다음 다섯 영역이 함께 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Workload reconciliation&lt;/b&gt;: Flux v2&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 갱신 자동화&lt;/b&gt;: Image Automation (image-reflector / image-automation controller)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인프라 프로비저닝&lt;/b&gt;: TF-Controller (Terraform CRD)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;온보딩 워크플로&lt;/b&gt;: Argo Workflows&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 이벤트 트리거&lt;/b&gt;: Argo Events (SQS sensor)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 워크샵은 위 다섯 컨트롤러를 Flux 생태계 중심으로 통합하여 하나의 reconcile loop로 묶은 레퍼런스 구현이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FluxCD를 다뤄본 건 이번이 처음이었는데, ArgoCD와는 결이 다른 매력이 있었다. 특히 다음 두 가지 흐름이 인상적이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TF-Controller&lt;/b&gt; &amp;mdash; Terraform 자체를 Kubernetes CRD로 묶어 K8s 워크로드와 AWS 인프라(SQS/DDB/IAM)를 동일한 GitOps 사이클에 올려놓는 구조. ArgoCD 생태계에서는 Crossplane(별도 DSL)이나 Atlantis(PR 코멘트 워크플로)로 우회해야 하는 영역을 기존 Terraform 모듈을 거의 그대로 재사용해 풀 수 있었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Image Automation&lt;/b&gt; &amp;mdash; ECR push만으로 Git commit과 reconcile이 자동으로 이어지는 흐름. ArgoCD에서는 Argo CD Image Updater라는 별도 프로젝트를 추가 설치해야 하는 기능이 Flux에서는 표준 컨트롤러 패밀리(&lt;code&gt;image-reflector&lt;/code&gt; / &lt;code&gt;image-automation&lt;/code&gt;)로 들어 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AWS/EKS</category>
      <category>argo workflow</category>
      <category>flux2</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/109</guid>
      <comments>https://hackjsp.tistory.com/109#entry109comment</comments>
      <pubDate>Mon, 27 Apr 2026 00:11:03 +0900</pubDate>
    </item>
    <item>
      <title>[AEWS4] EKS IRSA 트러블슈팅 가이드</title>
      <link>https://hackjsp.tistory.com/108</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 같은 에러, 다른 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA는 한 번 잘 세팅해 두면 거의 신경 쓸 일이 없다. 문제는 &quot;한 번 잘 세팅해 두면&quot;이라는 전제다. 클러스터를 새로 만들거나 ServiceAccount를 옮기거나 IAM Role을 재생성하는 순간 IRSA는 조용히 깨진다. 겉으로 Pod는 정상 기동하고 readiness probe도 통과한다. 그런데 AWS API 호출만 실패한다. 로그에는 다음과 같은 메시지가 반복된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity
operation: Not authorized to perform sts:AssumeRoleWithWebIdentity&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지만 보면 모든 사례가 같아 보인다. 하지만 실제 원인은 IRSA의 5단계 동작 과정 중 어느 단계에서 깨졌느냐에 따라 6가지로 갈라진다. 원인에 따라 진단 명령과 조치가 달라지므로 &quot;어느 단계에서 깨졌는지&quot;를 먼저 식별해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 대표적인 장애 트리거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://repost.aws/knowledge-center/eks-troubleshoot-irsa-errors&quot;&gt;AWS re:Post IRSA 트러블슈팅 KB&lt;/a&gt; 시리즈와 주요 오픈소스 프로젝트(AWS Load Balancer Controller, Karpenter, Grafana Loki 등)의 공개 이슈 아카이브를 교차 분석해 보면 IRSA가 깨지는 상황은 대체로 아래와 같이 정리된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ServiceAccount를 다른 namespace로 이동&lt;/li&gt;
&lt;li&gt;SA 이름 변경(Helm chart 업그레이드로 인한 기본값 변화 포함)&lt;/li&gt;
&lt;li&gt;클러스터 재구축 후 OIDC Provider 재등록 누락&lt;/li&gt;
&lt;li&gt;애플리케이션 이미지에 포함된 AWS SDK 버전이 IRSA 미지원&lt;/li&gt;
&lt;li&gt;Terraform 리팩토링 과정의 Trust Policy 변수 변경&lt;/li&gt;
&lt;li&gt;OIDC Provider 생성 시 ClientID 지정 누락&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통점은 &quot;원래 되던 것을 건드렸다&quot;는 점이다. 설정을 새로 만들 때보다 고칠 때 더 자주 깨진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 에러 메시지로 받는 첫 신호&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애를 가장 먼저 감지하는 곳은 애플리케이션 로그다. Pod 로그에서 전형적으로 관측되는 메시지는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;kubectl logs -n kube-system external-dns-xxxxx&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;time=&quot;2026-04-19T10:12:34Z&quot; level=error msg=&quot;records retrieval failed:
An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity
operation: Not authorized to perform sts:AssumeRoleWithWebIdentity&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 앞부분이 동일하다 보니 원인도 하나일 거라 착각하기 쉽지만, &lt;code&gt;Not authorized&lt;/code&gt;, &lt;code&gt;Incorrect token audience&lt;/code&gt;, &lt;code&gt;No OpenIDConnect provider found&lt;/code&gt;, &lt;code&gt;Unable to locate credentials&lt;/code&gt; 같은 세부 메시지는 각각 IRSA의 다른 단계에서 나오는 신호다. 본문 2장에서 IRSA 5단계를 정리하고, 3장에서 각 에러를 그 단계 위에 매핑한다. 에러 메시지만 보고도 어느 단계가 무너졌는지 짚을 수 있게 되는 것이 이 가이드의 목표다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. IRSA 5단계 다시 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4주차 글에서 IRSA 정상 동작 흐름을 다뤘다. 장애 대응 관점에서는 이 흐름을 &quot;깨질 수 있는 단계&quot;로 다시 볼 필요가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 5단계 흐름 정리&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;주체&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;깨졌을 때 증상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;① JWT 주입&lt;/td&gt;
&lt;td&gt;MutatingWebhook&lt;/td&gt;
&lt;td&gt;Pod Spec에 env 2개와 projected volume 추가&lt;/td&gt;
&lt;td&gt;Pod에 &lt;code&gt;AWS_WEB_IDENTITY_TOKEN_FILE&lt;/code&gt;이 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② SDK 자격증명&lt;/td&gt;
&lt;td&gt;애플리케이션 SDK&lt;/td&gt;
&lt;td&gt;env 두 개를 읽고 STS 호출 준비&lt;/td&gt;
&lt;td&gt;SDK가 web identity 경로를 모름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ STS 호출&lt;/td&gt;
&lt;td&gt;AWS SDK &amp;rarr; AWS STS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; API 호출&lt;/td&gt;
&lt;td&gt;STS 네트워크 도달 실패 또는 요청 구조 오류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;④ OIDC 검증&lt;/td&gt;
&lt;td&gt;AWS STS + IAM&lt;/td&gt;
&lt;td&gt;JWKS로 토큰 서명 검증&lt;/td&gt;
&lt;td&gt;OIDC Provider가 없거나 thumbprint 불일치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⑤ Trust Policy 매칭&lt;/td&gt;
&lt;td&gt;AWS IAM&lt;/td&gt;
&lt;td&gt;토큰 claim과 Trust Policy 조건 비교&lt;/td&gt;
&lt;td&gt;sub&amp;middot;aud 조건 불일치로 AccessDenied&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 단계별로 무엇이 검증되는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 ①에서는 Pod가 생성되는 순간 클러스터 내부에서 &lt;code&gt;pod-identity-webhook&lt;/code&gt;이 MutatingAdmissionWebhook으로 동작한다. IRSA가 설정된 SA를 쓰는 Pod에 한해 Pod Spec을 변형하고, 주입되는 내용은 env 두 개와 projected volume 하나다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;env:
- name: AWS_ROLE_ARN
  value: arn:aws:iam::123456789012:role/my-app-role
- name: AWS_WEB_IDENTITY_TOKEN_FILE
  value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
volumeMounts:
- mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
  name: aws-iam-token
  readOnly: true
volumes:
- name: aws-iam-token
  projected:
    sources:
    - serviceAccountToken:
        audience: sts.amazonaws.com
        expirationSeconds: 86400
        path: token&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;env 두 개가 주입되지 않으면 뒤의 모든 단계가 의미 없어진다. 그래서 디버깅은 Pod Spec 확인에서 시작하는 것이 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 ②는 SDK의 몫이다. AWS SDK는 Credential Provider Chain을 순회하면서 어떤 자격증명 방식을 쓸지 결정한다. IRSA가 쓰는 방식은 &quot;Web Identity Token&quot; 프로바이더다. 이 프로바이더를 지원하지 않는 구버전 SDK에서는 IRSA env가 존재해도 무시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 ③은 SDK가 STS에 API 호출을 하는 지점이다. 이 단계의 실패는 대부분 네트워크 문제(Proxy, VPC Endpoint 설정 누락)나 STS 리전 설정 오류다. IRSA 자체 설정과는 층이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 ④에서는 STS가 IAM에 &quot;이 토큰이 진짜인지&quot;를 묻는다. IAM은 클러스터의 OIDC Discovery endpoint에서 JWKS를 가져와 토큰 서명을 검증한다. OIDC Provider가 등록되어 있지 않으면 이 단계에서 바로 막힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 ⑤는 서명 검증을 통과한 토큰의 claim과 IAM Role의 Trust Policy 조건을 비교하는 단계다. &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt; 같은 필드가 Trust Policy와 한 글자라도 다르면 &lt;code&gt;AccessDenied&lt;/code&gt;가 반환된다. 공식 KB와 이슈 아카이브에서 가장 자주 인용되는 단계다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 깨질 수 있는 6개 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개 패턴을 5단계에 매핑하면 아래와 같다. 빈도 열은 AWS re:Post KB의 언급 순서와 참조한 GitHub 이슈 아카이브 건수를 토대로 매긴 상대 지표다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패턴&lt;/th&gt;
&lt;th&gt;깨진 단계&lt;/th&gt;
&lt;th&gt;참고 자료 빈도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SA annotation 누락&amp;middot;오타&lt;/td&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;자주&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MutatingWebhook 설정 손상&lt;/td&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;드묾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS SDK 버전 미달&lt;/td&gt;
&lt;td&gt;②&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OIDC Provider 미등록&lt;/td&gt;
&lt;td&gt;④&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust Policy sub 불일치&lt;/td&gt;
&lt;td&gt;⑤&lt;/td&gt;
&lt;td&gt;매우 자주&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust Policy aud 누락&lt;/td&gt;
&lt;td&gt;⑤&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 패턴을 증상&amp;middot;진단&amp;middot;조치 순서로 정리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 SA annotation 누락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA가 작동하려면 ServiceAccount에 IAM Role ARN을 annotation으로 알려줘야 한다. 이 annotation이 바로 MutatingWebhook이 참조하는 값이다. Helm chart를 새로 붙이거나 values에서 annotation 필드를 비운 채 배포하는 경우 가장 자주 발생하는 실수 유형이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/external-dns-irsa&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod에 &lt;code&gt;AWS_ROLE_ARN&lt;/code&gt; env 자체가 들어가지 않는다. SDK는 web identity 방식을 시도조차 하지 않고 다른 자격증명 프로바이더(IMDS 등)로 fallback한다. 결과적으로 노드 IAM Role을 쓰게 되거나 NoCredentialsError가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 로그에서 자주 보는 메시지는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;botocore.exceptions.NoCredentialsError: Unable to locate credentials&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 AWS Load Balancer Controller 같은 도구에서는 다음과 같이 나온다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;failed to retrieve credentials: NoCredentialProviders: no valid providers in chain&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ServiceAccount에 annotation이 붙어 있는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;kubectl describe sa external-dns -n kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에 다음 줄이 없으면 annotation이 빠진 것이다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;Annotations:  eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/external-dns-irsa&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 Pod Spec에 env가 주입되었는지 본다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;kubectl get pod external-dns-xxxxx -n kube-system -o yaml | grep -A2 AWS_ROLE_ARN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;annotation은 있는데 Pod env에 반영되지 않는다면 Pod를 다시 만들어야 한다. MutatingWebhook은 Pod 생성 시점에만 동작하므로 기존 Pod는 annotation 변경의 영향을 받지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;annotation을 추가하고 Pod를 재생성한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;kubectl annotate sa external-dns -n kube-system \
  eks.amazonaws.com/role-arn=arn:aws:iam::123456789012:role/external-dns-irsa \
  --overwrite

kubectl rollout restart deployment external-dns -n kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deployment가 아닌 수동 Pod라면 삭제하고 다시 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조치 후 확인은 Pod env로 한다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;kubectl exec -n kube-system external-dns-xxxxx -- env | grep AWS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 두 줄이 보이면 정상이다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;AWS_ROLE_ARN=arn:aws:iam::123456789012:role/external-dns-irsa
AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 MutatingWebhook 설정 손상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.1이 &quot;특정 SA의 annotation이 빠진 경우&quot;라면 이번 패턴은 &quot;클러스터 전체에서 Webhook이 동작하지 않는 경우&quot;다. 발생 빈도는 낮지만 터지면 모든 IRSA Pod가 동시에 깨진다. &lt;code&gt;pod-identity-webhook&lt;/code&gt;의 MutatingWebhookConfiguration이 삭제되거나 &lt;code&gt;failurePolicy&lt;/code&gt;가 &lt;code&gt;Ignore&lt;/code&gt;로 변경된 상태에서 컨트롤 플레인이 불안정해지면 JWT&amp;middot;env 주입이 스킵된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;annotation이 제대로 붙은 SA인데도 Pod env에 &lt;code&gt;AWS_ROLE_ARN&lt;/code&gt;이 주입되지 않는다. 이 상태에서 Pod를 다시 만들어도 상황이 바뀌지 않는다. 클러스터 관리자가 실수로 MutatingWebhookConfiguration을 삭제했거나 EKS 컨트롤 플레인 장애가 있었던 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook 설정이 남아 있는지 먼저 확인한다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;kubectl get mutatingwebhookconfiguration pod-identity-webhook -o yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에 &lt;code&gt;rules&lt;/code&gt;와 &lt;code&gt;clientConfig&lt;/code&gt;가 정상적으로 있어야 한다. 특히 &lt;code&gt;failurePolicy&lt;/code&gt;가 &lt;code&gt;Ignore&lt;/code&gt;로 바뀌어 있다면 이전 복구 시도에서 누군가 Webhook 오류를 피하려고 바꿨을 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pod-identity-webhook&lt;/code&gt;은 EKS 컨트롤 플레인이 자동 배포&amp;middot;관리하는 구성요소다. &lt;code&gt;vpc-cni&lt;/code&gt;나 &lt;code&gt;coredns&lt;/code&gt; 같은 EKS Managed Add-on과 달리 애드온 관리 API(&lt;code&gt;aws eks describe-addon&lt;/code&gt; 등)로는 제어할 수 없다. 설정이 손상된 경우 가장 안전한 조치는 EKS 컨트롤 플레인 업데이트를 재시도하거나 AWS 지원에 문의하는 것이다. 임의로 재생성하면 Webhook 인증서가 꼬일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 AWS SDK 버전 미달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA는 SDK의 Web Identity Token Credential Provider가 지원되는 버전에서만 작동한다. 오래된 SDK는 &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; API 자체를 모른다. 베이스 이미지가 오래된 배치 잡이나 레거시 언어 런타임을 쓰는 워크로드에서 특히 자주 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod Spec에 env가 잘 주입되어 있고 annotation도 정상인데 SDK가 자격증명을 못 찾는다. 구버전 SDK는 Web Identity Token 프로바이더를 Credential Provider Chain에 포함하지 않기 때문에, env를 무시하고 IMDS(EC2 메타데이터) 경로로 fallback한다. 결과적으로 노드 IAM Role 권한으로 API를 호출하게 되어, 노드 Role에 해당 권한이 없으면 아래와 같은 에러가 찍힌다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;NoCredentialProviders: no valid providers in chain&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 파이썬 boto3 같은 SDK에서는 &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; 경로를 건너뛴 채 다음과 같이 나온다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;botocore.exceptions.NoCredentialsError: Unable to locate credentials&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 이 두 메시지는 3.1(SA annotation 누락)과 겹친다는 것이다. 이 경우 Pod env는 정상 주입되어 있다는 점이 구분 포인트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 이미지에 포함된 SDK 버전을 확인한다. Distroless나 slim 이미지에는 &lt;code&gt;pip&lt;/code&gt; 자체가 없을 수 있으므로, 언어 런타임으로 직접 버전을 출력하는 방식이 안전하다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;kubectl exec -n app my-app-xxxxx -- python -c &quot;import boto3; print(boto3.__version__)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 애플리케이션이라면 go.mod를, Java라면 패키지 메타데이터(&lt;code&gt;mvn dependency:tree&lt;/code&gt; 등)를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최소 지원 버전 표&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-minimum-sdk.html&quot;&gt;AWS 공식 문서&lt;/a&gt;에 명시된 최소 버전은 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;언어&lt;/th&gt;
&lt;th&gt;최소 버전&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java v1&lt;/td&gt;
&lt;td&gt;1.12.782&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java v2&lt;/td&gt;
&lt;td&gt;2.10.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go v1&lt;/td&gt;
&lt;td&gt;1.23.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go v2&lt;/td&gt;
&lt;td&gt;전체 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python (boto3)&lt;/td&gt;
&lt;td&gt;1.9.220&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python (botocore)&lt;/td&gt;
&lt;td&gt;1.12.200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET v3&lt;/td&gt;
&lt;td&gt;3.3.659.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript v2 (Node)&lt;/td&gt;
&lt;td&gt;2.525.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript v3 (Node)&lt;/td&gt;
&lt;td&gt;3.27.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;3.58.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP v3&lt;/td&gt;
&lt;td&gt;3.110.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;1.7.174&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS CLI&lt;/td&gt;
&lt;td&gt;1.16.232&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SDK를 위 표 이상으로 업그레이드한다. 기본적으로 최신 LTS 버전을 쓰는 게 안전하다. 불가피하게 구버전을 써야 한다면 IRSA 대신 Pod Identity나 Secret에 정적 키를 주입하는 우회 방법을 써야 한다. Pod Identity 내용은 4주차 글에서 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SDK 업그레이드가 어려운 상황이라면 &lt;code&gt;STS_REGIONAL_ENDPOINTS=regional&lt;/code&gt; 환경변수를 추가로 설정해 글로벌 엔드포인트 이슈를 피할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;env:
- name: AWS_STS_REGIONAL_ENDPOINTS
  value: regional&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 OIDC Provider 미등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA는 클러스터의 OIDC Discovery endpoint가 AWS IAM에 &quot;OIDC Provider&quot;로 등록되어 있어야 작동한다. 클러스터를 새로 만들고 이 과정을 잊으면 모든 IRSA 요청이 실패한다. 프로덕션 클러스터를 재구축할 일은 드물어서 자주 보이지는 않지만, 테스트 환경을 반복 생성하거나 클러스터 구축과 IRSA 설정 주체가 다른 상황에서 자주 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC Provider가 등록되지 않아 IAM이 토큰 서명을 검증할 공개키를 가져올 경로를 모르는 상태다. SDK 로그에는 다음과 같은 메시지가 찍힌다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;InvalidIdentityTokenException: No OpenIDConnect provider found in your account
for https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계정에 등록된 OIDC Provider 목록을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;aws iam list-open-id-connect-providers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터의 OIDC issuer URL이 이 목록에 없다면 등록이 빠진 것이다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;CLUSTER_NAME=myeks
aws eks describe-cluster --name $CLUSTER_NAME \
  --query cluster.identity.oidc.issuer --output text&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;eksctl&lt;/code&gt;을 쓰면 한 줄로 끝난다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;eksctl utils associate-iam-oidc-provider \
  --cluster $CLUSTER_NAME --approve&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform을 쓰는 환경이라면 &lt;code&gt;aws_iam_openid_connect_provider&lt;/code&gt; 리소스를 모듈에 포함시키는 게 안전하다. 이 부분은 6장에서 다시 다룬다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.5 Trust Policy sub 불일치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6가지 원인 중 공식 KB와 이슈 아카이브에서 가장 자주 인용되는 패턴이다. &lt;a href=&quot;https://repost.aws/knowledge-center/eks-troubleshoot-irsa-errors&quot;&gt;AWS re:Post IRSA 트러블슈팅 KB&lt;/a&gt;의 첫 번째 체크 항목이자, &lt;a href=&quot;https://github.com/kubernetes-sigs/aws-load-balancer-controller/issues/1935&quot;&gt;kubernetes-sigs/aws-load-balancer-controller#1935&lt;/a&gt;, &lt;a href=&quot;https://github.com/aws/karpenter/issues/1666&quot;&gt;aws/karpenter#1666&lt;/a&gt;, &lt;a href=&quot;https://github.com/grafana/loki/issues/20429&quot;&gt;grafana/loki#20429&lt;/a&gt; 같은 주요 오픈소스 이슈 아카이브에서도 반복적으로 등장한다. IAM Role의 Trust Policy 안에 있는 &lt;code&gt;sub&lt;/code&gt; 조건이 Pod가 제시하는 JWT의 &lt;code&gt;sub&lt;/code&gt; claim과 한 글자라도 다르면 IAM은 즉시 &lt;code&gt;AccessDenied&lt;/code&gt;를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trust Policy는 다음과 같은 구조를 가진다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Federated&quot;: &quot;arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234:sub&quot;: &quot;system:serviceaccount:kube-system:external-dns&quot;,
          &quot;oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234:aud&quot;: &quot;sts.amazonaws.com&quot;
        }
      }
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;sub&lt;/code&gt; 값의 형식은 &lt;code&gt;system:serviceaccount:&amp;lt;namespace&amp;gt;:&amp;lt;serviceaccount-name&amp;gt;&lt;/code&gt;이다. 세 부분 중 어느 하나라도 실제 Pod의 SA와 다르면 일치 실패다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation:
Not authorized to perform sts:AssumeRoleWithWebIdentity&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지 동작하던 Pod에서 갑자기 발생하는 경우가 많으며, 직전의 설정 변경 이력을 먼저 확인하는 것이 효과적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세부 패턴&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;namespace 변경: SA를 &lt;code&gt;kube-system&lt;/code&gt;에서 &lt;code&gt;kube-addons&lt;/code&gt;로 옮겼는데 Trust Policy는 &lt;code&gt;kube-system&lt;/code&gt; 그대로&lt;/li&gt;
&lt;li&gt;SA 이름 rename: &lt;code&gt;external-dns&lt;/code&gt; &amp;rarr; &lt;code&gt;externaldns&lt;/code&gt;로 바꿨는데 Trust Policy 미반영&lt;/li&gt;
&lt;li&gt;Helm chart 업그레이드 후 &lt;code&gt;serviceAccount.name&lt;/code&gt; 기본값이 바뀐 경우&lt;/li&gt;
&lt;li&gt;Terraform 모듈 리팩토링 도중 변수 값이 한 글자 변경된 경우&lt;/li&gt;
&lt;li&gt;형식 오타: &lt;code&gt;system:serviceaccount&lt;/code&gt;를 &lt;code&gt;system:serviceaccounts&lt;/code&gt;로 적은 경우, 하이픈 위치가 어긋난 경우(&lt;code&gt;external-dns&lt;/code&gt; vs &lt;code&gt;external_dns&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단 1: JWT claim 직접 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod 안에서 실제로 발급된 토큰의 &lt;code&gt;sub&lt;/code&gt; 값을 본다. JWT payload는 base64url 인코딩 방식이고 padding이 생략되어 있어서, 일부 환경의 &lt;code&gt;base64 -d&lt;/code&gt;가 바로 실패할 수 있다. 아래처럼 padding을 보정하는 방식이 안전하다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;kubectl exec -n kube-system external-dns-xxxxx -- \
  cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token | \
  awk -F. '{
    p=$2
    while (length(p) % 4 != 0) p=p&quot;=&quot;
    print p
  }' | base64 -d | jq .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 &lt;code&gt;jwt&lt;/code&gt; CLI가 설치되어 있다면 더 간단하게 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;kubectl exec -n kube-system external-dns-xxxxx -- \
  cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token | jwt decode -&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에서 &lt;code&gt;sub&lt;/code&gt; 필드를 찾는다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;aud&quot;: [&quot;sts.amazonaws.com&quot;],
  &quot;sub&quot;: &quot;system:serviceaccount:kube-system:external-dns&quot;,
  &quot;iss&quot;: &quot;https://oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값과 Trust Policy의 &lt;code&gt;sub&lt;/code&gt; 조건이 정확히 같아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단 2: Trust Policy 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;ROLE_NAME=external-dns-irsa
aws iam get-role --role-name $ROLE_NAME \
  --query 'Role.AssumeRolePolicyDocument'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS CLI v2에서는 위 명령이 JSON 객체를 바로 돌려준다. v1에서는 URL-encoded 문자열로 반환되므로 별도 디코딩이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;aws iam get-role --role-name $ROLE_NAME \
  --query 'Role.AssumeRolePolicyDocument' --output text | \
  python3 -c &quot;import sys, urllib.parse, json; print(json.dumps(json.loads(urllib.parse.unquote(sys.stdin.read())), indent=2))&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단 3: CloudTrail에서 실제 요청 값 보기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trust Policy와 JWT를 비교해도 미묘한 차이(공백, 유사 문자)를 놓칠 수 있다. CloudTrail에는 STS가 실제로 받은 요청 값이 남는다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
  --max-results 5 | jq '.Events[].CloudTrailEvent | fromjson | .requestParameters'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에서 &lt;code&gt;roleSessionName&lt;/code&gt;과 &lt;code&gt;roleArn&lt;/code&gt;이 기대한 값인지 확인한다. 만약 Pod가 STS에 도달하지도 못하는 상태라면 CloudTrail에 이벤트 자체가 기록되지 않는다. 그 경우 장애는 ①~③ 단계에서 발생한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;CloudTrail 이벤트는 발생 시점부터 조회 가능해지기까지 최대 15분 정도 지연이 있다. 장애 직후 쿼리했는데 결과가 비어 있다면 몇 분 뒤 다시 시도해 본다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단 4: CloudTrail &lt;code&gt;errorMessage&lt;/code&gt; 상세 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail 이벤트의 &lt;code&gt;errorMessage&lt;/code&gt; 또는 &lt;code&gt;responseElements&lt;/code&gt; 필드에는 IAM이 거부 판정을 내린 구체적 사유가 남아 있는 경우가 있다. 예를 들어 sub 불일치 시 &quot;Not authorized to perform sts:AssumeRoleWithWebIdentity&quot;와 함께 제공된 &lt;code&gt;sub&lt;/code&gt; 값이 로그에 찍혀 Trust Policy 조건과 육안 비교가 가능해진다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
  --max-results 5 | jq '.Events[].CloudTrailEvent | fromjson | {errorCode, errorMessage, requestParameters}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 IAM 일반 거부 응답에는 &lt;code&gt;EncodedAuthorizationMessage&lt;/code&gt; 필드가 포함되어 &lt;code&gt;aws sts decode-authorization-message&lt;/code&gt;로 복호화할 수 있지만, &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;의 거부 응답에는 포함되지 않는 경우가 많다. CloudTrail 이벤트 원본을 먼저 확인하는 순서가 실효성이 더 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trust Policy를 실제 SA 값에 맞춰 수정한다. AWS CLI로 바로 업데이트할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;cat &amp;gt; trust-policy.json &amp;lt;&amp;lt;'EOF'
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Federated&quot;: &quot;arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234:sub&quot;: &quot;system:serviceaccount:kube-addons:external-dns&quot;,
          &quot;oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234:aud&quot;: &quot;sts.amazonaws.com&quot;
        }
      }
    }
  ]
}
EOF

aws iam update-assume-role-policy \
  --role-name external-dns-irsa \
  --policy-document file://trust-policy.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정은 즉시 반영된다. Pod 재시작 없이 STS가 다음 호출부터 새 Trust Policy로 판단한다. 다만 SDK가 자격증명을 캐싱하고 있을 수 있으므로 수 분 정도 기다린 뒤 확인하는 게 편하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.6 Trust Policy aud 누락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;aud&lt;/code&gt;는 토큰의 수신자를 지정한다. AWS STS가 받아들이는 값은 &lt;code&gt;sts.amazonaws.com&lt;/code&gt; 하나뿐이다. 이 조건이 Trust Policy에 없거나 다른 값으로 설정되면 IAM은 토큰을 거부한다. 오래된 정책 템플릿을 복사해 쓸 때 &lt;code&gt;aud&lt;/code&gt; 라인이 빠진 채 전파되는 경우가 대표적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation:
Incorrect token audience&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 Trust Policy에 aud 조건이 아예 없으면 sub만으로 매칭을 시도하다 실패해 일반 &lt;code&gt;Not authorized&lt;/code&gt; 메시지가 뜨기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;진단&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC Provider에 등록된 Client ID List를 확인한다. 한 계정에 EKS 클러스터가 여러 개라면 첫 번째 Provider가 엉뚱한 클러스터의 것일 수 있으므로, 대상 클러스터의 OIDC ID로 필터링해야 한다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;CLUSTER_NAME=myeks
OIDC_ID=$(aws eks describe-cluster --name $CLUSTER_NAME \
  --query &quot;cluster.identity.oidc.issuer&quot; --output text | awk -F/ '{print $NF}')

PROVIDER_ARN=$(aws iam list-open-id-connect-providers | \
  jq -r &quot;.OpenIDConnectProviderList[] | select(.Arn | contains(\&quot;$OIDC_ID\&quot;)) | .Arn&quot;)

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn $PROVIDER_ARN \
  --query 'ClientIDList'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;이 포함되어야 한다. 없다면 OIDC Provider 등록 시 ClientID를 잘못 지정한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC Provider를 재생성하거나 Trust Policy의 aud 조건을 다음과 같이 추가한다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;Condition&quot;: {
  &quot;StringEquals&quot;: {
    &quot;oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234:aud&quot;: &quot;sts.amazonaws.com&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trust Policy에 sub와 aud 두 조건을 모두 두는 게 표준 권장안이다. sub만 두면 같은 OIDC Provider를 쓰는 다른 AWS 서비스의 토큰도 통과할 수 있어 권한 격리가 약해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 어느 순서로 확인할 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6가지 패턴을 머릿속에 나열식으로 가지고 있으면 막상 장애가 터졌을 때 우왕좌왕하기 쉽다. &quot;어느 쪽부터 볼지&quot;에 대한 순서를 플로우차트로 정리해 두면 디버깅 시간을 크게 줄일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Pod 로그에 AccessDenied 또는 NoCredentials 에러
  │
  ▼
[Q1] 클러스터 전체가 영향받나, 특정 Pod만 영향받나?
  │
  ├── 전체 영향 ──► 3.2 MutatingWebhook 확인
  │                  kubectl get mutatingwebhookconfiguration pod-identity-webhook
  │                  또는 3.4 OIDC Provider 미등록
  │                  aws iam list-open-id-connect-providers
  │
  └── 특정 Pod 영향
        │
        ▼
      [Q2] Pod env에 AWS_ROLE_ARN이 있나?
        │
        ├── 없음 ──► 3.1 SA annotation 확인
        │            kubectl describe sa &amp;lt;name&amp;gt;
        │
        └── 있음
              │
              ▼
            [Q3] 에러 메시지가 &quot;No OpenIDConnect provider found&quot;인가?
              │
              ├── Yes ──► 3.4 OIDC Provider 등록 확인
              │
              └── No
                    │
                    ▼
                  [Q4] 에러 메시지가 &quot;Incorrect token audience&quot;인가?
                    │
                    ├── Yes ──► 3.6 Trust Policy aud 확인
                    │
                    └── No
                          │
                          ▼
                        [Q5] SDK 버전이 최소 지원 버전 이상인가?
                          │
                          ├── 미달 ──► 3.3 SDK 업그레이드
                          │
                          └── 충족
                                │
                                ▼
                              3.5 Trust Policy sub vs JWT sub 비교
                                (가장 자주 걸리는 지점)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #f2f2f2; color: #000000; text-align: start;&quot; data-line=&quot;600&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-line=&quot;600&quot; data-ke-size=&quot;size16&quot;&gt;플로우차트의 번호는 본문 3장의 절 번호(3.1~3.6)다. IRSA 5단계 번호(①~⑤)와는 별개의 축이므로 혼동하지 않아야 한다. 단계 번호는 2장 표에서, 원인 번호는 3장 소제목에서 확인할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 플로우를 따르면 대부분의 IRSA 장애는 빠르게 범위가 좁혀진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.5 sub 불일치는 &quot;한 글자 오타&quot;처럼 보이지만 범위 좁히기가 선행되지 않으면 다른 곳을 확인하느라 시간이 길어지기 쉽다. Q1~Q5를 순서대로 거른 뒤 sub 검증으로 내려오는 흐름을 권장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 에러 메시지와 깨진 단계 매트릭스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 메시지를 보고 단계를 역추적할 수 있도록 치트시트 형태로 정리한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;에러 메시지&lt;/th&gt;
&lt;th&gt;주로 깨진 단계&lt;/th&gt;
&lt;th&gt;확인할 곳&lt;/th&gt;
&lt;th&gt;대표 조치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Not authorized to perform sts:AssumeRoleWithWebIdentity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⑤&lt;/td&gt;
&lt;td&gt;Trust Policy &lt;code&gt;sub&lt;/code&gt;/&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trust Policy 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Incorrect token audience&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⑤&lt;/td&gt;
&lt;td&gt;Trust Policy &lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;aud 조건 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;No OpenIDConnect provider found in your account&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;④&lt;/td&gt;
&lt;td&gt;OIDC Provider 등록&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eksctl utils associate-iam-oidc-provider&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InvalidIdentityTokenException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;④&lt;/td&gt;
&lt;td&gt;OIDC Provider 등록 상태&lt;/td&gt;
&lt;td&gt;OIDC Provider 재등록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WebIdentityErr: failed to retrieve credentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;② ~ ③&lt;/td&gt;
&lt;td&gt;SDK 버전 / STS endpoint&lt;/td&gt;
&lt;td&gt;SDK 업그레이드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NoCredentialProviders: no valid providers in chain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;SA annotation / Webhook&lt;/td&gt;
&lt;td&gt;annotation 추가, Pod 재생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unable to locate credentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;①&lt;/td&gt;
&lt;td&gt;Pod env / 토큰 파일 경로&lt;/td&gt;
&lt;td&gt;Pod Spec 주입 여부 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표를 북마크하거나 디버깅 노트에 복사해 두면 로그 메시지만 보고도 의심할 단계를 한 번에 좁힐 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 팀 수준에서 재발 막기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA 장애는 대부분 &quot;사람이 한 글자 틀렸다&quot;로 환원된다. 개인의 주의력에 의존하는 방식으로는 같은 유형의 장애가 반복되기 쉽다. 파이프라인 차원의 문제로 바라보면 재발을 구조적으로 줄일 수 있다. 아래는 이번 학습 과정에서 조사한 자료와 공개 이슈 사례들을 바탕으로, 실무 적용 시 고려할 수 있는 패턴들을 정리한 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 Terraform 모듈화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SA, IAM Role, Trust Policy를 세 개의 다른 파일에서 관리하면 변경이 어긋날 확률이 높다. 한 모듈로 묶어 버리는 게 안전하다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;module &quot;irsa_external_dns&quot; {
  source = &quot;./modules/irsa-role&quot;

  cluster_name        = &quot;myeks&quot;
  oidc_provider_arn   = module.eks.oidc_provider_arn
  oidc_issuer         = module.eks.cluster_oidc_issuer_url
  service_account     = &quot;external-dns&quot;
  namespace           = &quot;kube-system&quot;
  managed_policy_arns = [aws_iam_policy.external_dns.arn]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 내부에서는 &lt;code&gt;sub&lt;/code&gt; 값을 자동으로 계산한다. 사용자가 namespace와 SA 이름을 바꾸면 Trust Policy도 같이 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;this&quot; {
  name = &quot;${var.service_account}-irsa&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Federated = var.oidc_provider_arn
        }
        Action = &quot;sts:AssumeRoleWithWebIdentity&quot;
        Condition = {
          StringEquals = {
            &quot;${replace(var.oidc_issuer, &quot;https://&quot;, &quot;&quot;)}:sub&quot; = &quot;system:serviceaccount:${var.namespace}:${var.service_account}&quot;
            &quot;${replace(var.oidc_issuer, &quot;https://&quot;, &quot;&quot;)}:aud&quot; = &quot;sts.amazonaws.com&quot;
          }
        }
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 sub 값을 사람이 직접 적어 넣는 데서 오는 오타를 크게 줄일 수 있다. &lt;a href=&quot;https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest/submodules/iam-role-for-service-accounts-eks&quot;&gt;&lt;code&gt;terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks&lt;/code&gt;&lt;/a&gt; 같은 공개 모듈을 쓰면 더 편하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Helm chart annotation 표준화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;External DNS, AWS Load Balancer Controller, cert-manager 같은 도구를 Helm chart로 배포한다면 &lt;code&gt;values.yaml&lt;/code&gt;에서 annotation을 표준화해 둔다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;serviceAccount:
  create: true
  name: external-dns
  annotations:
    eks.amazonaws.com/role-arn: &quot;arn:aws:iam::123456789012:role/external-dns-irsa&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 자동화 시스템(ArgoCD 등)에서 이 values를 git으로 관리하면 annotation 누락이 PR 리뷰 단계에서 드러나기 쉽다.&lt;/p&gt;</description>
      <category>AWS/EKS</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/108</guid>
      <comments>https://hackjsp.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 19 Apr 2026 22:23:39 +0900</pubDate>
    </item>
    <item>
      <title>EKS AuthN/AuthZ - AEWS4 4주차</title>
      <link>https://hackjsp.tistory.com/107</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Pod에서 AWS API를 호출하면 무슨 일이 생기나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 안에 AWS Access Key를 넣는 건 위험하다. 이미지가 유출되면 키도 같이 나간다. 그렇다고 노드의 IAM Role을 그대로 쓰면 모든 Pod가 동일한 권한을 갖게 된다. 개발팀이 원하는 건 &quot;런타임에서 자격증명이 자동으로 주입되면서도 Pod 단위로 최소 권한이 적용되는 것&quot;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 노드 IAM Role 공유의 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 어카운트를 마운트하지 않은 Pod를 하나 만들어 보자.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test1
spec:
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      args: ['s3', 'ls']
  restartPolicy: Never
  automountServiceAccountToken: false
  terminationGracePeriodSeconds: 0
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 확인하면 AccessDenied가 뜬다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;kubectl logs eks-iam-test1
# An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Pod는 SA 토큰이 없으므로 IMDS(Instance Metadata Service)를 타고 노드의 IAM Role을 사용한다. 노드 IAM Role에 S3 권한이 없으니 실패하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 진짜 문제가 드러난다. 만약 노드 IAM Role에 S3 권한을 추가하면? 이 노드 위의 &lt;b&gt;모든&lt;/b&gt; Pod가 S3에 접근할 수 있게 된다. 특정 Pod에만 S3 권한을 주고 싶어도 방법이 없다. &quot;Pod 단위로 권한을 나눌 수 없다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail에서 ListBuckets 이벤트를 보면 &lt;code&gt;userIdentity&lt;/code&gt;에 노드의 IAM Role ARN(&lt;code&gt;eksctl-myeks-nodegroup-ng-NodeInstanceRole-xxxx&lt;/code&gt;)이 찍힌다. 어떤 Pod가 호출했는지 구분이 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;kubectl delete pod eks-iam-test1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 기본 SA 토큰은 AWS용이 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 기본 SA가 자동 마운트되는 Pod를 만들어 본다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test2
spec:
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod에 마운트된 SA 토큰을 디코딩해 보자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# 토큰 확인
kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token ; echo

# jwt decode
SA_TOKEN=$(kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
jwt decode $SA_TOKEN --json --iso8601&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이로드를 보면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;aud&quot;: [&quot;https://kubernetes.default.svc&quot;],
  &quot;exp&quot;: 1716619848,
  &quot;iat&quot;: 1685083848,
  &quot;iss&quot;: &quot;https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX&quot;,
  &quot;kubernetes.io&quot;: {
    &quot;namespace&quot;: &quot;default&quot;,
    &quot;pod&quot;: { &quot;name&quot;: &quot;eks-iam-test2&quot; },
    &quot;serviceaccount&quot;: { &quot;name&quot;: &quot;default&quot; }
  },
  &quot;sub&quot;: &quot;system:serviceaccount:default:default&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;aud&lt;/code&gt;가 &lt;code&gt;https://kubernetes.default.svc&lt;/code&gt;다. Kubernetes API 서버의 ClusterIP 서비스 도메인으로, 이 토큰의 수신자가 K8s API 서버라는 뜻이다. AWS STS는 이 토큰을 받아주지 않는다. STS가 처리하려면 &lt;code&gt;aud&lt;/code&gt;가 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;이어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 1.12부터 도입된 ProjectedServiceAccountToken 기능으로 이 토큰에는 &lt;code&gt;aud&lt;/code&gt;와 &lt;code&gt;exp&lt;/code&gt;(만료 시간)가 설정되어 있다. OIDC 규격을 따르는 JWT이므로 원칙적으로 외부 서비스 인증에도 쓸 수 있다. 하지만 &lt;code&gt;aud&lt;/code&gt;가 K8s API용이기 때문에 AWS 인증에는 별도 토큰이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 상황은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 IAM Role을 공유하면 Pod 단위 권한 분리가 안 된다&lt;/li&gt;
&lt;li&gt;기본 SA 토큰은 K8s API용이라 AWS 인증에 쓸 수 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA는 이 두 문제를 OIDC로 풀었다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;kubectl delete pod eks-iam-test2&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. IRSA - OIDC로 Pod에 AWS 권한 부여하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA(IAM Roles for Service Accounts)는 Kubernetes가 발급한 JWT 토큰을 AWS가 OIDC Provider로 검증한 뒤 IAM Role을 부여하는 구조다. EKS 1.14부터 지원한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 OIDC 인증 흐름&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m03uH/dJMcahxhJ9R/1TllfIVPbQ1PTz3YLUpVE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m03uH/dJMcahxhJ9R/1TllfIVPbQ1PTz3YLUpVE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m03uH/dJMcahxhJ9R/1TllfIVPbQ1PTz3YLUpVE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm03uH%2FdJMcahxhJ9R%2F1TllfIVPbQ1PTz3YLUpVE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1812&quot; height=&quot;879&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 생성되면 MutatingWebhook이 JWT 토큰과 IAM Role ARN 정보를 Pod Spec에 주입한다. 이후 Pod 안의 Application SDK가 AWS 리소스를 사용하려 할 때 인증 흐름이 시작된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Pod의 SDK가 자신의 JWT와 IAM Role ARN을 STS에 보내 임시 키를 요청한다 (&lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;STS는 토큰과 Role ARN을 IAM에 전달하여 승인을 요청한다&lt;/li&gt;
&lt;li&gt;IAM은 토큰의 &lt;code&gt;iss&lt;/code&gt;(발급자)가 등록된 OIDC Provider와 일치하는지, &lt;code&gt;sub&lt;/code&gt;(주체)가 Trust Policy의 조건과 맞는지, &lt;code&gt;aud&lt;/code&gt;가 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;인지, &lt;code&gt;exp&lt;/code&gt;가 만료되지 않았는지 확인한다&lt;/li&gt;
&lt;li&gt;IAM이 OIDC Provider의 JWKS(JSON Web Key Set) URL에서 공개키를 가져와 토큰 서명을 검증한다&lt;/li&gt;
&lt;li&gt;검증을 통과하면 STS가 임시 자격증명을 발급한다&lt;/li&gt;
&lt;li&gt;Pod의 SDK가 발급받은 임시 키로 S3 같은 AWS 서비스를 호출한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRHIsP/dJMcacbEUhM/bhOATiNhlXpnVC4KShvqi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRHIsP/dJMcacbEUhM/bhOATiNhlXpnVC4KShvqi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRHIsP/dJMcacbEUhM/bhOATiNhlXpnVC4KShvqi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRHIsP%2FdJMcacbEUhM%2FbhOATiNhlXpnVC4KShvqi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;766&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 접하면 단계가 많아 보이지만 실제로는 SDK가 대부분 자동으로 처리한다. 개발자가 코드에서 별도의 인증 객체를 만들 필요 없이 Default Credential Provider를 쓰면 된다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;# IRSA가 설정된 Pod에서는 이렇게만 해도 된다
import boto3
s3 = boto3.client('s3')  # SDK가 환경변수의 WebIdentityToken을 자동으로 읽음
response = s3.list_buckets()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC Discovery endpoint로 이 구조를 직접 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# 클러스터의 OIDC issuer URL 확인
IDP=$(aws eks describe-cluster --name myeks --query cluster.identity.oidc.issuer --output text)

# Discovery endpoint 호출 - OIDC Provider의 메타데이터
curl -s $IDP/.well-known/openid-configuration | jq .&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;issuer&quot;: &quot;https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX&quot;,
  &quot;jwks_uri&quot;: &quot;https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX/keys&quot;,
  &quot;authorization_endpoint&quot;: &quot;urn:kubernetes:programmatic_authorization&quot;,
  &quot;response_types_supported&quot;: [&quot;id_token&quot;],
  &quot;subject_types_supported&quot;: [&quot;public&quot;],
  &quot;id_token_signing_alg_values_supported&quot;: [&quot;RS256&quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;jwks_uri&lt;/code&gt;로 공개키를 가져올 수 있고 IAM은 이 키로 Pod가 보낸 JWT 서명이 변조되지 않았는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 공개키 세트(JWKS) 확인
curl -s $IDP/keys | jq .&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 MutatingWebhook이 Pod Spec에 끼워넣는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA가 설정된 SA를 사용하는 Pod가 생성되면 &lt;code&gt;pod-identity-webhook&lt;/code&gt;이 작동한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;kubectl get MutatingWebhookConfiguration
# NAME                            WEBHOOKS   AGE
# pod-identity-webhook            1          4h38m
# ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 webhook의 이름은 &lt;code&gt;iam-for-pods.amazonaws.com&lt;/code&gt;이다. Pod 생성 요청을 가로채서 아래 항목을 주입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;환경변수&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;AWS_ROLE_ARN&lt;/code&gt;: assume할 IAM Role의 ARN&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AWS_WEB_IDENTITY_TOKEN_FILE&lt;/code&gt;: JWT 토큰 파일 경로 (&lt;code&gt;/var/run/secrets/eks.amazonaws.com/serviceaccount/token&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Projected Volume&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마운트 경로: &lt;code&gt;/var/run/secrets/eks.amazonaws.com/serviceaccount/token&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;audience: sts.amazonaws.com&lt;/code&gt;, 만료 86400초(24시간)로 자동 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deployment YAML에는 이 항목이 없다. &lt;code&gt;kubectl get deploy -o yaml&lt;/code&gt;과 &lt;code&gt;kubectl describe pod&lt;/code&gt;를 비교해 보면 차이가 보인다. Deployment 명세에는 정의하지 않았는데 Pod에는 들어 있는 Env와 Volume이 바로 webhook이 끼워넣은 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 실습: AWS LBC를 IRSA로 설치하고 주입 결과 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Load Balancer Controller(LBC)를 IRSA로 설치하면서 위에서 설명한 구조를 직접 확인해 보자. 현재 Node IAM Role에는 ELB 관련 권한이 없는 상태다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OIDC Provider 등록 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 클러스터에는 전용 OIDC Provider가 있다. IAM에 등록되어 있는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;oidc_id=$(aws eks describe-cluster --name myeks \
  --query &quot;cluster.identity.oidc.issuer&quot; --output text | cut -d '/' -f 5)
echo $oidc_id

aws iam list-open-id-connect-providers | grep $oidc_id | cut -d &quot;/&quot; -f4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OIDC ID가 출력되면 등록된 상태다. 출력이 없으면 &lt;code&gt;eksctl utils associate-iam-oidc-provider&lt;/code&gt;로 등록해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;IAM Policy 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LBC가 ALB/NLB를 제어하려면 관련 IAM 권한이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;curl -o aws_lb_controller_policy.json \
  https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json

ACCOUNT_ID=$(aws sts get-caller-identity --query &quot;Account&quot; --output text)

aws iam create-policy \
  --policy-name AWSLoadBalancerControllerIAMPolicy \
  --policy-document file://aws_lb_controller_policy.json&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;IRSA 생성 (iamserviceaccount)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;eksctl create iamserviceaccount&lt;/code&gt;는 CloudFormation을 통해 Kubernetes SA와 IAM Role을 한 번에 만들고 Trust Policy로 연결한다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;CLUSTER_NAME=myeks

eksctl create iamserviceaccount \
  --cluster=$CLUSTER_NAME \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
  --override-existing-serviceaccounts \
  --approve&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 결과를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# IRSA 목록 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME

# SA에 IAM Role ARN annotation 확인
kubectl get sa -n kube-system aws-load-balancer-controller -o yaml
# annotations:
#   eks.amazonaws.com/role-arn: arn:aws:iam::XXXX:role/eksctl-myeks-addon-iamserviceaccount-...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Trust Policy 구조 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM 콘솔에서 생성된 Role의 Trust Policy를 보면 이런 구조다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: {
      &quot;Federated&quot;: &quot;arn:aws:iam::XXXX:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX&quot;
    },
    &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
    &quot;Condition&quot;: {
      &quot;StringEquals&quot;: {
        &quot;oidc.eks....:sub&quot;: &quot;system:serviceaccount:kube-system:aws-load-balancer-controller&quot;,
        &quot;oidc.eks....:aud&quot;: &quot;sts.amazonaws.com&quot;
      }
    }
  }]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Principal이 EKS 클러스터의 OIDC Provider다. Condition에서 &lt;code&gt;sub&lt;/code&gt;(SA 이름)와 &lt;code&gt;aud&lt;/code&gt;(&lt;code&gt;sts.amazonaws.com&lt;/code&gt;)를 확인한다. &quot;이 OIDC Provider가 발급한 토큰 중에서 kube-system 네임스페이스의 aws-load-balancer-controller SA가 가진 토큰만 이 Role을 assume할 수 있다&quot;는 뜻이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Helm으로 LBC 설치 및 주입 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SA는 이미 만들었으므로 &lt;code&gt;serviceAccount.create=false&lt;/code&gt;로 설치한다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;helm repo add eks https://aws.github.io/eks-charts
helm repo update

helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system --version 3.1.0 \
  --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.name=aws-load-balancer-controller \
  --set serviceAccount.create=false \
  --set enableCertManager=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LBC Pod를 describe해서 webhook이 주입한 항목을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;kubectl describe pod -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;Environment:
  AWS_STS_REGIONAL_ENDPOINTS:   regional
  AWS_DEFAULT_REGION:           ap-northeast-2
  AWS_REGION:                   ap-northeast-2
  AWS_ROLE_ARN:                 arn:aws:iam::XXXX:role/eksctl-myeks-addon-...
  AWS_WEB_IDENTITY_TOKEN_FILE:  /var/run/secrets/eks.amazonaws.com/serviceaccount/token
Mounts:
  /var/run/secrets/eks.amazonaws.com/serviceaccount from aws-iam-token (ro)

Volumes:
  aws-iam-token:
    Type:                    Projected
    TokenExpirationSeconds:  86400&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Deployment&lt;/code&gt; YAML에는 &lt;code&gt;AWS_ROLE_ARN&lt;/code&gt;이나 &lt;code&gt;aws-iam-token&lt;/code&gt; 볼륨이 정의되어 있지 않다. 비교해 보면 webhook 주입을 눈으로 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;kubectl get deploy -n kube-system aws-load-balancer-controller -o yaml | grep -c AWS_ROLE_ARN
# 0 (Deployment에는 없음)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 토큰의 audience도 확인해 보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get pod -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller \
  -o yaml | grep -A3 'serviceAccountToken'
#   audience: sts.amazonaws.com
#   expirationSeconds: 86400
#   path: token&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;섹션 1.2에서 본 기본 SA 토큰은 &lt;code&gt;aud: https://kubernetes.default.svc&lt;/code&gt;였다. IRSA가 주입한 두 번째 토큰은 &lt;code&gt;aud: sts.amazonaws.com&lt;/code&gt;이다. 같은 Pod에 토큰이 두 개 마운트되는 셈이다. 하나는 K8s API용이고 다른 하나는 AWS STS용이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 실습: S3 ReadOnly IRSA로 권한 경계 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 S3 읽기 전용 권한만 가진 IRSA를 직접 만들어서 권한 경계가 어떻게 동작하는지 확인해 보자.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;eksctl create iamserviceaccount \
  --name my-sa \
  --namespace default \
  --cluster $CLUSTER_NAME \
  --approve \
  --role-name eksctl-myeks-pod-irsa-s3-readonly-role \
  --attach-policy-arn $(aws iam list-policies \
    --query 'Policies[?PolicyName==`AmazonS3ReadOnlyAccess`].Arn' --output text)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 SA를 사용하는 Pod를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test3
spec:
  serviceAccountName: my-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입된 IRSA 토큰을 디코딩해 본다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;SA_TOKEN=$(kubectl exec -it eks-iam-test3 -- \
  cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
jwt decode $SA_TOKEN --json --iso8601&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;aud&quot;: [&quot;sts.amazonaws.com&quot;],
  &quot;iss&quot;: &quot;https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX&quot;,
  &quot;kubernetes.io&quot;: {
    &quot;namespace&quot;: &quot;default&quot;,
    &quot;pod&quot;: { &quot;name&quot;: &quot;eks-iam-test3&quot; },
    &quot;serviceaccount&quot;: { &quot;name&quot;: &quot;my-sa&quot; }
  },
  &quot;sub&quot;: &quot;system:serviceaccount:default:my-sa&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;aud&lt;/code&gt;가 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;이고 &lt;code&gt;sub&lt;/code&gt;가 &lt;code&gt;system:serviceaccount:default:my-sa&lt;/code&gt;다. Trust Policy의 Condition에 적힌 값과 정확히 대응된다. 이 조합이 맞아야 STS가 임시 자격증명을 발급한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 경계를 확인해 보자.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 어떤 Role로 인증되었는지 확인
kubectl exec -it eks-iam-test3 -- aws sts get-caller-identity --query Arn
# &quot;arn:aws:sts::XXXX:assumed-role/eksctl-myeks-pod-irsa-s3-readonly-role/botocore-session-...&quot;

# S3는 된다
kubectl exec -it eks-iam-test3 -- aws s3 ls

# EC2는 안 된다
kubectl exec -it eks-iam-test3 -- aws ec2 describe-instances --region ap-northeast-2
# An error occurred (UnauthorizedOperation)

# VPC도 안 된다
kubectl exec -it eks-iam-test3 -- aws ec2 describe-vpcs --region ap-northeast-2
# An error occurred (UnauthorizedOperation)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3ReadOnlyAccess 정책만 붙었으므로 S3만 접근 가능하고 나머지는 전부 거부된다. 섹션 1.1에서 노드 IAM Role로는 불가능했던 Pod 단위 최소 권한이 동작하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail에서 &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; 이벤트도 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
  --max-items=1 | jq '.Events[] | (.CloudTrailEvent | fromjson) | {eventName, eventSource, userIdentity}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;userIdentity.type&lt;/code&gt;이 &lt;code&gt;WebIdentityUser&lt;/code&gt;이고 &lt;code&gt;identityProvider&lt;/code&gt;에 EKS OIDC Provider ARN이 찍혀 있다. 섹션 1.1에서 노드 IAM Role만 보이던 것과 비교하면 누가 호출했는지 추적이 가능해진 것이다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;# 리소스 정리
kubectl delete pod eks-iam-test3
eksctl delete iamserviceaccount --cluster $CLUSTER_NAME --name my-sa --namespace default&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Pod Identity - OIDC 없이 더 단순하게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS Pod Identity는 2023년에 나온 인증 방식이다. IRSA처럼 Pod 단위 IAM 권한을 부여하지만 OIDC Provider 등록이 필요 없다. EKS 1.24 이상에서 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Pod Identity Agent가 동작하는 방식&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R70rP/dJMcacbEUjS/fCIDepkkfJTKTILwrFjJhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R70rP/dJMcacbEUjS/fCIDepkkfJTKTILwrFjJhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R70rP/dJMcacbEUjS/fCIDepkkfJTKTILwrFjJhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR70rP%2FdJMcacbEUjS%2FfCIDepkkfJTKTILwrFjJhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;844&quot; height=&quot;846&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA에서는 Pod 안의 SDK가 직접 STS를 호출했다. Pod Identity는 각 노드에 설치된 Agent가 인증을 대행하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;eks-pod-identity-agent&lt;/code&gt;는 EKS addon으로 설치되는 DaemonSet이다. &lt;code&gt;hostNetwork: true&lt;/code&gt;로 노드 네트워크를 직접 사용하며 link-local 주소 &lt;code&gt;169.254.170.23&lt;/code&gt;의 port 80에서 요청을 수신한다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;# DaemonSet 확인
kubectl -n kube-system get daemonset eks-pod-identity-agent
kubectl get ds -n kube-system eks-pod-identity-agent -o yaml | grep -A2 hostNetwork
#   hostNetwork: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드에 SSH로 접속해서 네트워크 상태를 직접 확인해 보면 구조가 보인다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Agent가 리스닝하는 포트 확인
sudo ss -tnlp | grep eks-pod-identit
# LISTEN  169.254.170.23:80    users:((&quot;eks-pod-identit&quot;,...))
# LISTEN  127.0.0.1:2703       users:((&quot;eks-pod-identit&quot;,...))

# link-local 인터페이스 확인
sudo ip -c addr show pod-id-link0
# inet 169.254.170.23/32 scope global pod-id-link0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pod-id-link0&lt;/code&gt;이라는 가상 인터페이스에 &lt;code&gt;169.254.170.23&lt;/code&gt; 주소가 할당되어 있다. Pod가 이 주소로 자격증명을 요청하면 Agent가 EKS Auth API(&lt;code&gt;AssumeRoleForPodIdentity&lt;/code&gt;)를 호출하여 임시 자격증명을 받아 돌려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;link-local 주소를 사용하는 건 AWS 메타데이터 서비스(IMDS, &lt;code&gt;169.254.169.254&lt;/code&gt;)와 같은 패턴이다. Pod에서 별도 설정 없이 고정된 주소로 접근할 수 있어서 편리하다. &lt;code&gt;CAP_NET_BIND_SERVICE&lt;/code&gt; capability로 권한이 부여된 80 포트를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 실습: podidentityassociation으로 S3 접근 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA에서는 &lt;code&gt;eksctl create iamserviceaccount&lt;/code&gt;를 썼다. Pod Identity에서는 &lt;code&gt;eksctl create podidentityassociation&lt;/code&gt;을 쓴다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;eksctl create podidentityassociation \
  --cluster $CLUSTER_NAME \
  --namespace default \
  --create-service-account \
  --service-account-name s3-sa \
  --role-name s3-eks-pod-identity-role \
  --permission-policy-arns arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
  --region ap-northeast-2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 연결을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;eksctl get podidentityassociation --cluster $CLUSTER_NAME
# NAMESPACE  SERVICE ACCOUNT NAME  IAM ROLE ARN
# default    s3-sa                 arn:aws:iam::XXXX:role/s3-eks-pod-identity-role

aws eks list-pod-identity-associations --cluster-name $CLUSTER_NAME | jq&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Trust Policy 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 IAM Role의 Trust Policy를 확인하면 IRSA와 확연히 다르다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;aws iam get-role --query 'Role.AssumeRolePolicyDocument' \
  --role-name s3-eks-pod-identity-role | jq .&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: {
      &quot;Service&quot;: &quot;pods.eks.amazonaws.com&quot;
    },
    &quot;Action&quot;: [
      &quot;sts:AssumeRole&quot;,
      &quot;sts:TagSession&quot;
    ]
  }]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA의 Trust Policy에는 OIDC Provider ARN과 SA 이름까지 Condition에 들어갔다. Pod Identity는 &lt;code&gt;pods.eks.amazonaws.com&lt;/code&gt; 서비스만 Principal로 지정하면 끝이다. OIDC Provider URL도 없고 SA를 명시하는 Condition도 없다. &lt;code&gt;sts:TagSession&lt;/code&gt;은 세션 태그 기능(ABAC)을 위한 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Pod 생성 및 환경변수 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 SA를 사용하는 Pod를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-pod-identity
spec:
  serviceAccountName: s3-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수를 확인하면 IRSA와 다른 변수가 주입되어 있다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;kubectl exec -it eks-pod-identity -- env | grep AWS
# AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token
# AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.23/v1/credentials
# AWS_STS_REGIONAL_ENDPOINTS=regional
# AWS_DEFAULT_REGION=ap-northeast-2
# AWS_REGION=ap-northeast-2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IRSA는 &lt;code&gt;AWS_ROLE_ARN&lt;/code&gt;과 &lt;code&gt;AWS_WEB_IDENTITY_TOKEN_FILE&lt;/code&gt;을 주입했다. Pod Identity는 &lt;code&gt;AWS_CONTAINER_CREDENTIALS_FULL_URI&lt;/code&gt;로 link-local Agent 주소를 넣어준다. SDK가 이 URL로 자격증명을 요청하는 방식이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토큰 디코딩&lt;/h4&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;kubectl exec -it eks-pod-identity -- \
  cat /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;aud&quot;: [&quot;pods.eks.amazonaws.com&quot;],
  &quot;iss&quot;: &quot;https://oidc.eks.ap-northeast-2.amazonaws.com/id/XXXX&quot;,
  &quot;kubernetes.io&quot;: {
    &quot;namespace&quot;: &quot;default&quot;,
    &quot;pod&quot;: { &quot;name&quot;: &quot;eks-pod-identity&quot; },
    &quot;serviceaccount&quot;: { &quot;name&quot;: &quot;s3-sa&quot; }
  },
  &quot;sub&quot;: &quot;system:serviceaccount:default:s3-sa&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;aud&lt;/code&gt;가 &lt;code&gt;pods.eks.amazonaws.com&lt;/code&gt;이다. IRSA는 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;, 기본 SA는 &lt;code&gt;kubernetes.default.svc&lt;/code&gt;였다. 토큰의 &lt;code&gt;aud&lt;/code&gt; 값만 비교해도 세 방식이 구분된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;link-local 직접 호출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod 안에서 Agent의 link-local 주소로 직접 자격증명을 요청해 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;kubectl exec -it eks-pod-identity -- bash&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Pod 내부에서 실행
EKS_POD_IDENTITY_TOKEN=$(cat $AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE)
curl -s 169.254.170.23/v1/credentials \
  -H &quot;Authorization: $EKS_POD_IDENTITY_TOKEN&quot; | jq&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;AccessKeyId&quot;: &quot;ASIA...&quot;,
  &quot;SecretAccessKey&quot;: &quot;NGY/...&quot;,
  &quot;Token&quot;: &quot;IQoJb3...&quot;,
  &quot;Expiration&quot;: &quot;2026-04-02T16:14:05Z&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent가 EKS Auth API를 대신 호출해서 받아온 임시 자격증명이 그대로 돌아온다. SDK는 내부적으로 이 과정을 자동으로 처리하는 것이다. AWS 자격증명의 3요소(AccessKeyId, SecretAccessKey, Token)가 모두 들어 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;S3 접근 및 CloudTrail 확인&lt;/h4&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;kubectl exec -it eks-pod-identity -- aws sts get-caller-identity --query Arn
# &quot;arn:aws:sts::XXXX:assumed-role/s3-eks-pod-identity-role/...&quot;

kubectl exec -it eks-pod-identity -- aws s3 ls
# (버킷 목록 출력)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail에서는 &lt;code&gt;AssumeRoleForPodIdentity&lt;/code&gt; 이벤트로 기록된다. IRSA의 &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;와 이벤트 이름부터 다르다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleForPodIdentity \
  --max-items=1 | jq '.Events[] | (.CloudTrailEvent | fromjson) | {eventName, eventSource}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;eventSource&lt;/code&gt;가 &lt;code&gt;eks-auth.amazonaws.com&lt;/code&gt;인 것을 확인할 수 있다. IRSA는 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;이었다. 인증 경로 자체가 다르기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 리소스 정리
kubectl delete pod eks-pod-identity
eksctl delete podidentityassociation \
  --cluster $CLUSTER_NAME --namespace default --service-account-name s3-sa
kubectl delete sa s3-sa&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 IRSA에서 뭐가 달라졌나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식을 직접 실습하면서 확인한 차이를 정리한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;IRSA&lt;/th&gt;
&lt;th&gt;Pod Identity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Trust Policy&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;OIDC Provider를 Federated Principal로 지정 + Condition에 sub, aud 명시&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pods.eks.amazonaws.com&lt;/code&gt; 서비스 주체만 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;토큰 audience&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sts.amazonaws.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pods.eks.amazonaws.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인증 경로&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Pod SDK가 STS를 직접 호출&lt;/td&gt;
&lt;td&gt;Pod가 link-local Agent에 요청, Agent가 EKS Auth API 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;클러스터 확장&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;새 클러스터마다 OIDC 등록 필요, Trust Policy에 추가해야 함&lt;/td&gt;
&lt;td&gt;동일 Role 재사용 가능, Trust Policy 수정 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;세션 태그&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;미지원&lt;/td&gt;
&lt;td&gt;클러스터명, 네임스페이스, SA명이 세션 태그로 붙어 ABAC 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;CloudTrail 이벤트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; (sts.amazonaws.com)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AssumeRoleForPodIdentity&lt;/code&gt; (eks-auth.amazonaws.com)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 확장 시 차이가 실무에서 체감이 크다. IRSA는 EKS 클러스터를 새로 만들 때마다 OIDC Provider를 등록하고 기존 IAM Role의 Trust Policy에 새 OIDC Provider를 추가해야 한다. Trust Policy 크기 제한 때문에 하나의 Role에 최대 8개 클러스터까지만 연결할 수 있다. Pod Identity는 클러스터가 늘어나도 같은 Role에 podidentityassociation만 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AWS/EKS</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/107</guid>
      <comments>https://hackjsp.tistory.com/107#entry107comment</comments>
      <pubDate>Mon, 13 Apr 2026 00:17:29 +0900</pubDate>
    </item>
    <item>
      <title>EKS 컴퓨팅과 오토스케일링 - AEWS4 3주차</title>
      <link>https://hackjsp.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;EKS에서 워크로드를 운영하려면 두 가지를 결정해야 한다. 어디서 실행할 것인가(컴퓨팅)와 얼마나 실행할 것인가(오토스케일링). 3주차에서는 관리형 노드 그룹으로 컴퓨팅 옵션을 살펴보고 HPA부터 Karpenter까지 여러 스케일링 방법을 실습한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 실습 환경 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform으로 EKS 클러스터를 배포한다. 이번 실습 환경의 변경점은 k8s 1.35 버전 사용과 워커노드 Private Subnet 배치, SSM 접속 지원이다. add-on으로 metrics-server와 external-dns가 포함되어 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# 코드 다운로드
git clone https://github.com/gasida/aews.git
cd aews/3w

# IAM Policy 파일 작성
curl -o aws_lb_controller_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json

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

# 배포 완료 후 kubeconfig 설정
$(terraform output -raw configure_kubectl)
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') myeks&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포가 완료되면 add-on을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;aws eks list-addons --cluster-name myeks | jq
# coredns, external-dns, kube-proxy, metrics-server, vpc-cni&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;external-dns는 &lt;code&gt;policy=upsert-only&lt;/code&gt;로 설정되어 있어 레코드 생성/수정만 하고 삭제는 수동으로 해야 한다. &lt;code&gt;policy=sync&lt;/code&gt;로 바꾸면 자동 삭제도 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSM으로 노드 접속하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커 노드가 Private Subnet에 있기 때문에 SSH 대신 SSM Session Manager로 접속한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# SSM 대상 인스턴스 확인
aws ssm describe-instance-information \
  --query &quot;InstanceInformationList[*].{InstanceId:InstanceId, Status:PingStatus}&quot; \
  --output text

# 접속
aws ssm start-session --target i-0a6db521a84f6ea1f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSM은 AWS 공인 IP 대역을 통해 연결되며 별도의 인바운드 포트 오픈이 필요 없다. IAM 사용자의 세션 활동 로깅도 가능해서 감사(audit) 측면에서도 유리하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS Load Balancer Controller 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LBC 설치 시 이슈가 있었다. VPC를 찾지 못한다는 오류가 발생했는데 노드의 IMDS에서 VPC ID를 가져와야 하는 상황이었다. Terraform 코드에서 hop limit을 2로 설정해 두었기 때문에 파드에서 IMDS 접근이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system --version 3.1.0 --set clusterName=myeks&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;eks-node-viewer 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드의 할당 가능 용량과 요청 리소스를 시각적으로 보여주는 도구다. &quot;실제 파드 리소스 사용량이 아니라 request 값을 표시한다&quot;는 점을 기억해야 한다. init container는 포함하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;brew tap aws/tap
brew install eks-node-viewer

# CPU와 메모리를 함께 표시
eks-node-viewer --resources cpu,memory
# 노드 나이와 AZ 정보까지 표시
eks-node-viewer --resources cpu,memory --extra-labels eks-node-viewer/node-age,topology.kubernetes.io/zone&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kube-ops-view + kube-prometheus-stack&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 시각화를 위해 kube-ops-view를 배포하고 모니터링을 위해 kube-prometheus-stack을 설치한다. 같은 ALB를 공유하기 위해 &lt;code&gt;group.name: study&lt;/code&gt; 설정을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 \
  --set service.main.type=ClusterIP --set env.TZ=&quot;Asia/Seoul&quot; --namespace kube-system

# kube-prometheus-stack
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --version 80.13.3 -f monitor-values.yaml --create-namespace --namespace monitoring&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 컨트롤 플레인 메트릭(apiserver, scheduler, controller manager)도 &lt;code&gt;additionalScrapeConfigs&lt;/code&gt;로 수집하도록 구성했다. 설치 후 &lt;code&gt;prometheus.hackjap.link&lt;/code&gt;와 &lt;code&gt;grafana.hackjap.link&lt;/code&gt;로 접속할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 관리형 노드 그룹&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 관리형 노드 그룹(Managed Node Group)은 온디맨드, Graviton(ARM), Spot 인스턴스로 나뉜다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 온디맨드 노드 그룹&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 생성되는 &lt;code&gt;myeks-ng-1&lt;/code&gt;은 온디맨드 인스턴스다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;kubectl get nodes --label-columns eks.amazonaws.com/nodegroup,kubernetes.io/arch,eks.amazonaws.com/capacityType&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NAME                                                STATUS   ROLES    AGE   VERSION               NODEGROUP    ARCH    CAPACITYTYPE
ip-192-168-19-163.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   40m   v1.35.2-eks-f69f56f   myeks-ng-1   amd64   ON_DEMAND
ip-192-168-22-130.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   40m   v1.35.2-eks-f69f56f   myeks-ng-1   amd64   ON_DEMAND&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;amd64 아키텍처에 ON_DEMAND 타입으로 2대가 실행 중이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 Graviton(ARM) 노드 그룹&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Graviton은 64-bit ARM 프로세서 코어 기반의 AWS 커스텀 반도체다. 기존 인텔 x86 대비 &quot;20~40% 향상된 가격대비 성능&quot;을 제공한다. Terraform 코드에서 &lt;code&gt;ami_type&lt;/code&gt;을 &lt;code&gt;AL2023_ARM_64_STANDARD&lt;/code&gt;로 지정하고 &lt;code&gt;t4g.medium&lt;/code&gt; 인스턴스를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 아키텍처 환경에서 가장 주의할 점은 이미지 호환성이다. ARM 노드에 amd64 전용 이미지를 배포하면 실행되지 않는다. Taint와 Toleration으로 제어한다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# node label
labels = {
  tier = &quot;secondary&quot;
}

# node taint - Toleration 없는 파드는 스케줄링되지 않고, 기존 파드도 Evict됨
taints = {
  frontend = {
    key    = &quot;cpuarch&quot;
    value  = &quot;arm64&quot;
    effect = &quot;NO_EXECUTE&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;NO_EXECUTE&lt;/code&gt;는 가장 강한 Taint 효과다. 스케줄링을 막을 뿐 아니라 이미 실행 중인 파드도 Toleration이 없으면 퇴출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 추가 후 SSM으로 접속하면 &lt;code&gt;arch&lt;/code&gt; 명령어로 &lt;code&gt;aarch64&lt;/code&gt;가 출력되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NAME                                                STATUS   ROLES    AGE    NODEGROUP    ARCH    CAPACITYTYPE
ip-192-168-14-105.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   113s   myeks-ng-2   arm64   ON_DEMAND
ip-192-168-19-163.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   46m    myeks-ng-1   amd64   ON_DEMAND
ip-192-168-22-130.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   46m    myeks-ng-1   amd64   ON_DEMAND&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM 노드에 파드를 배포할 때는 nodeSelector로 아키텍처를 지정하고 Toleration을 추가해야 한다. &lt;code&gt;nginx:alpine&lt;/code&gt;처럼 멀티 아키텍처를 지원하는 이미지를 사용하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 Spot 인스턴스 노드 그룹&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spot 인스턴스는 AWS EC2 여유 용량을 경매 방식으로 사용한다. 온디맨드 대비 최대 90%까지 저렴하지만 언제든 회수될 수 있다. 상태 비저장(Stateless) 워크로드에 적합하다. Pod를 우아하게 종료할 수 있고 Spot이 중단되면 다른 노드에서 대체 Pod를 스케줄링할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spot 노드 그룹 설정에서 &quot;여러 인스턴스 타입을 지정하는 것&quot;이 가장 중요하다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;third = {
  name            = &quot;${var.ClusterBaseName}-ng-3&quot;
  ami_type        = &quot;AL2023_x86_64_STANDARD&quot;
  capacity_type   = &quot;SPOT&quot;
  instance_types  = [&quot;c5a.large&quot;, &quot;c6a.large&quot;, &quot;t3a.large&quot;, &quot;t3a.medium&quot;]
  desired_size    = 1
  max_size        = 1
  min_size        = 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 타입 하나만 지정하면 해당 타입의 Spot 용량이 없을 때 노드를 확보하지 못한다. 여러 타입으로 가용 풀을 넓혀야 안정적이다. nodeSelector로 Spot 노드에 파드를 배치할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;kubectl get nodes --label-columns eks.amazonaws.com/capacityType&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NAME                                                STATUS   ROLES    AGE    CAPACITYTYPE
ip-192-168-14-187.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   106s   SPOT
ip-192-168-19-163.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   61m    ON_DEMAND
ip-192-168-22-130.ap-northeast-2.compute.internal   Ready    &amp;lt;none&amp;gt;   61m    ON_DEMAND&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 IMDS 보안 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드에서 동작하는 파드는 EC2의 IMDS(Instance Metadata Service)에 접근할 수 있다. 이게 왜 문제가 되는지 확인해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;awscli 파드를 배포하고 별도의 IAM 자격 증명 없이 AWS API를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 별도 IAM 자격 증명 없이도 동작한다
kubectl exec -it $APODNAME1 -- aws ec2 describe-instances --region ap-northeast-2 --output table --no-cli-pager
kubectl exec -it $APODNAME2 -- aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자격 증명이 없는데도 API 호출이 된다. 파드가 노드의 EC2 Instance Profile(IAM Role)을 그대로 사용하기 때문이다. 보안이 취약한 컨테이너가 탈취되면 IMDS로 노드의 IAM Role에 접근할 수 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IMDSv2에서는 토큰 기반으로 접근을 제한한다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# IMDSv2 토큰 요청
TOKEN=$(curl -s -X PUT &quot;http://169.254.169.254/latest/api/token&quot; \
  -H &quot;X-aws-ec2-metadata-token-ttl-seconds: 21600&quot;)

# 토큰으로 IAM Role 자격증명 조회
curl -s -H &quot;X-aws-ec2-metadata-token: $TOKEN&quot; \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/myeks-ng-1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력된 AccessKeyId, SecretAccessKey, Token은 Expiration 전까지 어디서든 사용 가능하다. 제가 생각하기에 이 부분은 IMDS 접근 자체를 Pod 단위로 차단하고 IRSA(IAM Roles for Service Accounts)나 Pod Identity를 사용하는 것을 추천드립니다. 자세한 내용은 4주차(보안)에서 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 스케일링 전략 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케일링을 적용하기 전에 먼저 &quot;우리 워크로드에 어떤 수준의 스케일링이 필요한가&quot;를 판단해야 한다. EKS 스케일링 전략 의사결정 프레임워크를 참고하면 유용하다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;접근법&lt;/th&gt;
&lt;th&gt;핵심 전략&lt;/th&gt;
&lt;th&gt;E2E 스케일링 시간&lt;/th&gt;
&lt;th&gt;복잡도&lt;/th&gt;
&lt;th&gt;적합한 워크로드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;반응형 고속화&lt;/td&gt;
&lt;td&gt;Karpenter + KEDA + Warm Pool&lt;/td&gt;
&lt;td&gt;5~45초&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;극소수 미션 크리티컬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예측형 스케일링&lt;/td&gt;
&lt;td&gt;CronHPA + Predictive Scaling&lt;/td&gt;
&lt;td&gt;사전 확장 (0초)&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;패턴 있는 대부분의 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;아키텍처 복원력&lt;/td&gt;
&lt;td&gt;SQS/Kafka + Circuit Breaker&lt;/td&gt;
&lt;td&gt;스케일링 지연 허용&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;비동기 처리 가능한 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적정 기본 용량&lt;/td&gt;
&lt;td&gt;기본 replica 20~30% 증설&lt;/td&gt;
&lt;td&gt;불필요 (이미 충분)&lt;/td&gt;
&lt;td&gt;매우 낮음&lt;/td&gt;
&lt;td&gt;안정적인 트래픽&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;트래픽 급증 시 사용자 에러 방지&quot;라는 동일한 문제를 해결하는 접근법이 4가지나 있다. 대부분의 워크로드에서는 접근법 2~4가 더 비용 효율적이다. 반응형 고속화는 극소수 미션 크리티컬 서비스에만 적용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케일링 방법은 크게 다섯 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HPA&lt;/b&gt; - 파드 수를 수평 확장. CPU/Memory 메트릭 기반&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VPA&lt;/b&gt; - 파드 리소스를 수직 확장. request/limit 최적값 자동 조정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;KEDA&lt;/b&gt; - 이벤트 기반 파드 오토스케일링. Cron, SQS, Kafka 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CA/CAS&lt;/b&gt; - 노드를 수평 확장. ASG 연동&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Karpenter&lt;/b&gt; - 노드를 수평/수직 확장. EC2 API 직접 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. HPA - Horizontal Pod Autoscaler&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HPA는 쿠버네티스에 내장된 오토스케일러다. Pod 메트릭을 관찰하고 목표값에 맞춰 복제본 수를 조절한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 HPA 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 애플리케이션을 배포한다. hpa-example 이미지는 내부적으로 100만번 덧셈을 수행하는 CPU 과부하 연산을 실행한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  selector:
    matchLabels:
      run: php-apache
  template:
    metadata:
      labels:
        run: php-apache
    spec:
      containers:
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
          requests:
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
spec:
  ports:
  - port: 80
  selector:
    run: php-apache&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HPA 정책을 생성한다. CPU 사용률이 50%를 넘으면 Pod를 늘리고 최대 10개까지 확장한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        averageUtilization: 50
        type: Utilization&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증가와 감소의 대기 시간이 다르다는 점을 알아두자. 증가 시 기본 30초이고 감소 시 기본 5분이다. 감소 대기 시간이 더 긴 이유는 트래픽이 다시 급증할 경우를 대비하기 위해서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하를 발생시켜 스케일링을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;kubectl exec -it curl -- sh -c '
for i in $(seq 1 5); do
  while true; do curl -s php-apache &amp;amp; sleep 1; done &amp;amp;
done
wait'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;1290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DxMO6/dJMcahDVLRr/2BKrPUq8o9IThk15P4ovJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DxMO6/dJMcahDVLRr/2BKrPUq8o9IThk15P4ovJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DxMO6/dJMcahDVLRr/2BKrPUq8o9IThk15P4ovJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDxMO6%2FdJMcahDVLRr%2F2BKrPUq8o9IThk15P4ovJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1684&quot; height=&quot;1290&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;1290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kubectl describe hpa&lt;/code&gt;로 이벤트를 확인하면 Pod가 4개에서 5개로 순차적으로 늘어나는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Events:
  Type    Reason             Age    From                       Message
  ----    ------             ----   ----                       -------
  Normal  SuccessfulRescale  2m28s  horizontal-pod-autoscaler  New size: 4; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  2m13s  horizontal-pod-autoscaler  New size: 5; reason: cpu resource utilization (percentage of request) above target&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 커스텀 메트릭 HPA&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;autoscaling/v2&lt;/code&gt;부터는 CPU/Memory 외에 커스텀 메트릭으로도 스케일링할 수 있다. 메트릭 소스에 따라 세 가지 API로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Resource Metric API&lt;/b&gt; (&lt;code&gt;metrics.k8s.io&lt;/code&gt;) - Node나 Pod의 CPU/메모리 사용량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Custom Metric API&lt;/b&gt; (&lt;code&gt;custom.metrics.k8s.io&lt;/code&gt;) - 클러스터 내부 사용자 정의 메트릭&lt;/li&gt;
&lt;li&gt;&lt;b&gt;External Metric API&lt;/b&gt; (&lt;code&gt;external.metrics.k8s.io&lt;/code&gt;) - 클러스터 외부 메트릭&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케일링 기준이 되는 지표를 어디서 가져오느냐에 따라 타입이 달라진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pods&lt;/b&gt; 타입 - 대상 파드에서 직접 수집. 초당 패킷 수나 TPS 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Object&lt;/b&gt; 타입 - 클러스터 내 다른 리소스(Ingress, Service)에서 수집. Ingress의 초당 HTTP 요청 수 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;External&lt;/b&gt; 타입 - 클러스터 외부 시스템에서 수집. AWS SQS 대기 메시지 수나 Kafka 토픽 lag 등&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 50
- type: Pods
  pods:
    metric:
      name: packets-per-second
    target:
      type: AverageValue
      averageValue: 1k
- type: Object
  object:
    metric:
      name: requests-per-second
    describedObject:
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      name: main-route
    target:
      type: Value
      value: 10k&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 큐 처리 파드의 CPU는 여유롭지만 RabbitMQ 큐에 메시지가 만 건 이상 쌓였을 때 선제적으로 파드를 늘리는 식으로 활용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 HPA 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HPA를 운영할 때 두 가지를 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 스파이크성 트래픽에 대응이 어렵다. 메트릭 수집부터 Pod 생성까지 시간이 걸린다. 갑자기 트래픽이 급증하면 이미 응답 지연이 발생한 뒤에야 스케일 아웃된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, Pod 초기화 시 CPU/Memory가 일시적으로 급등하는 버스트 현상이 있다. Kubernetes Startup CPU Boost를 사용하면 InPlacePodVerticalScaling으로 시작 시에만 CPU를 높게 잡았다가 초기화 이후 원래 값으로 내릴 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. VPA - Vertical Pod Autoscaler&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPA는 Pod의 &lt;code&gt;resources.request&lt;/code&gt;를 최적값으로 수정한다. HPA와 동시에 사용할 수 없다는 점이 가장 큰 제약이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Pod를 재시작(기존 Pod 종료 후 새 Pod 생성)해야 했다. 최근 In-place 업데이트가 나오면서 재시작 없이 CPU를 조정할 수 있게 되었지만 Memory는 아직 불안정하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPA를 설치하고 공식 예제(hamster.yaml)를 배포하면 2~3분 뒤에 Pod의 request가 자동으로 변경된다. &lt;code&gt;updateMode: Off&lt;/code&gt;로 설정하면 추천값만 확인하고 실제 변경은 하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# VPA 추천값 확인
kubectl describe pod | grep Requests: -A2
    Requests:
      cpu:        587m
      memory:     262144k&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 기반 애플리케이션에서 주의할 점이 있다. VPA가 memory를 4Gi로 올려도 JVM 힙 사이즈가 &lt;code&gt;-Xmx2g&lt;/code&gt;로 고정되어 있으면 나머지 2Gi는 낭비된다. 해결 방법은 &lt;code&gt;-XX:MaxRAMPercentage=75.0&lt;/code&gt;을 사용해서 컨테이너 limit에 비례하도록 동적 계산하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPA 대안으로 KRR(Kubernetes Resource Recommender)도 있다. Prometheus 기반으로 동작하며 클러스터 외부에서 실행할 수 있고 즉시 결과를 보여준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;brew tap robusta-dev/homebrew-krr
brew install krr

# 1달 데이터 기준으로 CPU 90 percentile, Memory 10% 버퍼
krr simple --history_duration 720 --cpu_percentile 90 --memory_buffer_percentage 10&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. KEDA - Kubernetes Event-driven Autoscaling&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HPA는 리소스 메트릭 기반이다. KEDA는 여기에 이벤트 기반 스케일링을 추가한다. Airflow의 metadb에서 대기 중인 task 수를 보고 worker를 확장하거나 SQS 큐 깊이에 따라 처리 파드를 늘릴 수 있다. CPU 사용률이 올라가기 전에 미리 확장할 수 있어서 HPA보다 반응이 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KEDA는 세 가지 컴포넌트로 구성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;keda-operator&lt;/code&gt; - Deployment를 활성화/비활성화하여 0까지 스케일 다운&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keda-operator-metrics-apiserver&lt;/code&gt; - External Metrics API로 이벤트 데이터를 HPA에 노출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keda-admission-webhooks&lt;/code&gt; - 동일 타겟에 여러 ScaledObject가 걸리는 것을 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KEDA Cron 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KEDA를 설치하고 Cron 기반 스케일링을 테스트한다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --version 2.16.0 \
  --namespace keda --create-namespace -f keda-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ScaledObject를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron-scaled
spec:
  minReplicaCount: 0
  maxReplicaCount: 2
  pollingInterval: 30
  cooldownPeriod: 300
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 00,15,30,45 * * * *
      end: 05,20,35,50 * * * *
      desiredReplicas: &quot;1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ScaledObject가 기존 HPA를 대체하며 &lt;code&gt;pollingInterval&lt;/code&gt;(30초)마다 트리거 조건을 확인한다. &lt;code&gt;cooldownPeriod&lt;/code&gt;(300초)는 스케일 다운까지 대기 시간이다. Kafka trigger 같은 것도 지원해서 &lt;code&gt;lagThreshold&lt;/code&gt;를 기준으로 Consumer 파드를 스케일링할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽 패턴이 예측 가능한 서비스라면 Cron 트리거로 특정 시간대에 미리 파드를 확보하는 전략이 유용하다. Karpenter와 조합하면 해당 시간에 노드까지 함께 확장할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 노드 오토스케일링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod 스케일링만으로는 부족하다. Pod가 늘어나도 노드 자원이 부족하면 Pending 상태에 머문다. 노드 수를 자동으로 조절하는 방법을 살펴본다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 CA/CAS - Cluster Autoscaler&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CA는 쿠버네티스 기본 API의 일부가 아니라 선택적 컴포넌트다. Pending 상태인 파드가 있으면 워커 노드를 스케일 아웃하고 노드가 저활용 상태이면 스케일 인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서는 ASG(Auto Scaling Group)와 연동된다. Auto-Discovery 방식을 사용하면 태그 기반으로 ASG를 자동 인식한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# EKS 노드에 이미 아래 태그가 설정되어 있다
# k8s.io/cluster-autoscaler/enabled: true
# k8s.io/cluster-autoscaler/myeks: owned

# CA 배포
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
sed -i &quot;s/&amp;lt;YOUR CLUSTER NAME&amp;gt;/myeks/g&quot; cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CA의 스케일 다운 기본 대기 시간은 10분이다. &lt;code&gt;--scale-down-delay-after-add&lt;/code&gt; 플래그로 조정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CA + ASG 조합의 한계는 분명하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스 관리가 분리된다. ASG와 EKS가 각각 노드를 관리하므로 EKS에서 노드를 삭제해도 ASG의 인스턴스가 남아있을 수 있다&lt;/li&gt;
&lt;li&gt;노드 생성이 느리다. CA &amp;rarr; ASG &amp;rarr; EC2 Fleet API로 거치는 경로가 길다&lt;/li&gt;
&lt;li&gt;사전 정의된 노드 그룹에 의존하므로 인스턴스 타입이 동일해야 한다. 혼합 타입이면 CPU와 메모리가 균등해야 한다&lt;/li&gt;
&lt;li&gt;모범사례는 AZ당 노드 그룹을 갖는 것인데 관리할 노드 그룹이 많아진다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Karpenter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Karpenter는 CA의 한계를 해결하기 위해 등장했다. ASG를 거치지 않고 EC2 Fleet API를 직접 호출해서 노드를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 방식을 비교하면 차이가 뚜렷하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CA&lt;/b&gt;: Pending 파드 발견 &amp;rarr; ASG 조정 요청 &amp;rarr; EC2 Fleet API &amp;rarr; 노드 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Karpenter&lt;/b&gt;: Pending 파드 발견 &amp;rarr; Pod 스펙 평가 &amp;rarr; EC2 Fleet API 직접 호출 &amp;rarr; 노드 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASG를 거치지 않으니 속도가 빠르다. EC2 시작 템플릿도 필요 없고 NodePool CR이 ASG를 대체한다. GitOps 방식으로 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Karpenter v1에서 리소스 명칭이 바뀌었다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;이전(v1beta1)&lt;/th&gt;
&lt;th&gt;현재(v1)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Provisioner&lt;/td&gt;
&lt;td&gt;NodePool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWSNodeTemplate&lt;/td&gt;
&lt;td&gt;EC2NodeClass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine&lt;/td&gt;
&lt;td&gt;NodeClaim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 Karpenter 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Karpenter는 기존 myeks 클러스터가 아닌 별도의 전용 클러스터를 생성해서 실습한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 변수를 설정하고 CloudFormation으로 필요한 AWS 리소스를 만든다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;export KARPENTER_NAMESPACE=&quot;kube-system&quot;
export KARPENTER_VERSION=&quot;1.10.0&quot;
export K8S_VERSION=&quot;1.34&quot;
export CLUSTER_NAME=&quot;hackjap-karpenter-demo&quot;
export AWS_DEFAULT_REGION=&quot;ap-northeast-2&quot;
export AWS_ACCOUNT_ID=&quot;$(aws sts get-caller-identity --query Account --output text)&quot;
export TEMPOUT=&quot;$(mktemp)&quot;
export ALIAS_VERSION=&quot;$(aws ssm get-parameter --name &quot;/aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2023/x86_64/standard/recommended/image_id&quot; --query Parameter.Value | xargs aws ec2 describe-images --query 'Images[0].Name' --image-ids | sed -r 's/^.*(v[[:digit:]]+).*$/\1/')&quot;

# CloudFormation으로 IAM Role, Instance Profile 등 생성
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v&quot;${KARPENTER_VERSION}&quot;/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml &amp;gt; &quot;${TEMPOUT}&quot; \
&amp;amp;&amp;amp; aws cloudformation deploy \
  --stack-name &quot;Karpenter-${CLUSTER_NAME}&quot; \
  --template-file &quot;${TEMPOUT}&quot; \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides &quot;ClusterName=${CLUSTER_NAME}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eksctl로 EKS 클러스터를 생성한다. 모든 리소스에 &lt;code&gt;karpenter.sh/discovery&lt;/code&gt; 태그를 달아 Karpenter가 자신이 관리할 리소스를 식별하게 한다. Pod Identity addon도 함께 설치한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;eksctl create cluster -f - &amp;lt;&amp;lt;EOF
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: &quot;${K8S_VERSION}&quot;
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}

iam:
  withOIDC: true
  podIdentityAssociations:
  - namespace: &quot;${KARPENTER_NAMESPACE}&quot;
    serviceAccountName: karpenter
    roleName: ${CLUSTER_NAME}-karpenter
    permissionPolicyARNs:
    - arn:aws:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}

iamIdentityMappings:
- arn: &quot;arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}&quot;
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes

managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2023
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10

addons:
- name: eks-pod-identity-agent
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터가 생성되면 Helm으로 Karpenter를 설치한다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export CLUSTER_ENDPOINT=&quot;$(aws eks describe-cluster --name &quot;${CLUSTER_NAME}&quot; --query &quot;cluster.endpoint&quot; --output text)&quot;
export KARPENTER_IAM_ROLE_ARN=&quot;arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter&quot;

helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version &quot;${KARPENTER_VERSION}&quot; \
  --namespace &quot;${KARPENTER_NAMESPACE}&quot; --create-namespace \
  --set &quot;settings.clusterName=${CLUSTER_NAME}&quot; \
  --set &quot;settings.interruptionQueue=${CLUSTER_NAME}&quot; \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait

# 설치 확인
kubectl get crd | grep karpenter&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 NodePool과 EC2NodeClass 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가드레일 방식으로 인스턴스 조건만 선언하면 Karpenter가 워크로드에 맞는 최적 타입을 알아서 선택한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: [&quot;amd64&quot;]
        - key: kubernetes.io/os
          operator: In
          values: [&quot;linux&quot;]
        - key: karpenter.sh/capacity-type
          operator: In
          values: [&quot;on-demand&quot;]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: [&quot;c&quot;, &quot;m&quot;, &quot;r&quot;]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: [&quot;2&quot;]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: &quot;KarpenterNodeRole-${CLUSTER_NAME}&quot;
  amiSelectorTerms:
    - alias: &quot;al2023@${ALIAS_VERSION}&quot;
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: &quot;${CLUSTER_NAME}&quot;
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: &quot;${CLUSTER_NAME}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;karpenter.sh/discovery&lt;/code&gt; 태그로 Karpenter가 관리할 서브넷과 보안 그룹을 식별한다. 클러스터 생성 시 모든 리소스에 이 태그를 달아야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.5 Disruption - 노드 라이프사이클 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Karpenter의 Disruption은 노드를 적극적으로 관리하는 메커니즘이다. 조건이 충족되면 노드에 cordon을 설정하고 Pod를 안전하게 다른 노드로 옮긴 뒤 빈 노드를 종료한다. 세 가지 유형이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Expiration(만료)&lt;/b&gt; - 노드가 &lt;code&gt;expireAfter&lt;/code&gt;에 지정된 시간이 지나면 자동으로 교체된다. 기본값은 720시간(30일)이다. 오래된 노드를 주기적으로 교체해서 AMI 패치나 보안 업데이트를 자연스럽게 반영할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Drift(드리프트)&lt;/b&gt; - NodePool이나 EC2NodeClass 설정이 변경되면 기존 노드를 새 설정에 맞게 교체한다. AMI 업데이트나 보안 그룹 변경 시 자동으로 일관성을 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consolidation(통합)&lt;/b&gt; - 저활용 노드의 Pod를 다른 노드로 옮기고 빈 노드를 제거한다. Bin Packing 알고리즘으로 노드 수를 최소화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;consolidationPolicy&lt;/code&gt; 옵션은 세 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;WhenEmpty&lt;/code&gt; - 모든 워크로드가 제거된 경우에만 종료&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WhenUnderutilized&lt;/code&gt; - 리소스 사용률이 낮고 워크로드 이동이 가능하면 종료&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WhenEmptyOrUnderutilized&lt;/code&gt; - 가장 적극적인 정책&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.6 Over-Provisioning으로 스파이크 대응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CA든 Karpenter든 노드를 새로 프로비저닝하려면 시간이 걸린다. 기존 노드에 Pod를 추가하는 것보다 느리다. 이 지연을 해결하는 방법이 Over-Provisioning이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위가 낮은 placeholder Pod를 배포해서 여유 노드를 미리 확보해 둔다. 실제 워크로드가 확장되면 placeholder Pod가 선점(preempt)되고 그 자리에 실제 Pod가 들어간다. placeholder가 쫓겨나면 새 노드 프로비저닝이 시작되는데 이때는 이미 실제 워크로드는 기존 노드에서 돌고 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: placeholder-priority
value: -10
preemptionPolicy: Never
globalDefault: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: placeholder
spec:
  replicas: 10
  selector:
    matchLabels:
      pod: placeholder-pod
  template:
    metadata:
      labels:
        pod: placeholder-pod
    spec:
      priorityClassName: placeholder-priority
      terminationGracePeriodSeconds: 0
      containers:
      - name: ubuntu
        image: ubuntu
        command: [&quot;sleep&quot;]
        args: [&quot;infinity&quot;]
        resources:
          requests:
            cpu: 200m
            memory: 250Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;placeholder Pod의 리소스 크기는 실제 워크로드의 가장 큰 Pod 크기 이상이어야 한다. 그렇지 않으면 placeholder가 선점되어도 실제 Pod가 그 자리에 들어가지 못한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 스케일링 전략 비교&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;적합한 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HPA&lt;/td&gt;
&lt;td&gt;Pod&lt;/td&gt;
&lt;td&gt;수평 확장&lt;/td&gt;
&lt;td&gt;CPU/Memory 기반 Stateless 워크로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPA&lt;/td&gt;
&lt;td&gt;Pod&lt;/td&gt;
&lt;td&gt;수직 확장&lt;/td&gt;
&lt;td&gt;리소스 요청값 튜닝 (HPA와 병용 불가)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KEDA&lt;/td&gt;
&lt;td&gt;Pod&lt;/td&gt;
&lt;td&gt;이벤트 기반 수평 확장&lt;/td&gt;
&lt;td&gt;메시지 큐, Cron, 외부 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CA/CAS&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;ASG 연동&lt;/td&gt;
&lt;td&gt;기존 ASG 기반 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Karpenter&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;EC2 직접 호출&lt;/td&gt;
&lt;td&gt;빠른 스케일링과 비용 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 도구마다 대상과 방식이 다르기 때문에&amp;nbsp; 워크로드 특성과 트래픽 패턴에 맞게 조합해서 사용하는 것이 중요하다.&lt;/p&gt;</description>
      <category>AWS/EKS</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/106</guid>
      <comments>https://hackjsp.tistory.com/106#entry106comment</comments>
      <pubDate>Fri, 3 Apr 2026 19:10:46 +0900</pubDate>
    </item>
    <item>
      <title>[ AEWS4 ] EKS 네트워킹 살펴보기</title>
      <link>https://hackjsp.tistory.com/105</link>
      <description>&lt;h2 data-heading=&quot;실습 환경&quot; data-ke-size=&quot;size26&quot;&gt;실습 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습은 Terraform으로 배포한 EKS 클러스터에서 진행했다. 퍼블릭 서브넷 3개에 t3.medium 워커 노드 3대를 배치하고, VPC CNI의 다양한 IP 할당 모드와 로드밸런서 동작을 확인한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;클러스터&lt;/td&gt;
&lt;td&gt;myeks (EKS, Terraform 배포)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리전&lt;/td&gt;
&lt;td&gt;ap-northeast-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC CIDR&lt;/td&gt;
&lt;td&gt;192.168.0.0/16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서브넷&lt;/td&gt;
&lt;td&gt;퍼블릭 3개 + 프라이빗 3개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노드 그룹&lt;/td&gt;
&lt;td&gt;t3.medium &amp;times; 3대 (ENI 3개, ENI당 IP 6개)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CNI&lt;/td&gt;
&lt;td&gt;AWS VPC CNI (기본 설치)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 배포 상태는 아래 명령으로 확인할 수 있다:&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 노드 확인
aws ec2 describe-instances --query &quot;Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}&quot; \
  --filters Name=instance-state-name,Values=running --output table
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 클러스터 구성은 다음과 같다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOKXU9/dJMcadg6Slx/Vzy4AOrBzkT4YErYZc6SRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOKXU9/dJMcadg6Slx/Vzy4AOrBzkT4YErYZc6SRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOKXU9/dJMcadg6Slx/Vzy4AOrBzkT4YErYZc6SRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOKXU9%2FdJMcadg6Slx%2FVzy4AOrBzkT4YErYZc6SRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1628&quot; height=&quot;746&quot; data-origin-width=&quot;1628&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-heading=&quot;1. AWS VPC CNI&quot; data-ke-size=&quot;size26&quot;&gt;1. AWS VPC CNI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS VPC CNI는 EKS 클러스터의 기본 네트워킹 Add-on이다. 클러스터를 프로비저닝하면 자동으로 설치된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 특징은 &lt;b&gt;파드의 IP가 노드와 동일한 VPC 대역을 사용&lt;/b&gt;한다는 것이다. 보통 CNI는 Overlay 네트워크를 별도로 구성하지만, VPC CNI는 파드에 실제 VPC IP를 할당한다. NAT 없이 직접 통신이 가능하고 VPC Flow Logs, Security Group, 라우팅 정책 같은 AWS 네이티브 기능도 파드 수준에서 그대로 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Overlay CNI (Calico, Flannel 등) AWS VPC CNI&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;파드 IP 대역&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;별도 가상 네트워크 (예: 10.244.0.0/16)&lt;/td&gt;
&lt;td&gt;VPC 실제 대역 (예: 192.168.x.x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;통신 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;VXLAN/IPIP 캡슐화&lt;/td&gt;
&lt;td&gt;VPC 라우팅 직접 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;NAT&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파드 &amp;rarr; 외부 통신 시 SNAT 필요&lt;/td&gt;
&lt;td&gt;NAT 없이 직접 통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크 성능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;캡슐화 오버헤드 존재&lt;/td&gt;
&lt;td&gt;네이티브 VPC 수준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;관찰성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;별도 모니터링 필요&lt;/td&gt;
&lt;td&gt;VPC Flow Logs로 파드 트래픽 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;보안그룹&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;노드 단위 적용&lt;/td&gt;
&lt;td&gt;파드 단위 적용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;VPC CNI에서 파드는 노드와 같은 VPC 대역의 IP를 할당받는다&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;1248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7UwHV/dJMcaadDuMa/sJHPFNV5RmovSjarFUdlXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7UwHV/dJMcaadDuMa/sJHPFNV5RmovSjarFUdlXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7UwHV/dJMcaadDuMa/sJHPFNV5RmovSjarFUdlXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7UwHV%2FdJMcaadDuMa%2FsJHPFNV5RmovSjarFUdlXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1392&quot; height=&quot;1248&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;1248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;1.1 구성요소&quot; data-ke-size=&quot;size23&quot;&gt;1.1 구성요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CNI 바이너리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파드에 VPC IP를 실제로 할당하는 역할&lt;/li&gt;
&lt;li&gt;워커 노드의 /opt/cni/bin/aws-cni에 위치&lt;/li&gt;
&lt;li&gt;설정 파일은 /etc/cni/net.d/10-aws.conflist에서 확인 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IPAMD (IP Address Management Daemon)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 노드에서 ENI를 관리하고, IP 주소의 Warm Pool을 유지&lt;/li&gt;
&lt;li&gt;기본값 WARM_ENI_TARGET=1로, 여분의 ENI 1개를 항상 대기시켜 파드 생성 시 즉시 IP를 할당&lt;/li&gt;
&lt;li&gt;파드가 생성될 때 빠르게 IP를 할당할 수 있도록 ENI와 IP 주소를 미리 사전 할당(pre-allocation)해 두는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-heading=&quot;1.2 IP 할당 모드&quot; data-ke-size=&quot;size23&quot;&gt;1.2 IP 할당 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC CNI는 파드에 IP를 할당하는 모드가 3가지 있고, 모드에 따라 노드당 파드 집적도와 IP 관리 방식이 달라진다.&lt;/p&gt;
&lt;h4 data-heading=&quot;1.2.1 Secondary IP (기본 설정)&quot; data-ke-size=&quot;size20&quot;&gt;1.2.1 Secondary IP (기본 설정)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 기본 설정에서 파드는 노드(EC2 인스턴스)에 부착된 ENI(Elastic Network Interface)의 Secondary IP를 하나씩 할당받아 통신한다. 중요한 건 EC2 인스턴스 타입마다 부착 가능한 &lt;b&gt;최대 ENI 수&lt;/b&gt;와 &lt;b&gt;ENI당 할당 가능한 Secondary IP 수&lt;/b&gt;가 하드웨어 스펙으로 고정되어 있다는 것이다. 인스턴스 타입을 선택하는 순간 그 노드에서 실행 가능한 파드 수의 상한이 결정된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;942&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfx7JV/dJMb99MwkRv/MxjdSKEYOlH2YXCMEYadXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfx7JV/dJMb99MwkRv/MxjdSKEYOlH2YXCMEYadXk/img.png&quot; data-alt=&quot;인스턴스 타입별 ENI 및 IP 할당 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfx7JV/dJMb99MwkRv/MxjdSKEYOlH2YXCMEYadXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdfx7JV%2FdJMb99MwkRv%2FMxjdSKEYOlH2YXCMEYadXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1374&quot; height=&quot;942&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;942&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인스턴스 타입별 ENI 및 IP 할당 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파드 최대 개수 공식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;파드 개수 = (최대 ENI 수) &amp;times; (ENI당 Secondary IP 수 - 1) + 2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-1: 각 ENI의 Primary IP는 노드 자체의 통신에 사용되므로 파드에 할당할 수 없다&lt;/li&gt;
&lt;li&gt;+2: aws-node와 kube-proxy 같은 호스트 네트워크를 사용하는 시스템 파드 여유분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;t3.medium 기준 계산:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 값&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;최대 ENI 수&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENI당 IP 수&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최대 파드 수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;3 &amp;times; (6 - 1) + 2 = 17&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 t3.medium 노드에서 3개 ENI에 Secondary IP가 할당되는 구조다. 각 ENI의 Primary IP 1개는 노드가 사용하고, 나머지 5개가 파드에 할당된다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ENI 1 (Primary IP: 노드 사용)
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  └── Secondary IP &amp;rarr; Pod

ENI 2
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  └── Secondary IP &amp;rarr; Pod

ENI 3
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  ├── Secondary IP &amp;rarr; Pod
  └── Secondary IP &amp;rarr; Pod
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secondary IP의 최대 개수는 전적으로 EC2 인스턴스의 스펙(타입)에 달려있다. 인스턴스가 클수록 더 많은 ENI를 붙일 수 있고, ENI당 더 많은 Secondary IP를 가질 수 있다. 인스턴스 타입별 네트워크 스펙은 아래 명령으로 확인할 수 있다:&lt;/p&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
 --query &quot;InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}&quot; \
 --output table
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;+----------+----------+--------------+
| IPv4addr | MaxENI   |    Type      |
+----------+----------+--------------+
|  15      |  4       |  t3.2xlarge  |
|  6       |  3       |  t3.medium   |
|  12      |  3       |  t3.large    |
|  15      |  4       |  t3.xlarge   |
|  2       |  2       |  t3.micro    |
|  2       |  2       |  t3.nano     |
|  4       |  3       |  t3.small    |
+----------+----------+--------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한계를 직접 확인해보자. nginx deployment의 replicas를 50개로 늘리면, 17개를 초과하는 파드는 IP를 할당받지 못하고 Pending 상태에 빠진다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl scale deployment nginx-deployment --replicas=50
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIec5j/dJMcac3x8oD/yeGtjdZHOAtbqw7MvlbjXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIec5j/dJMcac3x8oD/yeGtjdZHOAtbqw7MvlbjXK/img.png&quot; data-alt=&quot;17개를 초과하는 파드는 Pending 상태에 머문다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIec5j/dJMcac3x8oD/yeGtjdZHOAtbqw7MvlbjXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIec5j%2FdJMcac3x8oD%2FyeGtjdZHOAtbqw7MvlbjXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1374&quot; height=&quot;424&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;17개를 초과하는 파드는 Pending 상태에 머문다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZDN6v/dJMcadOTds1/Sa11PjxjlBrDcDBf8Malj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZDN6v/dJMcadOTds1/Sa11PjxjlBrDcDBf8Malj0/img.png&quot; data-alt=&quot;Pending 파드의 이벤트 &amp;amp;mdash; 할당 가능한 IP가 소진되어 스케줄링에 실패했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZDN6v/dJMcadOTds1/Sa11PjxjlBrDcDBf8Malj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZDN6v%2FdJMcadOTds1%2FSa11PjxjlBrDcDBf8Malj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2856&quot; height=&quot;738&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pending 파드의 이벤트 &amp;mdash; 할당 가능한 IP가 소진되어 스케줄링에 실패했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Secondary IP 방식에서는 인스턴스 스펙이 파드 수의 물리적 한계가 된다. 이 제약을 풀려면 CNI 설정 튜닝이나 Prefix Delegation을 적용해야 한다.&lt;/p&gt;
&lt;h4 data-heading=&quot;1.2.2 VPC CNI 설정 튜닝&quot; data-ke-size=&quot;size20&quot;&gt;1.2.2 VPC CNI 설정 튜닝&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정인 WARM_ENI_TARGET=1은 IP가 1개라도 필요하면 ENI 전체를 새로 생성한다. 이는 IP를 많이 사용하지 않는 노드에서 불필요한 ENI가 할당되는 원인이 된다. IP 단위로 더 세밀하게 제어하려면 다음 환경변수를 사용한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WARM_IP_TARGET&lt;/td&gt;
&lt;td&gt;항상 확보해둘 여유 IP 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MINIMUM_IP_TARGET&lt;/td&gt;
&lt;td&gt;노드에서 최소 유지할 IP 수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 MINIMUM_IP_TARGET=10으로 설정하면, 최초에 10개의 IP를 미리 할당하므로 ENI도 2개가 생성된다. 파드 생성이 빈번한 워크로드에서 IP 할당 지연을 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정은 aws-node DaemonSet의 환경변수로 적용한다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl set env daemonset aws-node -n kube-system WARM_IP_TARGET=5 MINIMUM_IP_TARGET=10
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-heading=&quot;1.2.3 Prefix Delegation (접두사 위임)&quot; data-ke-size=&quot;size20&quot;&gt;1.2.3 Prefix Delegation (접두사 위임)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secondary IP에서는 ENI 슬롯 1개에 IP 1개만 들어간다. Prefix Delegation은 이 슬롯 하나에 &lt;b&gt;/28 크기의 IP 블록(16개)&lt;/b&gt;을 통째로 넣는 방식이다. 슬롯당 16배의 IP를 확보하니 노드당 파드 집적도가 크게 올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용 조건:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS Nitro 기반 인스턴스 (m5 이상) &amp;mdash; Prefix Delegation은 Nitro 시스템의 ENI에서만 지원하는 기능&lt;/li&gt;
&lt;li&gt;ENABLE_PREFIX_DELEGATION=true 설정&lt;/li&gt;
&lt;li&gt;kubelet --max-pods 값 조정 필요 (인스턴스 타입별로 할당 가능한 접두사 수가 다름)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prefix Delegation을 활성화하면 인스턴스 타입의 ENI/IP 스펙에 구애받지 않고 &lt;b&gt;권장 한도 110개&lt;/b&gt;까지 파드를 배치할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prefix 할당 상태는 다음 명령으로 확인한다:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;aws ec2 describe-instances \
  --filters &quot;Name=tag-key,Values=eks:cluster-name&quot; &quot;Name=tag-value,Values=myeks&quot; \
  --query 'Reservations[*].Instances[].{InstanceId: InstanceId, Prefixes: NetworkInterfaces[].Ipv4Prefixes[]}' | jq
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  { &quot;InstanceId&quot;: &quot;i-06bb745636f1a2cd2&quot;, &quot;Prefixes&quot;: [{ &quot;Ipv4Prefix&quot;: &quot;192.168.5.160/28&quot; }] },
  { &quot;InstanceId&quot;: &quot;i-0cabf55bffcdc3004&quot;, &quot;Prefixes&quot;: [{ &quot;Ipv4Prefix&quot;: &quot;192.168.3.176/28&quot; }] },
  { &quot;InstanceId&quot;: &quot;i-09ee8a731b20eff8f&quot;, &quot;Prefixes&quot;: [{ &quot;Ipv4Prefix&quot;: &quot;192.168.9.32/28&quot; }] }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 노드에 /28 블록이 할당된 것을 확인할 수 있다. /28은 16개의 IP를 포함하므로, 기존 Secondary IP 방식에서 슬롯당 1개였던 것이 16개로 늘어난 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: Node가 아닌 Pod가 IMDS로 메타데이터(VPC ID 등)를 가져올 수 있도록 &lt;b&gt;IMDS hop limit을 2로 설정&lt;/b&gt;해야 한다. EKS 모듈로 배포 시 관리형 노드 그룹의 시작 템플릿에 http_put_response_hop_limit = 2를 적용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LJyXm/dJMcacoYxHg/LjuyJKRLz2SIa6u0zzQiek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LJyXm/dJMcacoYxHg/LjuyJKRLz2SIa6u0zzQiek/img.png&quot; data-alt=&quot;IMDS hop limit 설정 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LJyXm/dJMcacoYxHg/LjuyJKRLz2SIa6u0zzQiek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLJyXm%2FdJMcacoYxHg%2FLjuyJKRLz2SIa6u0zzQiek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1390&quot; height=&quot;574&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;IMDS hop limit 설정 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-heading=&quot;1.2.4 커스텀 네트워킹&quot; data-ke-size=&quot;size20&quot;&gt;1.2.4 커스텀 네트워킹&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prefix Delegation이 노드당 파드 집적도를 높이는 기능이라면, 커스텀 네트워킹은 &lt;b&gt;파드가 IP를 할당받는 서브넷 자체를 노드와 분리&lt;/b&gt;하는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 상황:&lt;/b&gt; K8s 환경에서 파드가 수시로 생성/삭제되면서 대량의 IP를 소모한다. 노드와 파드가 같은 VPC 대역을 사용하면, 새로운 노드나 RDS 같은 AWS 리소스를 띄울 IP가 부족해지는 &lt;b&gt;VPC IP 고갈&lt;/b&gt;이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; VPC에 파드 전용 &lt;b&gt;Secondary CIDR&lt;/b&gt;을 추가한다. 주로 내부 통신 전용인 100.64.0.0/10(CGNAT) 대역을 사용하여 기존 VPC 대역과의 충돌을 회피한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;┌─── VPC ──────────────────────────────────────────────┐
│                                                      │
│  ┌─ Primary CIDR: 192.168.0.0/16 ─────────────────┐ │
│  │                                                  │ │
│  │   [Node]         [RDS]         [ALB]             │ │
│  │     │                                            │ │
│  │     ├── ENI (Primary)  &amp;rarr; 192.168.x.x            │ │
│  │     │                                            │ │
│  └─────┼────────────────────────────────────────────┘ │
│        │                                              │
│        └── ENI (Secondary)                            │
│             │                                         │
│  ┌─ Secondary CIDR: 100.64.0.0/16 (파드 전용) ─────┐ │
│  │         &amp;darr;                                        │ │
│  │   [Pod] [Pod] [Pod] ...  &amp;rarr; 100.64.x.x           │ │
│  │                                                  │ │
│  └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 파드가 아무리 많은 IP를 소모해도 노드, RDS, ALB 등 다른 AWS 리소스의 IP 대역에는 영향을 주지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;2. Service &amp;amp; 로드밸런싱&quot; data-ke-size=&quot;size26&quot;&gt;2. Service &amp;amp; 로드밸런싱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC CNI로 파드에 IP가 할당되었다면, 다음 단계는 이 파드들을 외부에 노출하는 것이다. 쿠버네티스는 Service 리소스를 통해 파드 그룹에 안정적인 네트워크 엔드포인트를 제공한다.&lt;/p&gt;
&lt;h3 data-heading=&quot;2.1 쿠버네티스 서비스 타입&quot; data-ke-size=&quot;size23&quot;&gt;2.1 쿠버네티스 서비스 타입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ClusterIP&lt;/b&gt;: 클러스터 내부 통신 전용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NodePort&lt;/b&gt;: 노드의 정적 포트로 외부 노출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LoadBalancer&lt;/b&gt;: 클라우드 프로바이더의 로드밸런서와 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;2.2 AWS Load Balancer Controller&quot; data-ke-size=&quot;size23&quot;&gt;2.2 AWS Load Balancer Controller&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS에서 외부 트래픽을 파드로 연결하는 컨트롤러다. Service나 Ingress 리소스에 어노테이션을 달아두면 컨트롤러가 이를 감지해서 목적에 맞는 AWS 로드밸런서를 자동으로 만들고 파드와 연결해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 생성되는 LB 용도&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service (type: LoadBalancer)&lt;/td&gt;
&lt;td&gt;NLB&lt;/td&gt;
&lt;td&gt;TCP/UDP L4 트래픽&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;ALB&lt;/td&gt;
&lt;td&gt;HTTP/HTTPS L7 트래픽&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Cloud Controller Manager(CCM)도 CLB/NLB를 프로비저닝할 수 있지만, 현재는 거의 사용되지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-heading=&quot;2.2.1 설치 및 구성&quot; data-ke-size=&quot;size20&quot;&gt;2.2.1 설치 및 구성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS LB Controller가 AWS 리소스를 생성하려면 적절한 IAM 권한이 필요하다. 이번 실습에서는 &lt;b&gt;IRSA&lt;/b&gt;(IAM Roles for Service Accounts)를 사용했다. IRSA는 ServiceAccount에 IAM 역할을 바인딩하여 해당 파드에만 최소 권한을 부여하는 방식으로, Pod Identity나 Instance Profile(취약) 대비 가장 보편적으로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 과정의 핵심 포인트:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;OIDC Provider 확인&lt;/b&gt; &amp;mdash; EKS 클러스터에 OIDC가 연결되어 있어야 IRSA가 동작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서브넷 태그 확인&lt;/b&gt; &amp;mdash; LB Controller는 kubernetes.io/role/elb: 1 태그가 붙은 서브넷을 자동으로 탐색하여 LB를 배치한다. 이 태그가 없으면 컨트롤러가 대상 서브넷을 찾지 못해 LB 생성에 실패한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IRSA 생성&lt;/b&gt; &amp;mdash; LB Controller용 ServiceAccount에 IAM 역할 바인딩&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Helm으로 설치&lt;/b&gt; &amp;mdash; eks/aws-load-balancer-controller 차트 배포&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;# 서브넷 태그 확인 예시
+-------------------------------------------------------+------------------+
|  Key                                                  |  Value           |
+-------------------------------------------------------+------------------+
|  kubernetes.io/role/elb                               |  1               |
+-------------------------------------------------------+------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Helm으로 설치
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.name=aws-load-balancer-controller \
  --set serviceAccount.create=false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;2.3 NLB (Network Load Balancer)&quot; data-ke-size=&quot;size23&quot;&gt;2.3 NLB (Network Load Balancer)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NLB 생성 시 타겟 유형을 지정해야 하며, 두 가지 모드가 있다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인스턴스 모드:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Client &amp;rarr; NLB &amp;rarr; Node(EC2) &amp;rarr; kube-proxy &amp;rarr; [다른 노드로 포워딩] &amp;rarr; Pod
                  &amp;uarr;                            &amp;uarr;
              1차 홉                        2차 홉 (파드가 다른 노드에 있을 때)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 노드를 경유하면서 불필요한 네트워크 홉이 발생하고, 목적지 파드가 다른 노드에 있으면 추가 점프가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IP 모드:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client &amp;rarr; NLB &amp;rarr; Pod (직접 연결)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC CNI 덕에 파드가 실제 VPC IP를 갖고 있으니 NLB가 &lt;b&gt;파드 IP로 바로 트래픽을 전달&lt;/b&gt;할 수 있다. 경유지가 없어지면서 지연 시간도 줄어든다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 대부분 &lt;b&gt;IP 모드&lt;/b&gt;를 기본으로 사용한다. 단, IP 모드는 반드시 AWS Load Balancer Controller와 IAM 정책 설정이 필요하다. LB Controller 없이 CCM(Cloud Controller Manager)만으로 운영하는 경우에는 인스턴스 모드를 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFH7ja/dJMcaf63xFU/8mDrzbiEu9mYHxMCKdF531/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFH7ja/dJMcaf63xFU/8mDrzbiEu9mYHxMCKdF531/img.png&quot; data-alt=&quot;NLB 대상 그룹 &amp;amp;mdash; IP 타겟으로 파드가 직접 등록된 것을 확인할 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFH7ja/dJMcaf63xFU/8mDrzbiEu9mYHxMCKdF531/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFH7ja%2FdJMcaf63xFU%2F8mDrzbiEu9mYHxMCKdF531%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1390&quot; height=&quot;678&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NLB 대상 그룹 &amp;mdash; IP 타겟으로 파드가 직접 등록된 것을 확인할 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2rCS5/dJMcaiWY2KP/THItgN9oQcKKb1O6fIrrUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2rCS5/dJMcaiWY2KP/THItgN9oQcKKb1O6fIrrUK/img.png&quot; data-alt=&quot;대상 그룹의 드레이닝 설정 &amp;amp;mdash; 파드 종료 시 기존 연결을 안전하게 종료하기 위한 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2rCS5/dJMcaiWY2KP/THItgN9oQcKKb1O6fIrrUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2rCS5%2FdJMcaiWY2KP%2FTHItgN9oQcKKb1O6fIrrUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1384&quot; height=&quot;580&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대상 그룹의 드레이닝 설정 &amp;mdash; 파드 종료 시 기존 연결을 안전하게 종료하기 위한 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-heading=&quot;2.4 ALB와 Ingress (L7)&quot; data-ke-size=&quot;size23&quot;&gt;2.4 ALB와 Ingress (L7)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress는 클러스터 내부 서비스를 외부로 노출하는 L7 리소스다. AWS LB Controller가 Ingress를 감지하면 ALB를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ingress가 제공하는 기능:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;호스트 기반 라우팅&lt;/b&gt;: 도메인별로 다른 서비스로 라우팅 (api.example.com &amp;rarr; api-service)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 기반 라우팅&lt;/b&gt;: URL 경로별로 다른 서비스로 라우팅 (/api &amp;rarr; api-service, /web &amp;rarr; web-service)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTPS 처리&lt;/b&gt;: 어노테이션으로 AWS ACM 인증서를 등록하고, ALB에서 SSL Termination 수행. 외부는 HTTPS, 내부 파드 통신은 HTTP로 처리하여 부하를 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-heading=&quot;2.4.1 IngressGroup으로 비용 절감&quot; data-ke-size=&quot;size20&quot;&gt;2.4.1 IngressGroup으로 비용 절감&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Ingress 리소스 1개당 ALB 1개가 생성된다. 서비스가 10개면 ALB도 10개 &amp;mdash; 비용이 급증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;alb.ingress.kubernetes.io/group.name 어노테이션으로 &lt;b&gt;여러 Ingress를 하나의 ALB로 병합&lt;/b&gt;할 수 있다:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# Ingress 1: API 서비스
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    alb.ingress.kubernetes.io/group.name: my-team
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80
---
# Ingress 2: Web 서비스 (같은 group.name)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    alb.ingress.kubernetes.io/group.name: my-team
spec:
  rules:
    - host: web.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-service
                port:
                  number: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 Ingress가 같은 group.name: my-team을 사용하므로, ALB는 1개만 생성된다. 같은 포트(80)의 리스너 안에 도메인별 리스너 규칙이 추가되어 api.example.com은 api-service로, web.example.com은 web-service로 트래픽을 분배한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;3. ExternalDNS&quot; data-ke-size=&quot;size26&quot;&gt;3. ExternalDNS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExternalDNS는 Service, Ingress, Gateway API 리소스에 도메인을 설정하면 해당 DNS 레코드를 클라우드 DNS 서비스(AWS Route53, Azure DNS, GCP Cloud DNS 등)에 &lt;b&gt;자동으로 생성/삭제&lt;/b&gt;해주는 컨트롤러다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NLB를 만들고 tetris.hackjap.link 도메인을 연결하고 싶다면 Route53 콘솔에서 A 레코드를 직접 만들 필요 없이 어노테이션 한 줄이면 된다.&lt;/p&gt;
&lt;h3 data-heading=&quot;3.1 설치 및 구성&quot; data-ke-size=&quot;size23&quot;&gt;3.1 설치 및 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExternalDNS가 Route53 레코드를 관리하려면 IAM 권한이 필요하다. LB Controller와 마찬가지로 IRSA로 설정한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;IAM 정책 생성&lt;/b&gt; &amp;mdash; route53:ChangeResourceRecordSets, route53:ListHostedZones 등 Route53 관련 권한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IRSA 생성&lt;/b&gt; &amp;mdash; external-dns ServiceAccount에 IAM 역할 바인딩&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Helm으로 설치&lt;/b&gt; &amp;mdash; 주요 설정값:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;provider: aws
serviceAccount:
  create: false
  name: external-dns

# 관리 대상 도메인 필터 (보안상 권장)
domainFilters:
  - hackjap.link

# 레코드 동기화 정책
# sync: K8s에서 삭제 시 Route53에서도 자동 삭제
# upsert-only: 생성/수정만 하고 삭제는 수동 (안전)
policy: sync

# 감시 대상 리소스
sources:
  - service
  - ingress

# 여러 클러스터가 동일 도메인 관리 시 충돌 방지용 식별자
txtOwnerId: &quot;myeks-cluster&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# Helm으로 설치
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
helm install external-dns external-dns/external-dns \
  -n kube-system \
  -f external-dns-values.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;3.2 NLB + ExternalDNS 연동 실습&quot; data-ke-size=&quot;size23&quot;&gt;3.2 NLB + ExternalDNS 연동 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테트리스 게임을 배포하고, NLB에 도메인을 연결하는 실습이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 테트리스 Deployment + NLB Service 배포
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tetris
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tetris
  template:
    metadata:
      labels:
        app: tetris
    spec:
      containers:
      - name: tetris
        image: bsord/tetris
---
apiVersion: v1
kind: Service
metadata:
  name: tetris
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: &quot;true&quot;
spec:
  selector:
    app: tetris
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: LoadBalancer
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NLB가 생성된 후, 어노테이션으로 도메인을 연결한다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# ExternalDNS로 도메인 연결
kubectl annotate service tetris &quot;external-dns.alpha.kubernetes.io/hostname=tetris.$MyDomain&quot;

# Route53에 A 레코드가 자동 생성되었는지 확인
aws route53 list-resource-record-sets --hosted-zone-id &quot;${MyDnzHostedZoneId}&quot; \
  --query &quot;ResourceRecordSets[?Type == 'A']&quot; | jq

# DNS 전파 확인
dig +short tetris.$MyDomain @8.8.8.8
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExternalDNS 로그에서 레코드 생성 과정을 실시간으로 확인할 수 있다:&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;kubectl logs deploy/external-dns -n kube-system -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스를 삭제하면(kubectl delete deploy,svc tetris), policy: sync 설정에 의해 Route53의 A 레코드도 자동으로 삭제된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;4. Gateway API&quot; data-ke-size=&quot;size26&quot;&gt;4. Gateway API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway API는 쿠버네티스의 차세대 트래픽 라우팅 표준이다. Ingress의 한계(어노테이션 의존, 프로바이더마다 다른 비표준 설정)를 해결하려고 만들어졌고, nginx ingress controller 지원 종료와 맞물려 전환이 빨라지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress가 하나의 리소스에 모든 설정을 몰아넣었다면 Gateway API는 &lt;b&gt;역할별로 리소스를 분리&lt;/b&gt;해서 관심사를 나눈다.&lt;/p&gt;
&lt;h3 data-heading=&quot;4.1 리소스 구조&quot; data-ke-size=&quot;size23&quot;&gt;4.1 리소스 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway API는 여러 CRD로 구성되며, 각 리소스가 계층적으로 연결된다:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;LoadBalancerConfiguration    &amp;larr; LB 세부 설정 (scheme: internet-facing 등)
        &amp;uarr;
  GatewayClass               &amp;larr; 구현체 정의 (ALB 사용)
        &amp;uarr;
    Gateway                  &amp;larr; 진입점 정의 (Listener: 포트, 프로토콜)
        &amp;uarr;
   HTTPRoute                 &amp;larr; 라우팅 규칙 (호스트, 경로 &amp;rarr; 서비스 매핑)

TargetGroupConfiguration     &amp;larr; 타겟 그룹 설정 (targetType: ip)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 역할 주요 설정&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LoadBalancerConfiguration&lt;/td&gt;
&lt;td&gt;ALB/NLB 프로비저닝 설정&lt;/td&gt;
&lt;td&gt;scheme: internet-facing (기본값은 internal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GatewayClass&lt;/td&gt;
&lt;td&gt;어떤 구현체(provider)를 사용할지 정의&lt;/td&gt;
&lt;td&gt;LoadBalancerConfiguration 참조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway&lt;/td&gt;
&lt;td&gt;실제 진입점 (LB 생성)&lt;/td&gt;
&lt;td&gt;GatewayClass 참조, Listener 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPRoute&lt;/td&gt;
&lt;td&gt;L7 라우팅 규칙&lt;/td&gt;
&lt;td&gt;호스트/경로 기반 &amp;rarr; 백엔드 서비스 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TargetGroupConfiguration&lt;/td&gt;
&lt;td&gt;AWS 타겟 그룹 세부 설정&lt;/td&gt;
&lt;td&gt;targetType: ip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-heading=&quot;4.2 설치 및 구성&quot; data-ke-size=&quot;size23&quot;&gt;4.2 설치 및 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway API 사용을 위해서는 CRD 설치, LB Controller의 feature flag 활성화, ExternalDNS의 source 추가 3단계가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. CRD 설치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 Gateway API CRD와 AWS LB Controller 전용 CRD를 각각 설치한다:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Gateway API 표준 CRD (필수)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml

# Gateway API Experimental CRD (L4 Route 사용 시 필요)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/experimental-install.yaml

# AWS LB Controller 전용 CRD (LoadBalancerConfiguration, TargetGroupConfiguration 등)
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/config/crd/gateway/gateway-crds.yaml

# 설치 확인
kubectl get crd | grep gateway.networking   # 표준 CRD
kubectl get crd | grep gateway.k8s.aws      # AWS 전용 CRD
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. LB Controller에 Gateway API feature flag 활성화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 LB Controller는 Gateway API CRD를 감시하지 않는다. Deployment에 feature flag를 추가해야 한다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# LBC v2.13.0 이상 필요
kubectl describe pod -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller | grep Image: | uniq

# feature flag 활성화 (args에 추가)
KUBE_EDITOR=&quot;nano&quot; kubectl edit deploy -n kube-system aws-load-balancer-controller
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;# deployment args에 아래 줄 추가
- --feature-gates=NLBGatewayAPI=true,ALBGatewayAPI=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. ExternalDNS에 Gateway API source 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExternalDNS가 Gateway API 리소스의 도메인도 감지하도록 source를 추가한다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# external-dns-values.yaml에 sources 추가
sources:
  - service
  - ingress
  - gateway-httproute
  - gateway-grpcroute
  - gateway-tlsroute
  - gateway-tcproute
  - gateway-udproute
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;helm upgrade -i external-dns external-dns/external-dns -n kube-system -f external-dns-values.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;4.3 실습: 2048 게임 배포&quot; data-ke-size=&quot;size23&quot;&gt;4.3 실습: 2048 게임 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway API의 전체 리소스 생성 흐름을 2048 게임 배포로 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoadBalancerConfiguration &amp;rarr; GatewayClass &amp;rarr; Gateway 생성:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 1. LoadBalancerConfiguration: 외부 접근 가능하도록 internet-facing 설정
apiVersion: gateway.k8s.aws/v1beta1
kind: LoadBalancerConfiguration
metadata:
  name: lbc-config
spec:
  scheme: internet-facing
---
# 2. GatewayClass: ALB 구현체 지정 + LoadBalancerConfiguration 참조
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: aws-alb
spec:
  controllerName: gateway.k8s.aws/alb
  parametersRef:
    group: gateway.k8s.aws
    kind: LoadBalancerConfiguration
    name: lbc-config
    namespace: default
---
# 3. Gateway: HTTP 80 포트 리스너 생성 (여기서 ALB가 실제 생성됨)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: alb-http
spec:
  gatewayClassName: aws-alb
  listeners:
  - name: http
    protocol: HTTP
    port: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Gateway 생성 확인 &amp;mdash; ALB DNS가 ADDRESS에 표시된다
kubectl get gateways
# NAME       CLASS     ADDRESS                                         PROGRAMMED   AGE
# alb-http   aws-alb   k8s-default-albhttp-xxx.ap-northeast-2.elb...  Unknown      24s
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션 + TargetGroupConfiguration + HTTPRoute 생성:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 4. 2048 게임 Deployment + ClusterIP Service
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-2048
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: app-2048
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app-2048
    spec:
      containers:
      - image: public.ecr.aws/l6m2t8p7/docker-2048:latest
        name: app-2048
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: service-2048
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: app-2048
  ports:
  - port: 80
    targetPort: 80
---
# 5. TargetGroupConfiguration: IP 모드로 타겟 그룹 설정
apiVersion: gateway.k8s.aws/v1beta1
kind: TargetGroupConfiguration
metadata:
  name: backend-tg-config
spec:
  targetReference:
    name: service-2048
  defaultConfiguration:
    targetType: ip
    protocol: HTTP
---
# 6. HTTPRoute: 도메인 &amp;rarr; 서비스 라우팅
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: alb-http-route
spec:
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: alb-http
    sectionName: http
  hostnames:
  - gwapi.hackjap.link
  rules:
  - backendRefs:
    - name: service-2048
      port: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 접속 확인
dig +short gwapi.hackjap.link @8.8.8.8
echo &quot;http://gwapi.hackjap.link&quot;

# 삭제
kubectl delete httproute,targetgroupconfigurations,Gateway,GatewayClass --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress에서 어노테이션으로 흩어져 있던 설정이 Gateway API에서는 리소스 단위로 분리된다. 처음엔 장황해 보이지만 역할이 나뉘어 있어 팀 간 책임 경계가 분명해지고, 프로바이더를 바꿔도 구조를 재사용하기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;5. CoreDNS&quot; data-ke-size=&quot;size26&quot;&gt;5. CoreDNS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreDNS는 클러스터 내부 DNS 서버다. 파드가 서비스 이름(my-service.default.svc.cluster.local)으로 다른 파드에 접근할 수 있게 해주며, EKS에서는 관리형 Add-on으로 클러스터 프로비저닝 시 자동 설치된다.&lt;/p&gt;
&lt;h3 data-heading=&quot;5.1 Corefile 구조&quot; data-ke-size=&quot;size23&quot;&gt;5.1 Corefile 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreDNS의 동작은 ConfigMap에 정의된 Corefile로 제어된다:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;kubectl get cm -n kube-system coredns -o yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;.:53 {
    errors                              # 에러 로깅
    health {
        lameduck 5s                     # 종료 시 5초간 지연 (DNS 실패 최소화)
    }
    ready                               # readiness 엔드포인트 (/ready:8181)
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure                   # 파드 DNS 레코드 생성
        fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153                    # 메트릭 엔드포인트
    forward . /etc/resolv.conf          # 클러스터 외부 도메인은 업스트림 DNS로 포워딩
    cache 30                            # DNS 응답 30초 캐시
    loop                                # 무한 루프 감지
    reload                              # Corefile 변경 시 자동 리로드
    loadbalance                         # 응답의 A 레코드 순서 랜덤화
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;5.2 주요 설정&quot; data-ke-size=&quot;size23&quot;&gt;5.2 주요 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;lameduck&lt;/b&gt;&lt;br /&gt;CoreDNS 파드가 재시작되거나 노드가 종료될 때 트래픽 수신을 바로 끊으면 DNS 확인이 실패할 수 있다. lameduck 5s는 종료 신호를 받아도 5초간 정상 응답을 유지해서 이 문제를 줄여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cache&lt;/b&gt;&lt;br /&gt;기본 설정은 cache 30으로, 모든 DNS 응답을 30초간 캐시한다. 대규모 클러스터에서는 캐시를 세밀하게 튜닝할 수 있다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cache 30 {
    success 10000 30   # positive 캐시: 최대 10,000건, 30초 TTL
    denial 2000 10     # negative 캐시: 최대 2,000건, 10초 TTL
    prefetch 5 60s     # 동일 질의 5회 이상이면 만료 60초 전에 미리 갱신
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;forward&lt;/b&gt;&lt;br /&gt;cluster.local에 매칭되지 않는 외부 도메인 질의는 /etc/resolv.conf에 설정된 업스트림 DNS(VPC DNS)로 포워딩한다. 대규모 환경에서는 동시 요청 수를 늘릴 수 있다:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;forward . /etc/resolv.conf {
    max_concurrent 2000
    prefer_udp
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;5.3 EKS 관리형 Add-on 특성&quot; data-ke-size=&quot;size23&quot;&gt;5.3 EKS 관리형 Add-on 특성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS의 CoreDNS는 관리형 Add-on이라 일반 쿠버네티스와 좀 다른 부분이 있다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PDB (Pod Disruption Budget)&lt;/b&gt; &amp;mdash; CoreDNS에 기본으로 PDB가 설정되어 있어, 노드 종료 시에도 CoreDNS 파드가 동시에 모두 내려가는 것을 방지한다:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;kubectl get pdb -n kube-system coredns
# NAME      MAX UNAVAILABLE   ALLOWED DISRUPTIONS
# coredns   1                 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;topologySpreadConstraints&lt;/b&gt; &amp;mdash; EKS Add-on 구성 스키마로 CoreDNS 파드를 AZ별로 분산 배치할 수 있다. 특정 AZ에 CoreDNS가 몰리면 해당 AZ 장애 시 DNS가 마비되므로, 프로덕션에서는 설정을 권장한다:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# add-on 설정으로 topologySpreadConstraints 적용
aws eks update-addon --cluster-name $CLUSTER_NAME --addon-name coredns \
  --configuration-values '{&quot;topologySpreadConstraints&quot;:[{&quot;maxSkew&quot;:1,&quot;topologyKey&quot;:&quot;topology.kubernetes.io/zone&quot;,&quot;whenUnsatisfiable&quot;:&quot;ScheduleAnyway&quot;,&quot;labelSelector&quot;:{&quot;matchLabels&quot;:{&quot;k8s-app&quot;:&quot;kube-dns&quot;}}}]}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;readinessProbe&lt;/b&gt; &amp;mdash; /health 대신 /ready 엔드포인트를 사용하여, Corefile의 ready 플러그인과 연동된 정확한 준비 상태를 확인한다.&lt;/p&gt;
&lt;h3 data-heading=&quot;5.4 DNS 쿼리 흐름&quot; data-ke-size=&quot;size23&quot;&gt;5.4 DNS 쿼리 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드에서 외부 도메인을 질의할 때의 흐름:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Pod &amp;rarr; CoreDNS (cluster.local?)
        ├── Yes &amp;rarr; 클러스터 내부 서비스 IP 반환
        └── No  &amp;rarr; forward &amp;rarr; VPC DNS (AmazonProvidedDNS) &amp;rarr; 외부 DNS
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드의 /etc/resolv.conf는 CoreDNS Service의 ClusterIP를 nameserver로 가리키고 있으며, ndots:5 설정에 의해 짧은 이름은 cluster.local 등의 search domain이 자동으로 붙는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;마무리&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차에 다룬 내용을 정리하면:&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;VPC CNI &amp;rarr; Service &amp;rarr; Gateway API + AWS LB Controller &amp;rarr; ExternalDNS &amp;rarr; CoreDNS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC&amp;nbsp;CNI가&amp;nbsp;파드에&amp;nbsp;실제&amp;nbsp;VPC&amp;nbsp;IP를&amp;nbsp;부여하고,&amp;nbsp;그&amp;nbsp;위에&amp;nbsp;Service로&amp;nbsp;엔드포인트를&amp;nbsp;잡고,&amp;nbsp;Gateway&amp;nbsp;API와&amp;nbsp;LB&amp;nbsp;Controller가&amp;nbsp;함께&amp;nbsp;ALB/NLB를&amp;nbsp;만들어&amp;nbsp;라우팅을&amp;nbsp;처리하고,&amp;nbsp;ExternalDNS가&amp;nbsp;도메인을&amp;nbsp;붙이는&amp;nbsp;흐름이다.&amp;nbsp;CoreDNS는&amp;nbsp;이&amp;nbsp;모든&amp;nbsp;과정에서&amp;nbsp;클러스터&amp;nbsp;내부&amp;nbsp;DNS&amp;nbsp;해석을&amp;nbsp;담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드 IP부터 로드밸런서, DNS까지 &amp;mdash; 쿠버네티스 리소스를 선언하면 대응하는 AWS 리소스가 바로 프로비저닝된다. EKS 네트워킹이 쿠버네티스 오브젝트와 AWS 인프라를 이 정도로 긴밀하게 연동해 놓았다는 점에서, 두 레이어 사이의 경계가 거의 느껴지지 않을 만큼 잘 설계되었다고 느꼈다.&lt;/p&gt;</description>
      <category>AWS/EKS</category>
      <category>aws vpc cni</category>
      <category>EKS</category>
      <category>Network</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/105</guid>
      <comments>https://hackjsp.tistory.com/105#entry105comment</comments>
      <pubDate>Wed, 25 Mar 2026 23:40:53 +0900</pubDate>
    </item>
    <item>
      <title>[AEWS4] EKS가 만들어주는 AWS 리소스 들여다보기</title>
      <link>https://hackjsp.tistory.com/104</link>
      <description>&lt;h2 data-heading=&quot;1. EKS = AWS 리소스의 집합&quot; data-ke-size=&quot;size26&quot;&gt;1. EKS = AWS 리소스의 집합&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon EKS(Elastic Kubernetes Service)는 AWS의 완전관리형 Kubernetes 서비스다. 컨트롤 플레인을 AWS가 운영하므로 사용자는 워크로드에 집중하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전관리형이란?&amp;nbsp;Kubernetes&amp;nbsp;컨트롤&amp;nbsp;플레인은&amp;nbsp;API&amp;nbsp;서버,&amp;nbsp;etcd,&amp;nbsp;스케줄러,&amp;nbsp;컨트롤러&amp;nbsp;매니저로&amp;nbsp;구성된다.&amp;nbsp;직접&amp;nbsp;구축하면&amp;nbsp;etcd&amp;nbsp;클러스터링,&amp;nbsp;API&amp;nbsp;서버&amp;nbsp;이중화,&amp;nbsp;인증서&amp;nbsp;로테이션&amp;nbsp;같은&amp;nbsp;운영&amp;nbsp;부담이&amp;nbsp;상당하다.&amp;nbsp;EKS는&amp;nbsp;이를&amp;nbsp;대신&amp;nbsp;처리하고&amp;nbsp;99.95%&amp;nbsp;SLA를&amp;nbsp;보장한다.&amp;nbsp;대신&amp;nbsp;시간당&amp;nbsp;$0.10의&amp;nbsp;클러스터&amp;nbsp;비용이&amp;nbsp;발생한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 구축(kubeadm 등)과 비교하면 EKS가 대신하는 범위가 명확하다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;etcd 3대 클러스터링 + 백업/복구&lt;/td&gt;
&lt;td&gt;3 AZ 분산, 자동 백업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 서버 이중화 + LB 구성&lt;/td&gt;
&lt;td&gt;최소 2대 + NLB 자동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증서 발급&amp;middot;갱신 (kubeadm은 1년 만료)&lt;/td&gt;
&lt;td&gt;자동 로테이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노드 조인 토큰 관리 (kubeadm join)&lt;/td&gt;
&lt;td&gt;UserData로 자동 조인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CNI 별도 설치 (Calico, Flannel 등)&lt;/td&gt;
&lt;td&gt;VPC CNI 기본 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;버전 업그레이드 (순서대로 수동)&lt;/td&gt;
&lt;td&gt;콘솔/CLI에서 한 번에&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &quot;완전관리형&quot;이라는 말 뒤에 가려진 게 있다. EKS 클러스터를 하나 띄우면 &lt;b&gt;VPC, EC2, ENI, NLB, IAM, Security Group, Route 53&lt;/b&gt; 같은 AWS 리소스가 함께 생성된다. 결국 &lt;b&gt;AWS 리소스를 잘 엮어놓은 것&lt;/b&gt;에 가깝다.&lt;/p&gt;
&lt;h3 data-heading=&quot;아키텍처 개요&quot; data-ke-size=&quot;size23&quot;&gt;아키텍처 개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS는 &lt;b&gt;컨트롤 플레인&lt;/b&gt;과 &lt;b&gt;데이터 플레인&lt;/b&gt; 두 영역으로 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영역 소유 구성 요소&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 48px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;컨트롤 플레인&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;AWS Managed VPC&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;API 서버(최소 2개, 2+ AZ), etcd(3개, 3 AZ), NLB &amp;mdash; 멀티 AZ 분산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;데이터 플레인&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Customer VPC&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;워커 노드(EC2), EKS Owned ENI, 파드 네트워크&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤 플레인은 AWS 소유의 별도 VPC에서 돌아간다. 고객 콘솔에서는 보이지 않는다. API 서버 인스턴스(CPI)는 최소 2개가 서로 다른 AZ에, etcd는 3개 AZ에 분산 배치되어 있고 장애 시 자동 교체된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CPI(Control Plane Instance)란?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; API 서버가 돌아가는 EC2 인스턴스다. AWS 문서에서는 &quot;at least 2 API server instances&quot;라고만 표현하는데, 실제로는 3개 AZ에 걸쳐 배치된다. 고객이 직접 접근할 수 없고 AWS가 패치&amp;middot;교체&amp;middot;스케일링을 자동으로 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 영역을 잇는 것이 &lt;b&gt;EKS Owned ENI&lt;/b&gt;다. 고객 VPC에 있지만 연결된 인스턴스는 AWS 소유다. 이 구조가 EKS 네트워킹의 핵심이다.&lt;/p&gt;
&lt;h3 data-heading=&quot;배포 방식&quot; data-ke-size=&quot;size23&quot;&gt;배포 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 &lt;b&gt;Terraform&lt;/b&gt;을 사용했다. terraform-aws-modules/eks/aws 공식 모듈 기반이고 eksctl처럼 CloudFormation을 거치지 않아 배포가 더 빠르다.&lt;/p&gt;
&lt;h2 data-heading=&quot;2. 배포 &amp;mdash; terraform apply의 결과&quot; data-ke-size=&quot;size26&quot;&gt;2. 배포 &amp;mdash; terraform apply의 결과&lt;/h2&gt;
&lt;h3 data-heading=&quot;실습 환경&quot; data-ke-size=&quot;size23&quot;&gt;실습 환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 값&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;리전&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ap-northeast-2 (서울)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;EKS 버전&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;v1.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;플랫폼 버전&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;eks.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;배포 도구&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Terraform 1.14.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;EKS 모듈&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;terraform-aws-modules/eks/aws v21.15.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;VPC 모듈&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;terraform-aws-modules/vpc/aws v6.6.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;노드 AMI&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Amazon Linux 2023 (AL2023)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;노드 타입&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;t3.medium &amp;times; 2 (ON_DEMAND)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컨테이너 런타임&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;containerd 2.1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;커널&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6.12.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;cgroup&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;v2 (systemd)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;VPC CNI (aws-node)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;도메인&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;hackjap.link&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습&amp;nbsp;비용:&amp;nbsp;EKS&amp;nbsp;클러스터&amp;nbsp;$0.10/hr&amp;nbsp;+&amp;nbsp;t3.medium&amp;nbsp;&amp;times;&amp;nbsp;2대&amp;nbsp;약&amp;nbsp;$0.084/hr&amp;nbsp;=&amp;nbsp;시간당&amp;nbsp;약&amp;nbsp;$0.18.&amp;nbsp;12분&amp;nbsp;배포&amp;nbsp;+&amp;nbsp;2시간&amp;nbsp;실습&amp;nbsp;기준&amp;nbsp;약&amp;nbsp;$0.40.&amp;nbsp;실습&amp;nbsp;후&amp;nbsp;반드시&amp;nbsp;terraform&amp;nbsp;destroy로&amp;nbsp;정리할&amp;nbsp;것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 후 버전 확인:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# EKS 클러스터 버전
aws eks describe-cluster --name myeks --query 'cluster.{Version:version, PlatformVersion:platformVersion}' --output table

# kubectl 버전
kubectl version --short

# 노드 OS/커널 확인
kubectl get nodes -o wide
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;사전 준비&quot; data-ke-size=&quot;size23&quot;&gt;사전 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM 자격 증명과 SSH 키페어가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# IAM 자격 증명 확인
aws sts get-caller-identity

# SSH 키페어 생성 (이미 있으면 생략)
aws ec2 create-key-pair --key-name hackjap-keypair --query 'KeyMaterial' --output text &amp;gt; ~/.ssh/hackjap-keypair.pem
chmod 400 ~/.ssh/hackjap-keypair.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필수 도구 (macOS 기준):&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew install awscli kubernetes-cli helm
brew install tfenv &amp;amp;&amp;amp; tfenv install 1.14.6 &amp;amp;&amp;amp; tfenv use 1.14.6

# (권장) 편의 도구
brew install krew k9s kube-ps1 kubectx kubecolor
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;배포&quot; data-ke-size=&quot;size23&quot;&gt;배포&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# 실습 코드 다운로드
git clone https://github.com/gasida/aews.git &amp;amp;&amp;amp; cd aews/1w

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

# 배포 (약 12분)
terraform init
terraform plan
terraform apply -auto-approve

# kubeconfig 설정
aws eks update-kubeconfig --region ap-northeast-2 --name myeks
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') myeks
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용된 Terraform 모듈:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;terraform-aws-modules/vpc/aws (v6.6.0) &amp;mdash; VPC&lt;/li&gt;
&lt;li&gt;terraform-aws-modules/eks/aws (v21.15.1) &amp;mdash; EKS 클러스터 + 노드 그룹&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;배포 후 생긴 것&quot; data-ke-size=&quot;size23&quot;&gt;배포 후 생긴 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform apply 완료 후 AWS 콘솔에서 생성된 리소스를 확인한다. terraform state list로 Terraform이 관리하는 리소스 목록도 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 클러스터 상태 확인
aws eks describe-cluster --name myeks --query 'cluster.{Status:status, Version:version, Endpoint:endpoint}' --output table

# 노드 상태 확인
kubectl get node -o wide
# NAME                                               STATUS   ROLES    AGE   VERSION   INTERNAL-IP     OS-IMAGE
# ip-192-168-2-xx.ap-northeast-2.compute.internal    Ready    &amp;lt;none&amp;gt;   5m    v1.32.x   192.168.2.xx    Amazon Linux 2023
# ip-192-168-3-xx.ap-northeast-2.compute.internal    Ready    &amp;lt;none&amp;gt;   5m    v1.32.x   192.168.3.xx    Amazon Linux 2023

# Terraform 리소스 목록 (일부)
terraform state list | head -10
# module.eks.aws_eks_cluster.this[0]
# module.eks.aws_eks_node_group.this[&quot;myeks-node-group&quot;]
# module.vpc.aws_vpc.this[0]
# module.vpc.aws_subnet.public[0]
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform apply 한 번에 아래 리소스가 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 서비스 생성된 리소스 역할&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;VPC&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;VPC, 서브넷, 라우팅 테이블, IGW&lt;/td&gt;
&lt;td&gt;네트워크 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;EC2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;워커 노드 (t3.medium &amp;times; 2)&lt;/td&gt;
&lt;td&gt;파드 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Auto Scaling&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ASG + 시작 템플릿&lt;/td&gt;
&lt;td&gt;노드 그룹 스케일링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ENI&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;EKS Owned ENI (2개)&lt;/td&gt;
&lt;td&gt;컨트롤 &amp;harr; 데이터 플레인 통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ELB&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;NLB (API 서버용)&lt;/td&gt;
&lt;td&gt;API 서버 부하분산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;IAM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 역할, 노드 역할&lt;/td&gt;
&lt;td&gt;권한&amp;middot;인증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Security Group&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 SG, 노드 SG&lt;/td&gt;
&lt;td&gt;접근 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;EKS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터, 노드 그룹, Add-on 3종&lt;/td&gt;
&lt;td&gt;Kubernetes 오케스트레이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;IAM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;OIDC Provider&lt;/td&gt;
&lt;td&gt;파드 수준 AWS 권한 부여 (&amp;rarr; 이후 주차 심화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;CloudWatch&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;로그 그룹 (선택)&lt;/td&gt;
&lt;td&gt;컨트롤 플레인 감사 로그 (&amp;rarr; 이후 주차 심화)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;3. AWS 리소스 탐색&quot; data-ke-size=&quot;size26&quot;&gt;3. AWS 리소스 탐색&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform apply로 생긴 리소스를 AWS 콘솔과 CLI로 하나씩 열어본다. 각 리소스가 왜 필요하고 EKS 아키텍처에서 어떤 역할을 하는지 확인하는 게 이 섹션의 목표다.&lt;/p&gt;
&lt;h3 data-heading=&quot;3.1 VPC / Subnet&quot; data-ke-size=&quot;size23&quot;&gt;3.1 VPC / Subnet&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 콘솔에서 VPC를 열면 myeks-VPC가 생성되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC CIDR은 192.168.0.0/16이고 서브넷 3개가 각 AZ에 /24씩 할당됐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서브넷&lt;/b&gt;: ap-northeast-2a/2b/2c에 퍼블릭 서브넷 3개 (192.168.1.0/24, 192.168.2.0/24, 192.168.3.0/24)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라우팅 테이블&lt;/b&gt;: myeks-VPC-public이 서브넷 3개에 연결. IGW로 향하는 0.0.0.0/0 라우트 존재&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 연결&lt;/b&gt;: myeks-IGW(Internet Gateway) 하나&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DNS 확인/호스트 이름&lt;/b&gt;: 둘 다 활성화됨 &amp;mdash; Endpoint Access Private 모드의 필수 조건&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 VPC가 EKS 클러스터의 네트워크 기반이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고객 VPC (Customer VPC)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Terraform VPC 모듈이 생성&lt;/li&gt;
&lt;li&gt;워커 노드와 파드가 여기서 돌아간다&lt;/li&gt;
&lt;li&gt;서브넷은 퍼블릭/프라이빗으로 나뉨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;퍼블릭 서브넷&lt;/b&gt; &amp;mdash; 라우팅 테이블에 IGW(Internet Gateway)가 연결돼 있다. 노드에 공인 IP가 붙는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프라이빗 서브넷&lt;/b&gt; &amp;mdash; 인터넷 직접 접근 불가. Fully Private 클러스터에서 사용. NAT Gateway를 통한 아웃바운드만 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 서브넷을 나누는가?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; EKS에서 퍼블릭 서브넷은 NLB나 인터넷 경유 트래픽이 필요한 리소스에 쓰이고, 프라이빗 서브넷은 워커 노드가 올라가는 곳이다. 보안 관점에서 노드를 프라이빗에 두면 외부에서 직접 접근할 수 없어 공격 표면이 줄어든다. 이번 실습에서는 편의상 퍼블릭에 노드를 뒀지만, &lt;/span&gt;&lt;b&gt;운영 환경에서는 프라이빗 서브넷이 표준&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보이지 않는 VPC&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: 컨트롤 플레인은 &lt;/span&gt;&lt;b&gt;AWS Managed VPC&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에 있다. 고객 콘솔에선 보이지 않는다. API 서버와 etcd가 여기서 돌아가고 EKS Owned ENI로 고객 VPC에 연결된다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-heading=&quot;EKS 서브넷 필수 조건&quot; data-ke-size=&quot;size20&quot;&gt;EKS 서브넷 필수 조건&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최소 2개 서브넷&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;서로 다른 AZ에 위치 (고가용성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사용 가능 IP 6개 이상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ENI 프로비저닝에 필요. VPC CNI가 파드에 실제 IP를 할당하므로 넉넉하게 설계해야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;VPC DNS 설정&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;enableDnsSupport=true, enableDnsHostnames=true 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-heading=&quot;서브넷 태그&quot; data-ke-size=&quot;size20&quot;&gt;서브넷 태그&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 로드 밸런서 컨트롤러가 서브넷을 자동 인식하려면 태그가 필요하다. 없으면 LoadBalancer 타입 서비스 생성 시 서브넷을 못 찾아 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그 값 대상&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.io/cluster/&amp;lt;cluster-name&amp;gt;&lt;/td&gt;
&lt;td&gt;shared 또는 owned&lt;/td&gt;
&lt;td&gt;모든 EKS 서브넷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.io/role/elb&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;퍼블릭 서브넷 &amp;mdash; 외부향 LB 배치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.io/role/internal-elb&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;프라이빗 서브넷 &amp;mdash; 내부 LB 배치&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-heading=&quot;환경별 서브넷 구성 패턴&quot; data-ke-size=&quot;size20&quot;&gt;환경별 서브넷 구성 패턴&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 구성 서브넷 수 비고&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;학습/개발&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;퍼블릭 &amp;times; 3 AZ&lt;/td&gt;
&lt;td&gt;3개&lt;/td&gt;
&lt;td&gt;이번 실습 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;일반 운영&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;퍼블릭 &amp;times; 3 + 프라이빗 &amp;times; 3&lt;/td&gt;
&lt;td&gt;6개&lt;/td&gt;
&lt;td&gt;가장 보편적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;엔터프라이즈&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;퍼블릭 &amp;times; 3 + 앱 &amp;times; 3 + DB &amp;times; 3&lt;/td&gt;
&lt;td&gt;9개&lt;/td&gt;
&lt;td&gt;3-tier 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습은 퍼블릭 서브넷 3개(/24)만 사용한다. 운영 환경에서는 노드를 프라이빗 서브넷에, NLB/ALB를 퍼블릭 서브넷에 배치하는 6개 구성이 표준이다.&lt;/p&gt;
&lt;h4 data-heading=&quot;생성 이후 변경 가능 여부&quot; data-ke-size=&quot;size20&quot;&gt;생성 이후 변경 가능 여부&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 가능 여부 비고&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;클러스터 서브넷 추가/제거&lt;/td&gt;
&lt;td&gt;&lt;b&gt;가능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최소 2개 AZ 유지 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노드 그룹 서브넷 변경&lt;/td&gt;
&lt;td&gt;&lt;b&gt;불가&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;노드 그룹 재생성 필요 (블루/그린)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC 자체 변경&lt;/td&gt;
&lt;td&gt;&lt;b&gt;불가&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 재생성 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-heading=&quot;3.2 EC2 &amp;mdash; 관리형 노드 그룹&quot; data-ke-size=&quot;size23&quot;&gt;3.2 EC2 &amp;mdash; 관리형 노드 그룹&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 콘솔에서 워커 노드 2개를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;myeks-node-g... 이름으로 t3.medium 인스턴스 2개가 실행 중이다. 각각 ap-northeast-2b, 2c에 분산 배치되어 있고 퍼블릭 IP가 할당돼 있다. 상태 검사 3/3 통과.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASG(Auto Scaling Group) 콘솔에서 스케일링 설정을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 용량 2, 크기 조정 한도 1~4. 노드가 죽으면 ASG가 자동으로 새 인스턴스를 띄워서 desired 2를 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 템플릿의 UserData가 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodeConfig 리소스에 EKS 클러스터 정보가 전부 들어 있다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;apiServerEndpoint &amp;mdash; API 서버 주소&lt;/li&gt;
&lt;li&gt;certificateAuthority &amp;mdash; 클러스터 CA 인증서 (base64)&lt;/li&gt;
&lt;li&gt;cidr: 10.100.0.0/16 &amp;mdash; 서비스 CIDR&lt;/li&gt;
&lt;li&gt;maxPods: 17 &amp;mdash; ENI/IP 기반 계산값&lt;/li&gt;
&lt;li&gt;clusterDNS: 10.100.0.10 &amp;mdash; CoreDNS 서비스 IP&lt;/li&gt;
&lt;li&gt;--node-labels 플래그에 AMI ID, 노드그룹명, 시작 템플릿 ID 등 메타데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드가 부팅되면 이 설정으로 kubelet이 자동으로 클러스터에 조인한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# 노드 그룹 정보
aws eks describe-nodegroup --cluster-name myeks --nodegroup-name myeks-node-group | jq
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관리형 노드 그룹&lt;/b&gt;은 EC2 Auto Scaling Group을 EKS가 감싸서 관리하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관리형 vs 자체 관리형 vs Fargate&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: 워커 노드를 올리는 방법은 세 가지다. &lt;/span&gt;&lt;b&gt;관리형 노드 그룹&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;은 ASG/시작 템플릿/AMI 업데이트를 AWS가 관리한다. &lt;/span&gt;&lt;b&gt;자체 관리형 노드 그룹&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;은 ASG를 직접 만들어 EKS에 조인시키는 방식으로 커스터마이징 자유도가 높다. &lt;/span&gt;&lt;b&gt;Fargate&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;는 EC2 없이 파드 단위로 실행하는 서버리스 방식이다. 관리형이 운영 부담과 유연성의 균형이 가장 좋아서 대부분의 워크로드에서 쓰인다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 값&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인스턴스 타입&lt;/td&gt;
&lt;td&gt;t3.medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMI&lt;/td&gt;
&lt;td&gt;AL2023 (Amazon Linux 2023)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity Type&lt;/td&gt;
&lt;td&gt;ON_DEMAND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling&lt;/td&gt;
&lt;td&gt;min 1 / max 4 / desired 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨테이너 런타임&lt;/td&gt;
&lt;td&gt;containerd 2.1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cgroup&lt;/td&gt;
&lt;td&gt;v2 (systemd)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시작 템플릿(Launch Template):&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UserData에 EKS 노드 부트스트랩 설정이 들어 있다&lt;/li&gt;
&lt;li&gt;kubelet과 containerd가 자동 설정된다&lt;/li&gt;
&lt;li&gt;클러스터 CA 인증서와 API 엔드포인트도 주입된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-heading=&quot;maxPods가 왜 17인가&quot; data-ke-size=&quot;size20&quot;&gt;maxPods가 왜 17인가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;t3.medium은 ENI 3개를 쓸 수 있고 ENI당 IP는 6개다. 공식 계산식:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(ENI 3개 &amp;times; (IP 6개 - 1)) + 2 = 17
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-1은 ENI 자체의 프라이머리 IP&lt;/li&gt;
&lt;li&gt;+2는 호스트 네트워크를 쓰는 파드(aws-node, kube-proxy)가 파드 IP를 소모하지 않는 것을 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS VPC CNI는 파드에 &lt;b&gt;VPC의 실제 IP&lt;/b&gt;를 할당한다. overlay가 아니다. 그래서 인스턴스 타입에 따라 maxPods가 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;VPC CNI vs Overlay 네트워크&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: 일반적인 Kubernetes CNI(Calico, Flannel 등)는 overlay 네트워크를 만든다. 파드 IP는 가상이고 VXLAN이나 IP-in-IP 캡슐화를 거쳐야 외부와 통신한다. AWS VPC CNI는 다르다. 파드에 VPC 서브넷의 실제 IP를 직접 할당한다. 캡슐화 없이 VPC 네이티브로 통신하니 레이턴시가 낮고 보안 그룹을 파드 단위로 적용할 수 있다. 대신 서브넷 IP를 소모하기 때문에 CIDR 설계를 넉넉하게 해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-heading=&quot;노드 내부 확인&quot; data-ke-size=&quot;size20&quot;&gt;노드 내부 확인&lt;/h4&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 노드 IP 확인
NODE1=&amp;lt;public-ip-1&amp;gt;
NODE2=&amp;lt;public-ip-2&amp;gt;

# SSH 접속
ssh -i ~/.ssh/id_rsa ec2-user@$NODE1

# OS 확인
hostnamectl
# &amp;rarr; Amazon Linux 2023, kernel 6.12.x, t3.medium

# 컨테이너 런타임
nerdctl info
# &amp;rarr; containerd 2.1.5, cgroup v2, systemd

# kubelet 설정 핵심
cat /etc/kubernetes/kubelet/config.json | jq '{maxPods, cgroupDriver, evictionHard, serializeImagePulls, protectKernelDefaults}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet 주요 설정:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 값 의미&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;maxPods&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;ENI/IP 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;evictionHard&lt;/td&gt;
&lt;td&gt;메모리 100Mi&amp;darr;, 디스크 10%&amp;darr;&lt;/td&gt;
&lt;td&gt;리소스 부족 시 파드 강제 축출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serializeImagePulls&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;이미지 병렬 다운로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;protectKernelDefaults&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;kubelet이 커널 파라미터를 건드리지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serverTLSBootstrap&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;TLS 인증서 자동 발급&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-heading=&quot;3.3 ENI &amp;mdash; EKS Owned ENI의 정체&quot; data-ke-size=&quot;size23&quot;&gt;3.3 ENI &amp;mdash; EKS Owned ENI의 정체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS에서 가장 독특한 리소스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ENI(Elastic Network Interface)란?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; VPC 안에서 가상 네트워크 카드 역할을 하는 AWS 리소스다. EC2 인스턴스에 붙이면 해당 서브넷의 프라이빗 IP를 받고 보안 그룹도 적용된다. 보통은 EC2를 만들면 자동으로 하나 생기는데, EKS에서는 &lt;/span&gt;&lt;b&gt;AWS가 고객 VPC에 ENI를 직접 꽂아서&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 자기네 VPC의 API 서버와 연결하는 특별한 용도로 쓴다. Cross-account ENI attachment라고 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;고객 VPC 서브넷 안에 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;소유&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ENI 자체는 고객 계정에 보인다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;연결 대상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;AWS Managed VPC의 API 서버(CPI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;개수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 서브넷에 각 1개씩 기본 2개 (스케일링/업그레이드 시 추가 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;보안 그룹&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 SG가 붙어 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-heading=&quot;콘솔에서 보면 소유자가 둘이다&quot; data-ke-size=&quot;size20&quot;&gt;콘솔에서 보면 소유자가 둘이다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENI 상세 정보를 열어보면 &quot;소유자&quot;와 &quot;인스턴스 소유자&quot;가 다른 계정으로 나온다. 이것이 Cross-Account ENI의 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드 값 의미&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;소유자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;본인 계정 ID&lt;/td&gt;
&lt;td&gt;ENI 리소스가 고객 VPC에 위치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;요청자 ID / 인스턴스 소유자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;AWS EKS 서비스 계정&lt;/td&gt;
&lt;td&gt;연결된 인스턴스(API 서버)가 AWS Managed VPC에 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인스턴스 ID&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;비어있음&lt;/td&gt;
&lt;td&gt;API 서버 인스턴스는 고객 콘솔에서 안 보임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Description&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Amazon EKS &amp;lt;cluster-name&amp;gt;&lt;/td&gt;
&lt;td&gt;EKS Owned ENI 식별자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 VPC에 꽂혀있는 네트워크 카드인데, 케이블 반대쪽은 AWS Managed VPC의 API 서버로 연결&quot;된 구조다.&lt;/p&gt;
&lt;h4 data-heading=&quot;왜 필요한가&quot; data-ke-size=&quot;size20&quot;&gt;왜 필요한가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤 플레인(AWS Managed VPC)과 데이터 플레인(고객 VPC)은 다른 VPC에 있다. 이 두 세계를 잇는 건 EKS Owned ENI뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;통신 경로:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;API 서버 &amp;rarr; kubelet&lt;/b&gt;: kubectl logs나 kubectl exec를 실행하면 API 서버가 이 ENI를 통해 노드의 kubelet(10250 포트)에 접근한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노드 &amp;rarr; API 서버&lt;/b&gt;: Public+Private 또는 Private 모드에서 kubelet, kube-proxy, aws-node가 ENI의 프라이빗 IP로 API 서버에 접근한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Endpoint Access 모드에 따라 노드&amp;rarr;API 경로가 달라진다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모드 노드 &amp;rarr; API 서버 경로 ss -tnp Peer Address&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Public&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인터넷 &amp;rarr; NLB 퍼블릭 IP&lt;/td&gt;
&lt;td&gt;43.x.x.x:443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Public+Private&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;VPC 내부 &amp;rarr; EKS Owned ENI&lt;/td&gt;
&lt;td&gt;192.168.x.x:443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Private&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;VPC 내부 &amp;rarr; EKS Owned ENI&lt;/td&gt;
&lt;td&gt;192.168.x.x:443&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Public 모드에서는 EKS Owned ENI가 존재하지만, 노드&amp;rarr;API 방향에서는 사용되지 않는다 (API&amp;rarr;노드 방향의 kubectl exec, kubectl logs에서만 사용). 4절에서 모드별 차이를 실습으로 확인한다.&lt;/p&gt;
&lt;pre class=&quot;nsis&quot;&gt;&lt;code&gt;# 노드에서 확인 &amp;mdash; Peer Address로 현재 모드 판별
ssh ec2-user@$NODE1 sudo ss -tnp | grep kubelet
# Public 모드:        Peer Address = 43.x.x.x:443 (NLB 퍼블릭 IP)
# Public+Private 모드: Peer Address = 192.168.x.x:443 (EKS Owned ENI IP)

# kubectl exec 실행 시 API &amp;rarr; 노드 방향 연결 확인
kubectl exec -it &amp;lt;pod&amp;gt; -- bash  # 다른 터미널에서 실행
ssh ec2-user@$NODE1 sudo ss -tnp | grep kubelet
# &amp;rarr; 10250 포트로 들어오는 연결 확인 (API &amp;rarr; kubelet, 모든 모드에서 ENI 경유)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cross-Account&amp;nbsp;ENI는&amp;nbsp;EKS만의&amp;nbsp;패턴이&amp;nbsp;아니다.&amp;nbsp;RDS,&amp;nbsp;Lambda(VPC&amp;nbsp;연결),&amp;nbsp;EFS,&amp;nbsp;VPC&amp;nbsp;Endpoint&amp;nbsp;등도&amp;nbsp;고객&amp;nbsp;VPC에&amp;nbsp;ENI를&amp;nbsp;생성한다.&amp;nbsp;차이점은&amp;nbsp;이들은&amp;nbsp;같은&amp;nbsp;VPC&amp;nbsp;안에서의&amp;nbsp;연결인&amp;nbsp;반면,&amp;nbsp;EKS&amp;nbsp;Owned&amp;nbsp;ENI는&amp;nbsp;서로&amp;nbsp;다른&amp;nbsp;VPC(AWS&amp;nbsp;Managed&amp;nbsp;VPC&amp;nbsp;&amp;harr;&amp;nbsp;고객&amp;nbsp;VPC)를&amp;nbsp;잇는&amp;nbsp;Cross-VPC&amp;nbsp;연결이라는&amp;nbsp;점이다.&lt;/p&gt;
&lt;h3 data-heading=&quot;3.4 Security Group &amp;mdash; 클러스터 SG, 노드 SG&quot; data-ke-size=&quot;size23&quot;&gt;3.4 Security Group &amp;mdash; 클러스터 SG, 노드 SG&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS를 배포하면 보안 그룹이 자동으로 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 보안 그룹이 두 개인가?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;b&gt;클러스터 SG&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;는 EKS가 만들고 관리하는 것으로, 컨트롤 플레인과 노드 사이의 통신을 보장한다. 건드리면 클러스터가 깨질 수 있으니 함부로 수정하면 안 된다. &lt;/span&gt;&lt;b&gt;노드 SG&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;는 사용자가 추가로 붙이는 것으로, SSH 접근이나 외부 서비스 노출 같은 커스텀 정책을 여기에 건다. 관심사를 분리해서 EKS 핵심 통신과 사용자 정책이 서로 영향을 주지 않게 한 구조다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터 보안 그룹 (Cluster SG)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EKS가 자동 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EKS Owned ENI에 적용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;컨트롤 플레인 &amp;harr; 노드 간 통신을 전부 허용&lt;/li&gt;
&lt;li&gt;노드도 이 SG에 포함되어 양방향 통신이 보장된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노드 보안 그룹 (Node SG)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Terraform에서 추가로 설정하는 보안 그룹&lt;/li&gt;
&lt;li&gt;SSH 접근이나 NodePort 서비스 접근 용도&lt;/li&gt;
&lt;li&gt;이 실습에서는 자기 IP에서만 SSH/NodePort를 열었다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 노드 보안 그룹 확인
aws ec2 describe-security-groups --filters &quot;Name=tag:Name,Values=myeks-node-group-sg&quot; --query 'SecurityGroups[*].IpPermissions' --output text
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;3.5 IAM &amp;mdash; 역할과 인증 흐름&quot; data-ke-size=&quot;size23&quot;&gt;3.5 IAM &amp;mdash; 역할과 인증 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS에서 IAM은 &lt;b&gt;인프라 권한&lt;/b&gt;과 &lt;b&gt;Kubernetes 인증&lt;/b&gt; 두 가지를 맡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 IAM으로 인증하는가?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 바닐라 Kubernetes는 X.509 인증서나 서비스 어카운트 토큰으로 인증한다. AWS 환경에서는 IAM이 이미 모든 사용자와 서비스의 신원을 관리하고 있다. EKS는 이를 활용해서 &quot;AWS에서 누구인지&quot;와 &quot;Kubernetes에서 무엇을 할 수 있는지&quot;를 연결한다. 별도의 인증서 발급 인프라 없이 IAM의 MFA, 조건부 정책 같은 보안 기능을 그대로 쓸 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터 역할 (Cluster IAM Role)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EKS 서비스 자체가 쓰는 역할&lt;/li&gt;
&lt;li&gt;ENI 생성, CloudWatch 로그 전송, 보안 그룹 관리 등&lt;/li&gt;
&lt;li&gt;Trust: eks.amazonaws.com&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노드 역할 (Node IAM Role)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;워커 노드(EC2)가 쓰는 역할&lt;/li&gt;
&lt;li&gt;ECR 이미지 풀, EKS API 통신, CloudWatch 지표 전송&lt;/li&gt;
&lt;li&gt;Trust: ec2.amazonaws.com&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-heading=&quot;인증 흐름: kubectl &amp;rarr; IAM &amp;rarr; Kubernetes&quot; data-ke-size=&quot;size20&quot;&gt;인증 흐름: kubectl &amp;rarr; IAM &amp;rarr; Kubernetes&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS는 인증서가 아닌 &lt;b&gt;IAM 기반 인증&lt;/b&gt;을 쓴다. kubeconfig를 열어보면:&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;users:
- name: myeks
  user:
    exec:
      command: aws
      args:
        - eks
        - get-token
        - --cluster-name
        - myeks
        - --region
        - ap-northeast-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl 요청 처리 과정:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;aws eks get-token이 실행되고 IAM 자격증명으로 &lt;b&gt;STS 토큰&lt;/b&gt;을 발급받는다&lt;/li&gt;
&lt;li&gt;이 토큰이 Bearer Token으로 API 서버에 전달된다&lt;/li&gt;
&lt;li&gt;API 서버는 AWS IAM Authenticator를 거쳐 &lt;b&gt;IAM 엔티티 &amp;rarr; Kubernetes 사용자&lt;/b&gt; 매핑을 한다&lt;/li&gt;
&lt;li&gt;이후 Kubernetes RBAC으로 인가 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 토큰 발급 확인
aws eks get-token --cluster-name myeks --region ap-northeast-2 | jq

# 인증 과정 로그 확인 (-v=6)
kubectl get node -v=6
# &amp;rarr; Config loaded from file: /root/.kube/config
# &amp;rarr; GET https://&amp;lt;api-endpoint&amp;gt;/api/v1/nodes 200 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet도&amp;nbsp;같은&amp;nbsp;구조다.&amp;nbsp;/var/lib/kubelet/kubeconfig를&amp;nbsp;보면&amp;nbsp;동일한&amp;nbsp;IAM&amp;nbsp;기반&amp;nbsp;인증을&amp;nbsp;쓴다.&lt;/p&gt;
&lt;h4 data-heading=&quot;파드 수준 IAM: OIDC Provider&quot; data-ke-size=&quot;size20&quot;&gt;파드 수준 IAM: OIDC Provider&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 클러스터를 만들면 &lt;b&gt;OIDC Provider&lt;/b&gt;도 자동으로 생성된다. 이를 통해 파드에 최소 권한의 IAM 역할을 부여할 수 있다 (IRSA 또는 Pod Identity). 노드 역할에 모든 권한을 몰아주는 것보다 훨씬 안전하다. 파드 수준 IAM은 이후 주차에서 다룬다.&lt;/p&gt;
&lt;h3 data-heading=&quot;3.6 ELB &amp;mdash; NLB와 Hyperplane&quot; data-ke-size=&quot;size23&quot;&gt;3.6 ELB &amp;mdash; NLB와 Hyperplane&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 서버 앞에는 &lt;b&gt;NLB(Network Load Balancer)&lt;/b&gt;가 붙어 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# API 엔드포인트 확인
APIDNS=$(aws eks describe-cluster --name myeks | jq -r .cluster.endpoint | cut -d '/' -f 3)
dig +short $APIDNS
# &amp;rarr; 퍼블릭 IP 2개 (NLB의 AZ별 IP)

# IP 소유자 확인
curl -s ipinfo.io/&amp;lt;ip&amp;gt;
# &amp;rarr; &quot;org&quot;: &quot;AS16509 Amazon.com, Inc.&quot;

# API 서버 접근 테스트
curl -sk https://$APIDNS/version
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-heading=&quot;왜 ALB가 아니라 NLB인가&quot; data-ke-size=&quot;size20&quot;&gt;왜 ALB가 아니라 NLB인가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교 ALB NLB&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;동작 계층&lt;/td&gt;
&lt;td&gt;L7 (HTTP/HTTPS)&lt;/td&gt;
&lt;td&gt;L4 (TCP/UDP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;아키텍처&lt;/td&gt;
&lt;td&gt;VM 기반&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Hyperplane&lt;/b&gt; (메모리 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능&lt;/td&gt;
&lt;td&gt;상대적 낮음&lt;/td&gt;
&lt;td&gt;초당 수억 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;레이턴시&lt;/td&gt;
&lt;td&gt;수십~수백 ms&lt;/td&gt;
&lt;td&gt;수십 ms 미만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;헬스체크 최소&lt;/td&gt;
&lt;td&gt;~10초&lt;/td&gt;
&lt;td&gt;~20초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes API는 &lt;b&gt;HTTPS/gRPC&lt;/b&gt; 통신이라 L4에서 처리하는 게 맞고, NLB의 Hyperplane 아키텍처 덕분에 메모리 기반 처리로 지연이 극히 낮다. 워밍업 없이 즉시 확장되니 노드 100대가 동시에 부팅해도 문제없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hyperplane이란?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; AWS 내부의 네트워크 가상화 플랫폼이다. NLB뿐 아니라 NAT Gateway, PrivateLink, VPC Endpoint도 Hyperplane 위에서 동작한다. 사용자가 직접 제어할 수 있는 리소스는 아니고, NLB의 고성능을 가능하게 하는 AWS 내부 기술이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-heading=&quot;4. 딥다이브: Endpoint Access와 DNS&quot; data-ke-size=&quot;size26&quot;&gt;4. Endpoint Access와 DNS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 네트워크 설계에서 가장 중요한 결정 중 하나다. &lt;b&gt;API 서버 엔드포인트를 어떻게 노출할 것인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 모드가 있는데 단순히 &quot;퍼블릭이냐 프라이빗이냐&quot;가 아니다. &lt;b&gt;Route 53 Private Hosted Zone이 생기느냐, VPC Endpoint가 필요하냐&lt;/b&gt;까지 달라지는 결정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 이게 중요한가?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; Endpoint Access 모드는 보안과 비용 두 축에 영향을 준다. Public 모드는 설정이 간단하지만 노드의 API 트래픽이 인터넷을 거친다. Public+Private은 노드 트래픽을 VPC 내부로 돌리면서도 외부 접근을 유지한다. Private은 가장 안전하지만 VPC Endpoint(개당 월 $7~10)가 여러 개 필요하고 VPN/bastion 인프라도 갖춰야 한다. 모드를 바꾸면 DNS 응답부터 보안 그룹까지 여러 리소스가 연쇄적으로 바뀌니 초기에 잘 골라야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-heading=&quot;4.1 세 가지 모드 비교&quot; data-ke-size=&quot;size23&quot;&gt;4.1 세 가지 모드 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모드 외부 &amp;rarr; API 노드 &amp;rarr; API API &amp;rarr; 노드 추가 AWS 리소스&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Public&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;퍼블릭 IP (인터넷)&lt;/td&gt;
&lt;td&gt;퍼블릭 IP (인터넷)&lt;/td&gt;
&lt;td&gt;EKS Owned ENI&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Public + Private&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;퍼블릭 IP (인터넷)&lt;/td&gt;
&lt;td&gt;프라이빗 IP (ENI)&lt;/td&gt;
&lt;td&gt;EKS Owned ENI&lt;/td&gt;
&lt;td&gt;Private Hosted Zone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Private&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;불가 (VPC 내부만)&lt;/td&gt;
&lt;td&gt;프라이빗 IP (ENI)&lt;/td&gt;
&lt;td&gt;EKS Owned ENI&lt;/td&gt;
&lt;td&gt;Private Hosted Zone + VPC Endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;4.2 Public 모드&quot; data-ke-size=&quot;size23&quot;&gt;4.2 Public 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 구성이다. 이 실습에서 기본으로 배포되는 모드다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;사용자 kubectl &amp;rarr; (인터넷) &amp;rarr; NLB &amp;rarr; API 서버
워커 노드       &amp;rarr; (인터넷) &amp;rarr; NLB &amp;rarr; API 서버
API 서버       &amp;rarr; (EKS Owned ENI) &amp;rarr; 워커 노드 kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정이 간편하고 어디서든 kubectl을 쓸 수 있다&lt;/li&gt;
&lt;li&gt;대신 API 서버가 인터넷에 그대로 노출된다&lt;/li&gt;
&lt;li&gt;학습이나 개발 환경용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# endpoint 설정 확인
aws eks describe-cluster --name myeks | jq '.cluster.resourcesVpcConfig | {endpointPublicAccess, endpointPrivateAccess}'
# &amp;rarr; endpointPublicAccess: true, endpointPrivateAccess: false

# 노드의 kubelet이 퍼블릭 IP로 API 서버에 연결 중
ssh ec2-user@$NODE1 sudo ss -tnp | grep kubelet
# &amp;rarr; Peer Address가 퍼블릭 IP (3.37.x.x 등)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;4.3 Public + Private 모드&quot; data-ke-size=&quot;size23&quot;&gt;4.3 Public + Private 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 가장 많이 쓰는 구성이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;사용자 kubectl &amp;rarr; (인터넷) &amp;rarr; NLB &amp;rarr; API 서버
워커 노드       &amp;rarr; (프라이빗, EKS Owned ENI) &amp;rarr; API 서버
API 서버       &amp;rarr; (EKS Owned ENI) &amp;rarr; 워커 노드 kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-heading=&quot;핵심: 같은 도메인인데 DNS 응답이 다르다&quot; data-ke-size=&quot;size20&quot;&gt;핵심: 같은 도메인인데 DNS 응답이 다르다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모드로 전환하면 AWS가 &lt;b&gt;Route 53 Private Hosted Zone&lt;/b&gt;을 자동으로 만든다. 같은 API 엔드포인트 도메인이라도:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질의 위치 DNS 응답 경유&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VPC 내부 (노드)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프라이빗 IP&lt;/b&gt; (EKS Owned ENI)&lt;/td&gt;
&lt;td&gt;VPC 내부 직접&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC 외부 (로컬 PC)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;퍼블릭 IP&lt;/b&gt; (NLB)&lt;/td&gt;
&lt;td&gt;인터넷&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동작이 되려면 VPC에서 &lt;b&gt;enableDnsSupport&lt;/b&gt;와 &lt;b&gt;enableDnsHostnames&lt;/b&gt;가 켜져 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eks.tf의 endpoint 설정을 변경한 뒤 terraform apply를 실행한다(약 7분 소요). 전환이 진행되는 동안 노드에서 dig를 반복 실행하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 응답이 퍼블릭 IP에서 프라이빗 IP로 바뀌는 순간을 포착할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전환 직후에는 기존 TCP 세션이 아직 퍼블릭 IP를 물고 있기 때문에, ss -tnp로 확인하면 Peer Address가 여전히 퍼블릭이다. kube-proxy와 aws-node를 rollout restart하고, kubelet도 재시작해야 비로소 프라이빗 IP로 전환되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Terraform 코드 변경:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;endpoint_public_access  = true
endpoint_private_access = true
endpoint_public_access_cidrs = [var.ssh_access_cidr]  # 소스 IP 제한
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전환 후 DNS 변화 확인:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# VPC 외부 (로컬)
dig +short $APIDNS
# &amp;rarr; 3.37.98.54, 3.39.80.185 (퍼블릭)

# VPC 내부 (노드)
ssh ec2-user@$NODE1 dig +short $APIDNS
# &amp;rarr; 192.168.2.185, 192.168.1.202 (프라이빗 = EKS Owned ENI IP)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-heading=&quot;기존 연결은 안 바뀐다 &amp;mdash; 컴포넌트 재시작 필수&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모드를 전환해도 이미 연결된 kubelet, kube-proxy, aws-node의 TCP 세션은 퍼블릭 IP를 계속 쓴다. 프라이빗 IP로 바꾸려면 &lt;b&gt;컴포넌트를 재시작&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# kube-proxy
kubectl rollout restart ds/kube-proxy -n kube-system

# aws-node
kubectl rollout restart ds/aws-node -n kube-system

# kubelet &amp;mdash; 노드에서 직접
for i in $NODE1 $NODE2; do
  ssh ec2-user@$i sudo systemctl restart kubelet
done

# 확인 &amp;mdash; Peer Address가 프라이빗 IP로 바뀜
ssh ec2-user@$NODE1 sudo ss -tnp | grep -v ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;4.4 Private 모드&quot; data-ke-size=&quot;size23&quot;&gt;4.4 Private 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안이 가장 강한 구성이다. 금융이나 의료처럼 규제가 엄격한 환경에서 쓴다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;사용자 kubectl &amp;rarr; VPC 안(bastion/VPN)에서만 접근
워커 노드       &amp;rarr; (프라이빗, EKS Owned ENI) &amp;rarr; API 서버
API 서버       &amp;rarr; (EKS Owned ENI) &amp;rarr; 워커 노드 kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 서버가 인터넷에 &lt;b&gt;아예 보이지 않는다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;외부에서 DNS 질의 자체가 되지 않는다&lt;/li&gt;
&lt;li&gt;VPN이나 bastion이 반드시 필요하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VPC Endpoint도 추가로 필요하다&lt;/b&gt;: ECR(dkr, api), S3, STS, CloudWatch Logs, EC2 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fully Private 클러스터를 배포하면(cd eks-private &amp;amp;&amp;amp; terraform apply, 약 16분 소요) 로컬에서 kubectl을 실행했을 때 i/o timeout이 발생한다. API 서버가 인터넷에 노출되지 않기 때문이다. bastion에 SSH로 접속한 뒤 kubectl을 실행하면 정상적으로 노드가 보인다. &lt;b&gt;클러스터 SG에 bastion SG의 HTTPS(443) 인바운드를 수동으로 추가&lt;/b&gt;해야 한다. 이 설정이 빠지면 bastion에서도 접근이 안 된다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 로컬에서 시도 &amp;rarr; 타임아웃
kubectl get node -v=7
# &amp;rarr; dial tcp 10.0.14.147:443: i/o timeout

# bastion으로 접속
ssh ubuntu@$(terraform output -raw bastion_ec2-public_ip)

# bastion에서 kubeconfig 잡고
kubectl get node
# &amp;rarr; Ready (프라이빗 IP로만 통신)

# DNS도 프라이빗 IP만 응답
dig +short $APIDNS
# &amp;rarr; 10.0.14.147, 10.0.28.171
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-heading=&quot;Fully Private에서 node-shell 트릭&quot; data-ke-size=&quot;size20&quot;&gt;Fully Private에서 node-shell 트릭&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH가 안 되는 프라이빗 노드에 들어가야 할 때, privileged 파드를 띄우고 chroot로 호스트에 진입한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;kubectl apply -f - &amp;lt;&amp;lt;EOF
apiVersion: v1
kind: Pod
metadata:
  name: node-shell
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: debug
    image: public.ecr.aws/docker/library/alpine:latest
    command: [&quot;sleep&quot;, &quot;36000&quot;]
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: hostfs
  volumes:
  - name: hostfs
    hostPath:
      path: /
EOF

# 호스트 진입
kubectl exec -it node-shell -- chroot /host /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;4.5 어떤 모드를 골라야 하나&quot; data-ke-size=&quot;size23&quot;&gt;4.5 어떤 모드를 골라야 하나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 추천 모드 이유&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;학습/개발&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;빠르게 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반 운영&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Public + Private&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;노드 트래픽은 내부로, 외부 접근은 CIDR 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;규제/보안&lt;/td&gt;
&lt;td&gt;Private&lt;/td&gt;
&lt;td&gt;API 서버가 인터넷에 아예 노출 안 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 모드를 직접 전환해 보면, Public+Private 모드에서 같은 도메인인데 DNS 응답이 달라지는 동작이 가장 인상적이다. Route 53 Private Hosted Zone이 VPC 안팎을 자동으로 구분해주는 구조를 확인하고 나면, 실무에서도 &lt;b&gt;Public+Private을 기본으로 가져가되 publicAccessCidrs로 소스 IP를 제한하는 방식&lt;/b&gt;이 가장 현실적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-heading=&quot;5. Add-on과 시스템 파드&quot; data-ke-size=&quot;size26&quot;&gt;5. Add-on과 시스템 파드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS를 배포하면 기본 Add-on 3종이 설치된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Add-on이 &quot;관리형&quot;이라는 의미&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: EKS Add-on은 AWS가 버전 호환성을 검증하고 자동 업데이트를 해준다. 직접 helm install로 설치한 것과 달리 EKS 콘솔에서 버전 관리가 되고 클러스터 업그레이드 시 호환 버전으로 같이 올라간다. 다만 커스텀 설정을 덮어쓸 수 있으니 configurationValues로 원하는 설정을 명시해두는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;aws eks list-addons --cluster-name myeks | jq
# &amp;rarr; [&quot;coredns&quot;, &quot;kube-proxy&quot;, &quot;vpc-cni&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Add-on 역할 배포 형태 특징&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;coredns&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 내부 DNS&lt;/td&gt;
&lt;td&gt;Deployment (2 replica)&lt;/td&gt;
&lt;td&gt;PDB로 가용성 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;kube-proxy&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;서비스 라우팅&lt;/td&gt;
&lt;td&gt;DaemonSet&lt;/td&gt;
&lt;td&gt;iptables 모드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;vpc-cni (aws-node)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파드 네트워킹&lt;/td&gt;
&lt;td&gt;DaemonSet&lt;/td&gt;
&lt;td&gt;컨테이너 2개: CNI + Network Policy Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인할 점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 이미지가 전부 &lt;b&gt;602401143452.dkr.ecr.ap-northeast-2.amazonaws.com&lt;/b&gt;에서 온다. 퍼블릭 레지스트리 없이 AWS 내부에서 완결된다.&lt;/li&gt;
&lt;li&gt;pause 컨테이너는 노드 로컬(localhost/kubernetes/pause)에 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 이미지 목록 확인
kubectl get pods --all-namespaces -o jsonpath=&quot;{.items[*].spec.containers[*].image}&quot; | tr -s '[[:space:]]' '\n' | sort | uniq -c
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;실습 중 자주 겪는 문제&quot; data-ke-size=&quot;size23&quot;&gt;실습 리소스 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 후 반드시 리소스를 삭제해서 불필요한 과금을 방지한다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 리소스 삭제 (약 10분 소요)
terraform destroy -auto-approve

# 삭제 확인
aws eks list-clusters --query 'clusters' --output text
# &amp;rarr; (빈 결과면 정상)
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>AWS/EKS</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/104</guid>
      <comments>https://hackjsp.tistory.com/104#entry104comment</comments>
      <pubDate>Thu, 19 Mar 2026 08:57:21 +0900</pubDate>
    </item>
    <item>
      <title>[ Claude Code ] WebFetch 사용 시 404 에러를 주의하세요(trailing slash)</title>
      <link>https://hackjsp.tistory.com/102</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code로 작업하다 보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서 참조할 때, MCP 서버 정보 확인할 때, 외부 레퍼런스 긁어올 때, WebFetch를 꽤 자주 쓰게 됩니다.&lt;br /&gt;그러다 이슈를 하나 발견했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분명히 존재하는 페이지인데 404 에러가 발생합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfFqgX/dJMcagLdekD/z1yuhMkkBj6fiCBZP1g04k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfFqgX/dJMcagLdekD/z1yuhMkkBj6fiCBZP1g04k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfFqgX/dJMcagLdekD/z1yuhMkkBj6fiCBZP1g04k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfFqgX%2FdJMcagLdekD%2Fz1yuhMkkBj6fiCBZP1g04k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;124&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제의 원인?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;URL 끝에 붙는 슬래시(/), 이른바 &lt;b&gt;트레일링 슬래시(trailing slash)&lt;/b&gt;가 문제였습니다.&lt;/p&gt;
&lt;table style=&quot;height: 58px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;URL&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;https://example.com/path/&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;404 에러&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;https://example.com/path&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;정상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;HTTP 스펙상 /path와 /path/는 서로 다른 리소스입니다. /path는 파일, /path/는 디렉터리를 가리킵니다.&lt;br /&gt;&lt;br /&gt;브라우저에서는 둘 다 잘 열리는데, 서버가 보내는 301 리다이렉트를 자동으로 따라가기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;WebFetch는 이 리다이렉트를 따라가지 못하고 그대로 실패합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 문제는&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 직접 슬래시를 명시하지 않아도, &lt;b&gt;에이전트가 알아서 붙이는 경우가 있습니다..&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프롬프트에 &lt;code&gt;&lt;span style=&quot;background-color: #e6f5ff; color: #333333; text-align: start;&quot;&gt;&lt;a href=&quot;https://example.com/path&quot;&gt;https://example.com/path&lt;/a&gt;&lt;/span&gt;&lt;/code&gt;라고 명시하였지만,&amp;nbsp; 에이전트가 실제로 fetch할 때는 끝에 &lt;code&gt;/&lt;/code&gt;를 붙여서 요청할때가 종종 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작업 상황을 유심히보지 않을 경우&lt;/b&gt;에 WebFetch 실패했는데 그걸 모른 채로 대화가 이어지면 중요한 콘텍스트가 누락될 수 있습니다(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;--dangerously-skip-permissions&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; bypass 모드 시, 주의)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;에 규칙 하나만 추가하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;## WebFetch 규칙
- URL 끝에는 trailing slash(`/`) 제거 후 요청합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 넣어두면 에이전트가 WebFetch 전에 trailing slash를 제거하고 요청합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zz6hc/dJMcahcfSVJ/pXN3NnXJGzCFdTZN3J5jg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zz6hc/dJMcahcfSVJ/pXN3NnXJGzCFdTZN3J5jg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zz6hc/dJMcahcfSVJ/pXN3NnXJGzCFdTZN3J5jg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzz6hc%2FdJMcahcfSVJ%2FpXN3NnXJGzCFdTZN3J5jg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1125&quot; height=&quot;286&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 Claude-Code의 기본 WebFetch 도구는 권한 이슈(가져오지 못하는 사이트가 많음)와 각종 오류가 많이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 fetch MCP나 jina reader mcp와 같은 대안 도구들을 많이 사용한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 들어, 토큰 절약을 위해, 에이전트를 위한 브라우저와 같은 서비스들이 출시되고 있는 것 같은데, 조금 더 기다려 봐야겠습니다.&lt;/p&gt;</description>
      <category>Lab</category>
      <category>claude-code</category>
      <category>trailing-slash</category>
      <category>webfetch</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/102</guid>
      <comments>https://hackjsp.tistory.com/102#entry102comment</comments>
      <pubDate>Wed, 11 Feb 2026 19:43:03 +0900</pubDate>
    </item>
    <item>
      <title>Istio 시리즈 # 13 - Istio Ambient Mode 살펴보기</title>
      <link>https://hackjsp.tistory.com/101</link>
      <description>&lt;h2 data-heading=&quot;Ambient Mesh란&quot; data-ke-size=&quot;size26&quot;&gt;Ambient Mesh란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio Ambient Mesh는 기존 사이드카 기반 구조를 대체하는 &lt;b&gt;사이드카리스(Sidecar-less)&lt;/b&gt; 서비스 메시 아키텍처다. 사이드카를 파드마다 주입하는 대신, 클러스터의 노드와 네임스페이스 단위로 프록시 컴포넌트를 분리 배치하여 &lt;b&gt;운영의 유연성&lt;/b&gt;, &lt;b&gt;리소스 효율성&lt;/b&gt;, &lt;b&gt;보안 격리&lt;/b&gt;를 크게 향상시킨다.&lt;br /&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W0Ppc/btsOrXg7I4M/Mdya8FRQM1nLKbQ0oK1QY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W0Ppc/btsOrXg7I4M/Mdya8FRQM1nLKbQ0oK1QY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W0Ppc/btsOrXg7I4M/Mdya8FRQM1nLKbQ0oK1QY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW0Ppc%2FbtsOrXg7I4M%2FMdya8FRQM1nLKbQ0oK1QY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;978&quot; height=&quot;415&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 새로운 방식은 &lt;b&gt;두 가지 계층으로 구성&lt;/b&gt;된다. 첫 번째는 &lt;b&gt;Secure Overlay&lt;/b&gt;로, &lt;code&gt;ztunnel&lt;/code&gt;이라는 경량 프록시를 통해 L4 수준에서 트래픽을 암호화하고 인증하는 역할을 한다. 두 번째는 &lt;b&gt;L7 Processing Layer&lt;/b&gt;로, HTTP 라우팅이나 인증, 로깅이 필요한 경우에만 &lt;code&gt;waypoint proxy&lt;/code&gt;를 통해 고급 기능을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 사이드카 방식과 달리, Ambient Mesh는 &lt;b&gt;애플리케이션과 메시 기능을 완전히 분리&lt;/b&gt;해 운영자가 서비스 중단 없이 메시를 확장&amp;middot;업그레이드할 수 있도록 설계되었다. 또한, 필요에 따라 &lt;b&gt;L4 전용 구성에서 L7 기능이 포함된 구성으로 점진적으로 도입&lt;/b&gt;할 수 있으며, 사이드카 모드와도 &lt;b&gt;혼합 구성이 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성 방식&lt;/th&gt;
&lt;th&gt;사이드카 모드&lt;/th&gt;
&lt;th&gt;앰비언트 모드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프록시 배치&lt;/td&gt;
&lt;td&gt;Pod 내부 (Envoy)&lt;/td&gt;
&lt;td&gt;노드(zTunnel), 네임스페이스(Waypoint)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트래픽 처리&lt;/td&gt;
&lt;td&gt;L4~L7 일괄 처리&lt;/td&gt;
&lt;td&gt;L4: zTunnel, L7: Waypoint Proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리소스 사용&lt;/td&gt;
&lt;td&gt;Pod 개수만큼 증가&lt;/td&gt;
&lt;td&gt;공유 자원 기반, 자동 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 방식&lt;/td&gt;
&lt;td&gt;사이드카 주입 필요&lt;/td&gt;
&lt;td&gt;비침입적 구성, 주입 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;기존 사이드카 방식의 한계&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 사이드카(Sidecar) 구조는 Istio의 핵심 기능을 각 애플리케이션 포드 옆에 Envoy 프록시를 주입하는 방식으로 제공하지만, 이로 인해 &lt;b&gt;침입적인 아키텍처, 과도한 리소스 소모, 복잡한 운영 부담이라는 문제가 발생&lt;/b&gt;한다. 사이드카는 모든 포드에 강제로 붙기 때문에 애플리케이션의 배포나 디버깅 시 예기치 않은 트래픽 흐름을 유발할 수 있고, 각 사이드카가 독립적으로 리소스를 소비하면서 클러스터 전체의 자원 효율이 떨어진다. 또한 사이드카의 버전 업그레이드나 장애 대응 시에는 &lt;b&gt;전체 워크로드의 재배포&lt;/b&gt;가 필요해 운영이 번거롭고 복잡해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ambient Mesh는 이러한 한계를 극복하기 위해 &lt;b&gt;사이드카 없는 구조로 설계&lt;/b&gt;되어, 보안과 성능은 유지하면서도 더 유연하고 단순한 메시 환경을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;Istio Ambient Mode와 Kubernetes CNI 간 충돌 해결&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;Ambient Mesh 아키텍처 초기에는 중요한 문제가 있었다. 다양한 Kubernetes 플랫폼과 네트워크 구현 환경에서 CNI 플러그인이 사용하는 커널 수준 리디렉션과 ztunnel의 사용자 공간 리디렉션이 충돌을 일으킬 수 있었던 것이다. 이러한 충돌을 해결하기 위해 Istio는 &lt;b&gt;ztunnel이 포드와 동일한 네트워크 네임스페이스 내에서 리디렉션 소켓을 직접 생성&lt;/b&gt;하도록 아키텍처를 개선했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 사이드카 방식에서는 포드 내부에서 사이드카와 앱이 동일한 네임스페이스를 공유하므로 트래픽 리디렉션이 간단했다. 이를 바탕으로, Istio는 &lt;b&gt;사이드카 방식처럼 ztunnel이 애플리케이션 포드의 네트워크 네임스페이스 안에서 리디렉션 소켓을 시작&lt;/b&gt;하도록 설계했다. 놀랍게도 Linux 소켓 API는 &lt;b&gt;한 네트워크 네임스페이스 외부에서 내부로 진입하여 소켓을 리스닝할 수 있는 기능&lt;/b&gt;을 제공하며, 이 점이 ztunnel 구조 전환의 핵심이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Istio는 &lt;b&gt;ztunnel과 Istio-CNI 노드 에이전트 구조를 리팩토링&lt;/b&gt;했고, 다양한 Kubernetes 환경에서 프로토타입을 검증한 뒤, &lt;b&gt;Pod 내부 리디렉션을 구현한 새로운 아키텍처를 upstream에 반영&lt;/b&gt;하였다. 이로써 ztunnel은 포드 외부에서 실행되면서도 포드 내부 네임스페이스에서 소켓을 소유하는 형태로 작동하게 되었고, 사이드카 모델과 매우 유사한 방식으로 트래픽을 처리할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 장점은 이러한 방식이 eBPF든 iptables든 모든 주요 CNI 구현체와 충돌 없이 동작하며, Kubernetes 기본 &lt;b&gt;네트워크 정책이 그대로 유지될 수 있다는 점&lt;/b&gt;이다. 덕분에 Ambient 모드는 &lt;b&gt;보안성과 호환성을 모두 확보&lt;/b&gt;하면서도, 사이드카 없는 경량 구조의 이점을 활용할 수 있게 되었다. ztunnel은 단순한 보안 프록시를 넘어, Istio Ambient Mesh 아키텍처를 가능하게 한 기술적 핵심이라 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;Istio Ambient Mode, v1.24에서 정식 출시(GA)&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;Istio Ambient 모드는 Istio 1.24 버전부터 &lt;b&gt;공식 GA(General Availability)&lt;/b&gt; 가 되었다.&lt;br /&gt;이제 Ambient 모드를 사용하면 사이드카 없이도 Istio의 핵심 기능을 누릴 수 있다. 복잡한 프록시 배포 없이도 메시 기반의 &lt;b&gt;보안 정책 적용&lt;/b&gt;, &lt;b&gt;트래픽 관찰&lt;/b&gt;, &lt;b&gt;서비스 간 통신 제어&lt;/b&gt;가 가능해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 특징은 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 빠른 성능 (Fast)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;L7 처리가 꼭 필요한 경우에만 waypoint proxy를 사용하고, 기본 통신은 ztunnel에서 L4 수준으로 처리하기 때문에 불필요한 부하를 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;기존 사이드카 방식보다 평균 지연 시간이 줄어드는 효과가 보고되었으며, 내부적으로는 엣지 간 처리 단계를 통합하여 홉 수를 줄이는 방식으로 최적화되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 보안 강화 (Secure)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mTLS 기반의 워크로드 간 통신 암호화는 기본값으로 제공되며, compromised workload와 메시 인프라(ztunnel, waypoint)를 격리할 수 있는 아키텍처 덕분에 전체 시스템의 보안성이 높아진다.&lt;/li&gt;
&lt;li&gt;Envoy 기반의 안정적인 프록시 구성과 Kubernetes RBAC과 연동되는 인증/인가 체계가 기본으로 제공된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 간결한 운영 (Simple)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 이상 각 워크로드마다 사이드카를 삽입하고 관리할 필요가 없기 때문에, 운영 부담이 대폭 줄어든다.&lt;/li&gt;
&lt;li&gt;네트워크 정책과 트래픽 설정은 네임스페이스 단위 또는 서비스 계정 단위로 통합 적용할 수 있어 구성 복잡도도 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio 1.24 GA는 단순히 기능이 안정화되었다는 의미를 넘어, &lt;b&gt;프로덕션 환경에서도 안전하게 사용할 수 있는 신뢰 수준&lt;/b&gt;에 도달했음을 의미한다. 다양한 Kubernetes 환경과 CNI에서의 테스트, Rust 기반 ztunnel의 리팩토링, 세분화된 보안 정책 등 지금까지의 개선이 모두 이 GA 선언으로 이어진 것이다.&lt;/p&gt;
&lt;h2 data-heading=&quot;Istio Ambient Ztunnel&quot; data-ke-size=&quot;size26&quot;&gt;Istio Ambient Ztunnel&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio Ambient Mesh의 핵심 구성 요소 중 하나는 ztunnel이라는 새로운 컴포넌트다. 이 컴포넌트는 기존 사이드카 없이도 메시 내부의 워크로드 간 통신을 &lt;b&gt;보안(mTLS)&lt;/b&gt; 기반으로 보호해주는 역할을 한다.&lt;br /&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;1008&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0jAWD/btsOtnFoix3/az6KaItDKV1JPl1J7esFRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0jAWD/btsOtnFoix3/az6KaItDKV1JPl1J7esFRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0jAWD/btsOtnFoix3/az6KaItDKV1JPl1J7esFRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0jAWD%2FbtsOtnFoix3%2Faz6KaItDKV1JPl1J7esFRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1488&quot; height=&quot;1008&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;1008&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;br /&gt;기존 Istio는 각 워크로드 옆에 사이드카 프록시(Envoy)를 배치했지만, 앰비언트 모드에서는 각 노드에 공유 형태로 ztunnel을 배포한다. 모든 워크로드의 트래픽은 ztunnel을 거쳐 흐르게 되고, 이 과정에서 트래픽은 자동으로 암호화된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 점은 이 ztunnel이 &lt;b&gt;Rust 언어로 구현되었다는 것&lt;/b&gt;이다. Rust는 C/C++ 수준의 고성능을 가지면서도 &lt;b&gt;메모리 안전성을 컴파일 타임에 보장&lt;/b&gt;한다. 덕분에 보안이 중요한 메시 계층에서 더 신뢰할 수 있는 동작을 기대할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rust 기반 ztunnel의 장점은 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;낮은 리소스 사용량과 빠른 시작 시간&lt;/b&gt;: 단일 바이너리로 컴파일되기 때문에 컨테이너 이미지가 작고 배포도 빠르다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고성능 트래픽 처리&lt;/b&gt;: 멀티스레딩을 효율적으로 활용하며, 수많은 워크로드 트래픽을 안정적으로 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안성과 격리성 향상&lt;/b&gt;: 메모리 오류나 데이터 경쟁을 방지하며, 민감한 보안 계층에서도 신뢰할 수 있는 기반을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 ztunnel은 사이드카 없이도 보안 메시를 구성할 수 있게 하면서, 성능과 보안을 모두 만족시키는 &lt;b&gt;앰비언트 메시의 경량 프록시&lt;/b&gt; 역할을 하게 된다. 기존 Istio 사용자가 가장 부담스러워했던 사이드카의 리소스 오버헤드 문제를 크게 줄이면서, 보다 단순하고 확장 가능한 아키텍처로의 전환을 가능하게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ambient Mode에서 보안은 어떻게 작동할까?&lt;/b&gt;&lt;br /&gt;앰비언트 모드에서는 보안 모델 역시 기존 사이드카 방식과는 전혀 다르게 설계되었다. 기본적으로 &lt;b&gt;ztunnel이 모든 워크로드의 입출력 트래픽을 가로채어 mTLS로 암호화&lt;/b&gt;하며, 이를 통해 메시 내에서 &lt;b&gt;제로 트러스트(Zero Trust)&lt;/b&gt; 보안을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아키텍처의 가장 큰 특징은 보안 계층이 &lt;b&gt;워크로드와 격리된 별도의 컴포넌트(ztunnel, waypoint)에서 수행된다는 점&lt;/b&gt;이다. 이를 통해 사이드카 모델에서 자주 지적되던 몇 가지 문제를 해결할 수 있다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워크로드 침해 시에도 정책은 그대로&lt;/b&gt;&lt;br /&gt;기존 사이드카 방식은 워크로드와 Envoy 프록시가 같은 Pod 안에 있었기 때문에, 애플리케이션이 침해되면 해당 사이드카를 통해 메시 네트워크 전체가 위협받을 가능성이 있었다. 반면, 앰비언트 모드에서는 ztunnel이 별도의 Pod로 격리되어 있으며, 애플리케이션이 손상되어도 &lt;b&gt;ztunnel의 보안 정책은 여전히 유효하게 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최소 권한으로, 더 안전하게&lt;/b&gt;&lt;br /&gt;ztunnel은 자신이 위치한 노드의 워크로드 키만 접근 가능하다. 즉, &lt;b&gt;노드 단위의 제한된 권한 모델&lt;/b&gt;을 따르기 때문에 공격 반경이 제한적이다. 이는 일반적인 CNI 수준의 암호화 모델과 유사하며, 전체 클러스터를 위험에 빠뜨릴 가능성을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, ztunnel은 오직 &lt;b&gt;L4 계층까지만 처리하도록 제한되어 있기 때문에&lt;/b&gt;, 복잡한 L7 로직에서 발생할 수 있는 보안 취약점으로부터도 비교적 안전하다. 트래픽이 애플리케이션 레벨의 세부 요청을 처리하는 waypoint proxy로 넘어가는 경우에도, 이 프록시는 &lt;b&gt;하나의 서비스 계정에 대해서만 작동하도록 구성 가능&lt;/b&gt;해 보안적 격리를 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Envoy 대신 Rust를 선택한 이유도 보안&lt;/b&gt;&lt;br /&gt;Rust로 구현된 ztunnel은 메모리 안전성과 thread-safe한 구조 덕분에, &lt;b&gt;런타임에서 발생하는 잠재적인 메모리 취약점이 크게 줄어든다.&lt;/b&gt; 특히 공유 프록시 구조에서 중요한 리소스를 안전하게 다루기 위해서는 이러한 특성이 큰 장점이다.&lt;/p&gt;
&lt;h2 data-heading=&quot;Istio Ambient Waypoint Proxy&quot; data-ke-size=&quot;size26&quot;&gt;Istio Ambient Waypoint Proxy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio Ambient Mesh는 기존 사이드카 아키텍처의 복잡성과 확장성 문제를 해결하기 위해 &lt;b&gt;Waypoint 프록시&lt;/b&gt;를 도입했다. 이 프록시는 Envoy 기반으로, &lt;b&gt;레이어 7(L7) 트래픽 처리&lt;/b&gt;를 담당하는 선택적 구성 요소다. 웨이포인트는 &lt;b&gt;네임스페이스 또는 서비스 계정 단위로 배포&lt;/b&gt;되며, &lt;b&gt;각 애플리케이션 파드 외부&lt;/b&gt;에서 실행되기 때문에 &lt;b&gt;업그레이드와 관리가 독립적&lt;/b&gt;이다.&lt;br /&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1875&quot; data-origin-height=&quot;777&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRyDkC/btsOs22yLDW/ne8oH3eKCuf2HZgSk508w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRyDkC/btsOs22yLDW/ne8oH3eKCuf2HZgSk508w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRyDkC/btsOs22yLDW/ne8oH3eKCuf2HZgSk508w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRyDkC%2FbtsOs22yLDW%2Fne8oH3eKCuf2HZgSk508w0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1875&quot; height=&quot;777&quot; data-origin-width=&quot;1875&quot; data-origin-height=&quot;777&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;br /&gt;특히 정책 적용 구조가 바뀌었다. 기존에는 &lt;b&gt;트래픽 관련 정책은 클라이언트 사이드카에서&lt;/b&gt;, &lt;b&gt;보안 정책은 서버 사이드카에서 처리&lt;/b&gt;되었지만, Ambient 모드에서는 &lt;b&gt;모든 정책을 대상(Waypoint) 프록시에서 적용&lt;/b&gt;한다. 이는 디버깅, 스케일링, 정책 일관성 측면에서 많은 이점을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Waypoint 모델은 구성 전파량을 크게 줄인다. 예를 들어 두 개의 네임스페이스에 각각 2개의 워크로드가 있을 경우:&lt;br /&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbCLxf/btsOs3NXf7G/2O2rqgeV2cDKU92ZGvdV11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbCLxf/btsOs3NXf7G/2O2rqgeV2cDKU92ZGvdV11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbCLxf/btsOs3NXf7G/2O2rqgeV2cDKU92ZGvdV11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbCLxf%2FbtsOs3NXf7G%2F2O2rqgeV2cDKU92ZGvdV11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1009&quot; height=&quot;516&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사이드카 모델은 각 워크로드가 모든 목적지에 대한 구성을 알아야 하므로 총 16개의 구성이 필요&lt;/li&gt;
&lt;li&gt;웨이포인트 모델은 네임스페이스당 1개의 웨이포인트만 구성되므로 총 2개만&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 환경에서 이 차이는 더 극명해진다. 이처럼 Waypoint 구조는 &lt;b&gt;제어 플레인과 데이터 플레인 리소스 사용량(CPU, RAM, 네트워크 트래픽)을 획기적으로 줄인다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Waypoint는 istioctl experimental waypoint generate 명령어 혹은 Kubernetes Gateway 리소스를 통해 선언적으로 배포할 수 있다. Istiod는 이 리소스를 감지하고 자동으로 프록시를 배포 및 구성한다. 이 방식은 기존의 복잡한 사이드카 설정을 대체하며 운영 편의성과 확장성을 동시에 확보할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;$ istioctl experimental waypoint generate
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: namespace
spec:
  gatewayClassName: istio-waypoint
  listeners:
  - name: mesh
    port: 15008
    protocol: HBONE
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;Ambient Mode 실습&quot; data-ke-size=&quot;size26&quot;&gt;Ambient Mode 실습&lt;/h2&gt;
&lt;h3 data-heading=&quot;실습환경구성&quot; data-ke-size=&quot;size23&quot;&gt;실습환경구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio Ambient 모드를 테스트하기 위해 kind 기반의 로컬 Kubernetes 클러스터를 구성한다. 실습은 1개의 컨트롤 플레인 노드와 2개의 워커 노드로 이루어진 3노드 클러스터 환경에서 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kind 클러스터 생성&lt;/b&gt;&lt;br /&gt;다양한 시각화 도구와 샘플 앱을 노출하기 위해 NodePort 포트 매핑을 추가한 컨트롤 플레인 노드, 그리고 일반 워커 노드 2대를 구성한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 
kind create cluster --name myk8s --image kindest/node:v1.32.2 --config - &amp;lt;&amp;lt;EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000 # Sample Application
    hostPort: 30000
  - containerPort: 30001 # Prometheus
    hostPort: 30001
  - containerPort: 30002 # Grafana
    hostPort: 30002
  - containerPort: 30003 # Kiali
    hostPort: 30003
  - containerPort: 30004 # Tracing
    hostPort: 30004
  - containerPort: 30005 # kube-ops-view
    hostPort: 30005
- role: worker
- role: worker
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
EOF

# 설치 확인
docker ps

# 노드에 기본 툴 설치
for node in control-plane worker worker2; do echo &quot;node : myk8s-$node&quot; ; docker exec -it myk8s-$node sh -c 'apt update &amp;amp;&amp;amp; apt install tree psmisc lsof ipset wget bridge-utils net-tools dnsutils tcpdump ngrep iputils-ping git vim -y'; echo; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트용 외부 PC 컨테이너 배포&lt;/b&gt;&lt;br /&gt;Istio 메시 외부의 트래픽 시뮬레이션을 위해 netshoot 이미지 기반의 컨테이너(mypc)를 생성하고, kind 네트워크에 붙인다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.18.0.0/16 대역
docker network ls
docker inspect kind

# '테스트용 PC(mypc)' 컨테이너 기동 : kind 도커 브리지를 사용
혹은 IP 지정 실행 시 에러 발생 시 아래 처럼 IP 지정 없이 실행
docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity # IP 지정 없이 실행 시
docker ps

# kind network 중 컨테이너(노드) IP(대역) 확인
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'
/mypc 192.168.107.3
/myk8s-worker2 192.168.107.6
/myk8s-control-plane 192.168.107.2
/myk8s-worker 192.168.107.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MetalLB 설치&lt;/b&gt;&lt;br /&gt;편리한 실습을 위해, MetalLB를 설치해 서비스 타입 LoadBalancer를 지원하도록 설정한다.&lt;br /&gt;실습에서는 자신의 도커 브리지 대역(예: 192.168.107.0/24)을 활용해 IP 풀을 구성하고, Layer2 방식으로 announce한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# MetalLB 배포
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml


# 확인
kubectl get crd
kubectl get pod -n metallb-system


# IPAddressPool, L2Advertisement 설정
cat &amp;lt;&amp;lt; EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default
  namespace: metallb-system
spec:
  addresses:
  - 192.168.107.101-192.168.107.120 # 자신의 도커브릿지 네트워크 대역 사용
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
  - default
EOF

# 확인
kubectl get IPAddressPool,L2Advertisement -A

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Istio 1.26 설치 (Ambient 프로파일)&lt;/b&gt;&lt;br /&gt;Ambient 모드를 실습하기 위해, Istio 1.26 버전을 ambient 프로파일로 설치한다. 이 프로파일은 ztunnel과 기본 컨트롤 플레인만 배포되며, 사이드카 프록시는 포함되지 않는다.&lt;br /&gt;Gateway API CRD도 함께 설치하고, Prometheus, Grafana, Kiali, Tracing 등 기본 모니터링 애드온을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# myk8s-control-plane 진입 후 설치 진행
docker exec -it myk8s-control-plane bash
-----------------------------------
# istioctl 설치
export ISTIOV=1.26.0
echo 'export ISTIOV=1.26.0' &amp;gt;&amp;gt; /root/.bashrc

curl -s -L https://istio.io/downloadIstio | ISTIO_VERSION=$ISTIOV sh -
cp istio-$ISTIOV/bin/istioctl /usr/local/bin/istioctl
istioctl version --remote=false
client version: 1.26.0

# ambient 프로파일 컨트롤 플레인 배포
istioctl install --set profile=ambient --set meshConfig.accessLogFile=/dev/stdout --skip-confirmation
istioctl install --set profile=ambient --set meshConfig.enableTracing=true -y

# Install the Kubernetes Gateway API CRDs
kubectl get crd gateways.gateway.networking.k8s.io &amp;amp;&amp;gt; /dev/null || \
  kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml

# 보조 도구 설치
kubectl apply -f istio-$ISTIOV/samples/addons
kubectl apply -f istio-$ISTIOV/samples/addons # nodePort 충돌 시 한번 더 입력

# 빠져나오기
exit
-----------------------------------

# 설치 확인 : istiod, istio-ingressgateway, crd 등
kubectl get all,svc,ep,sa,cm,secret,pdb -n istio-system
kubectl get crd | grep istio.io
kubectl get crd | grep -v istio | grep -v metallb
kubectl get crd  | grep gateways
gateways.gateway.networking.k8s.io          2025-06-01T04:54:23Z
gateways.networking.istio.io                2025-06-01T04:53:51Z

kubectl api-resources | grep Gateway
gatewayclasses                      gc           gateway.networking.k8s.io/v1        false        GatewayClass
gateways                            gtw          gateway.networking.k8s.io/v1        true         Gateway
gateways                            gw           networking.istio.io/v1              true         Gateway

kubectl describe cm -n istio-system istio
...
Data
====
mesh:
----
accessLogFile: /dev/stdout
defaultConfig:
  discoveryAddress: istiod.istio-system.svc:15012
defaultProviders:
  metrics:
  - prometheus
enablePrometheusMerge: true
...

docker exec -it myk8s-control-plane istioctl proxy-status
NAME                           CLUSTER        CDS         LDS         EDS         RDS         ECDS        ISTIOD                     VERSION
ztunnel-6ddsp.istio-system     Kubernetes     IGNORED     IGNORED     IGNORED     IGNORED     IGNORED     istiod-86b6b7ff7-gbjv7     1.26.0
ztunnel-86ch2.istio-system     Kubernetes     IGNORED     IGNORED     IGNORED     IGNORED     IGNORED     istiod-86b6b7ff7-gbjv7     1.26.0
ztunnel-m2ct2.istio-system     Kubernetes     IGNORED     IGNORED     IGNORED     IGNORED     IGNORED     istiod-86b6b7ff7-gbjv7     1.26.0

docker exec -it myk8s-control-plane istioctl ztunnel-config workload
docker exec -it myk8s-control-plane istioctl ztunnel-config service

# iptables 규칙 확인
for node in control-plane worker worker2; do echo &quot;node : myk8s-$node&quot; ; docker exec -it myk8s-$node sh -c 'iptables-save'; echo; done


# NodePort 변경 및 nodeport 30001~30003으로 변경 : prometheus(30001), grafana(30002), kiali(30003), tracing(30004)
kubectl patch svc -n istio-system prometheus -p '{&quot;spec&quot;: {&quot;type&quot;: &quot;NodePort&quot;, &quot;ports&quot;: [{&quot;port&quot;: 9090, &quot;targetPort&quot;: 9090, &quot;nodePort&quot;: 30001}]}}'
kubectl patch svc -n istio-system grafana -p '{&quot;spec&quot;: {&quot;type&quot;: &quot;NodePort&quot;, &quot;ports&quot;: [{&quot;port&quot;: 3000, &quot;targetPort&quot;: 3000, &quot;nodePort&quot;: 30002}]}}'
kubectl patch svc -n istio-system kiali -p '{&quot;spec&quot;: {&quot;type&quot;: &quot;NodePort&quot;, &quot;ports&quot;: [{&quot;port&quot;: 20001, &quot;targetPort&quot;: 20001, &quot;nodePort&quot;: 30003}]}}'
kubectl patch svc -n istio-system tracing -p '{&quot;spec&quot;: {&quot;type&quot;: &quot;NodePort&quot;, &quot;ports&quot;: [{&quot;port&quot;: 80, &quot;targetPort&quot;: 16686, &quot;nodePort&quot;: 30004}]}}'

# Prometheus 접속 : envoy, istio 메트릭 확인
open http://127.0.0.1:30001

# Grafana 접속
open http://127.0.0.1:30002

# Kiali 접속 : NodePort
open http://127.0.0.1:30003

# tracing 접속 : 예거 트레이싱 대시보드
open http://127.0.0.1:30004
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ztunnel 상태 확인&lt;/b&gt;&lt;br /&gt;ztunnel이 정상적으로 배포되었는지, istiod 컨트롤 플레인과의 연동 상태는 정상인지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; istioctl proxy-status
NAME                           CLUSTER        CDS         LDS         EDS         RDS         ECDS        ISTIOD                     VERSION
ztunnel-6ddsp.istio-system     Kubernetes     IGNORED     IGNORED     IGNORED     IGNORED     IGNORED     istiod-86b6b7ff7-gbjv7     1.26.0
ztunnel-86ch2.istio-system     Kubernetes     IGNORED     IGNORED     IGNORED     IGNORED     IGNORED     istiod-86b6b7ff7-gbjv7     1.26.0
ztunnel-m2ct2.istio-system     Kubernetes     IGNORED     IGNORED     IGNORED     IGNORED     IGNORED     istiod-86b6b7ff7-gbjv7     1.26.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워크로드와 서비스 ztunnel 매핑 상태 확인&lt;/b&gt;&lt;br /&gt;Istio 1.26부터 새롭게 추가된 ztunnel-config 명령어를 활용하면, 각 워크로드와 서비스가 ztunnel을 통해 어떤 식으로 관찰되고 있는지 확인할 수 있다.&lt;br /&gt;TCP&amp;rdquo;로 표기된 워크로드는 아직 waypoint proxy 설정이 없는 초기 상태라고 보면 되고, 이후 실습을 통해 waypoint proxy를 설정하면 &quot;HBONE&quot;으로 바뀌게 된다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;  istioctl ztunnel-config workload
NAMESPACE          POD NAME                                    ADDRESS       NODE                WAYPOINT PROTOCOL
...
istio-system       grafana-65bfb5f855-2j8mp                    10.10.1.5     myk8s-worker2       None     TCP
istio-system       istio-cni-node-2mqpq                        10.10.0.5     myk8s-control-plane None     TCP
istio-system       istio-cni-node-phcpx                        10.10.1.3     myk8s-worker2       None     TCP
...

 istioctl ztunnel-config service
NAMESPACE      SERVICE NAME            SERVICE VIP  WAYPOINT ENDPOINTS
default        kubernetes              10.200.1.1   None     1/1
istio-system   grafana                 10.200.1.194 None     1/1
istio-system   istiod                  10.200.1.161 None     1/1
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ztunnel과 istio-cni-node 데몬셋 확인&lt;/b&gt;&lt;br /&gt;Ambient Mesh가 정상 작동하려면 두 가지 핵심 컴포넌트가 각 노드에 제대로 배포되어 있어야 한다: istio-cni-node와 ztunnel. 각각의 역할은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;istio-cni-node&lt;/b&gt;: CNI 플러그인으로서 각 Pod의 네트워크 설정을 가로채고, ztunnel로 트래픽이 흐르도록 구성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ztunnel&lt;/b&gt;: Rust로 작성된 lightweight 프록시로, L4 레벨의 암호화된 터널링과 기본적인 라우팅을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 데몬셋 배포 상태를 확인해보면 다음과 같다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; kubectl get ds -n istio-system

NAME             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
istio-cni-node   3         3         3       3            3           kubernetes.io/os=linux   5m35s
ztunnel          3         3         3       3            3           kubernetes.io/os=linux   5m25s
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i&lt;/b&gt;&lt;b&gt;stio-cni-node 구성 상세 확인&lt;/b&gt;&lt;br /&gt;여기서는 &lt;code&gt;/opt/cni/bin&lt;/code&gt; 경로에 istio-cni 바이너리가 존재하는지, &lt;code&gt;/etc/cni/net.d&lt;/code&gt;에 CNI 설정 파일이 있는지 등을 점검하면 좋다. &lt;code&gt;/var/run/istio-cni&lt;/code&gt; 경로는 CNI 로그, 소켓, kubeconfig 파일들이 위치한 곳이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 내에서의 상태는 다음과 같은 환경 변수들로 드러난다:&lt;br /&gt;&amp;bull; REPAIR_NODE_NAME, REPAIR_RUN_AS_DAEMON 등 네트워크 설정 복구와 관련된 변수&lt;br /&gt;&amp;bull; &lt;code&gt;/var/run/ztunnel&lt;/code&gt; 마운트를 통해 &lt;b&gt;ztunnel과 소켓 통신이 가능&lt;/b&gt;하도록 설정됨&lt;/p&gt;
&lt;pre class=&quot;tcl&quot;&gt;&lt;code&gt; kubectl describe pod -n istio-system -l k8s-app=istio-cni-node
...
Containers:
  install-cni:
    Container ID:  containerd://0cdb0dec8fd3d4bc41f64daec3b807eb48d1d5db3ee8314b74272d1ce5926ae7
    Image:         docker.io/istio/install-cni:1.26.0-distroless
    Image ID:      docker.io/istio/install-cni@sha256:e69cea606f6fe75907602349081f78ddb0a94417199f9022f7323510abef65cb
    Port:          15014/TCP
    Host Port:     0/TCP
    Command:
      install-cni
    Args:
      --log_output_level=info
    State:          Running
      Started:      Mon, 09 Jun 2025 03:32:03 +0900
    Ready:          True
    Restart Count:  0
    Requests:
      cpu:      100m
      memory:   100Mi
    Readiness:  http-get http://:8000/readyz delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment Variables from:
      istio-cni-config  ConfigMap  Optional: false
    Environment:
      REPAIR_NODE_NAME:            (v1:spec.nodeName)
      REPAIR_RUN_AS_DAEMON:       true
      REPAIR_SIDECAR_ANNOTATION:  sidecar.istio.io/status
      ALLOW_SWITCH_TO_HOST_NS:    true
      NODE_NAME:                   (v1:spec.nodeName)
      GOMEMLIMIT:                 node allocatable (limits.memory)
      GOMAXPROCS:                 node allocatable (limits.cpu)
      POD_NAME:                   istio-cni-node-ptw7d (v1:metadata.name)
      POD_NAMESPACE:              istio-system (v1:metadata.namespace)
    Mounts:
      /host/etc/cni/net.d from cni-net-dir (rw)
      /host/opt/cni/bin from cni-bin-dir (rw)
      /host/proc from cni-host-procfs (ro)
      /host/var/run/netns from cni-netns-dir (rw)
      /var/run/istio-cni from cni-socket-dir (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-bmsmd (ro)
      /var/run/ztunnel from cni-ztunnel-sock-dir (rw)
...
Volumes:
  cni-bin-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /opt/cni/bin
    HostPathType:
  cni-host-procfs:
    Type:          HostPath (bare host directory volume)
    Path:          /proc
    HostPathType:  Directory
  cni-ztunnel-sock-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/run/ztunnel
    HostPathType:  DirectoryOrCreate
  cni-net-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /etc/cni/net.d
    HostPathType:
  cni-socket-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/run/istio-cni
    HostPathType:
  cni-netns-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/run/netns
    HostPathType:  DirectoryOrCreate

# 노드에서 기본 정보 확인
for node in control-plane worker worker2; do echo &quot;node : myk8s-$node&quot; ; docker exec -it myk8s-$node sh -c 'ls -l /opt/cni/bin'; echo; done
-rwxr-xr-x 1 root root 52428984 Jun  1 05:42 istio-cni
...

for node in control-plane worker worker2; do echo &quot;node : myk8s-$node&quot; ; docker exec -it myk8s-$node sh -c 'ls -l /etc/cni/net.d'; echo; done
-rw-r--r-- 1 root root 862 Jun  1 04:54 10-kindnet.conflist


for node in control-plane worker worker2; do echo &quot;node : myk8s-$node&quot; ; docker exec -it myk8s-$node sh -c 'ls -l /var/run/istio-cni'; echo; done
-rw------- 1 root root 2990 Jun  1 05:42 istio-cni-kubeconfig
-rw------- 1 root root  171 Jun  1 04:54 istio-cni.log
srw-rw-rw- 1 root root    0 Jun  1 04:54 log.sock
srw-rw-rw- 1 root root    0 Jun  1 04:54 pluginevent.sock

for node in control-plane worker worker2; do echo &quot;node : myk8s-$node&quot; ; docker exec -it myk8s-$node sh -c 'ls -l /var/run/netns'; echo; done
...

for node in control-plane worker worker2; do echo &quot;node : myk8s-$node&quot; ; docker exec -it myk8s-$node sh -c 'lsns -t net'; echo; done


# istio-cni-node 데몬셋 파드 로그 확인
kubectl logs  -n istio-system -l k8s-app=istio-cni-node -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ztunnel Pod 상태 및 내부 정보 확인&lt;/b&gt;&lt;br /&gt;Pod 내부에서는 proxy ztunnel 명령어로 구동되고 있으며, HBONE 활성화 여부는 ISTIO_META_ENABLE_HBONE=true로 확인 가능하다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;kubectl get pod -n istio-system -l app=ztunnel -owide
kubectl get pod -n istio-system -l app=ztunnel
ZPOD1NAME=$(kubectl get pod -n istio-system -l app=ztunnel -o jsonpath=&quot;{.items[0].metadata.name}&quot;)
ZPOD2NAME=$(kubectl get pod -n istio-system -l app=ztunnel -o jsonpath=&quot;{.items[1].metadata.name}&quot;)
ZPOD3NAME=$(kubectl get pod -n istio-system -l app=ztunnel -o jsonpath=&quot;{.items[2].metadata.name}&quot;)
echo $ZPOD1NAME $ZPOD2NAME $ZPOD3NAME

kubectl describe pod -n istio-system -l app=ztunnel
...
Containers:
  istio-proxy:
    Container ID:  containerd://7903d6a0a20d5cefbe427f03e9698c43f97fe4633d6ab560a8836aad76ac087b
    Image:         docker.io/istio/ztunnel:1.26.0-distroless
    Image ID:      docker.io/istio/ztunnel@sha256:d711b5891822f4061c0849b886b4786f96b1728055333cbe42a99d0aeff36dbe
    Port:          15020/TCP
    Host Port:     0/TCP
    Args:
      proxy
      ztunnel
    State:          Running
      Started:      Mon, 09 Jun 2025 03:32:12 +0900
    Ready:          True
    Restart Count:  0
    Requests:
      cpu:      200m
      memory:   512Mi
    Readiness:  http-get http://:15021/healthz/ready delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment:
      CA_ADDRESS:                        istiod.istio-system.svc:15012
      XDS_ADDRESS:                       istiod.istio-system.svc:15012
      RUST_LOG:                          info
      RUST_BACKTRACE:                    1
      ISTIO_META_CLUSTER_ID:             Kubernetes
      INPOD_ENABLED:                     true
      TERMINATION_GRACE_PERIOD_SECONDS:  30
      POD_NAME:                          ztunnel-m2ct2 (v1:metadata.name)
      POD_NAMESPACE:                     istio-system (v1:metadata.namespace)
      NODE_NAME:                          (v1:spec.nodeName)
      INSTANCE_IP:                        (v1:status.podIP)
      SERVICE_ACCOUNT:                    (v1:spec.serviceAccountName)
      ISTIO_META_ENABLE_HBONE:           true
    Mounts:
      /tmp from tmp (rw)
      /var/run/secrets/istio from istiod-ca-cert (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-vprlz (ro)
      /var/run/secrets/tokens from istio-token (rw)
      /var/run/ztunnel from cni-ztunnel-sock-dir (rw)
...
Volumes:
  istio-token:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  43200
  istiod-ca-cert:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      istio-ca-root-cert
    Optional:  false
  cni-ztunnel-sock-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/run/ztunnel
    HostPathType:  DirectoryOrCreate
...



&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ztunnel은 &lt;code&gt;/var/run/ztunnel/ztunnel.sock&lt;/code&gt;이라는 &lt;b&gt;Unix 도메인 소켓&lt;/b&gt;을 통해 다른 컴포넌트와 통신한다. 이 소켓 내부 동작을 확인하려면 &lt;a class=&quot;external-link&quot; href=&quot;https://github.com/ssup2/kpexec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-tooltip-position=&quot;top&quot; aria-label=&quot;https://github.com/ssup2/kpexec&quot;&gt;pexec&lt;/a&gt;이라는 krew 플러그인을 활용할 수 있다. pexec은 쿠버네티스에서 &lt;b&gt;SSH 없이도 high privileges 디버깅&lt;/b&gt;을 가능하게 해주는 도구로, 대상 파드와 같은 노드에 privileged 컨테이너를 띄운 뒤, 네트워크&amp;middot;프로세스&amp;middot;마운트 등 다양한 &lt;b&gt;네임스페이스를 공유&lt;/b&gt;해 실제 노드 수준에서 디버깅이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kubectl krew install pexec
kubectl pexec $ZPOD1NAME -it -T -n istio-system -- bash

ss -xnp
Netid       State        Recv-Q        Send-Q                               Local Address:Port                Peer Address:Port        Process                                                                              
u_str       ESTAB        0             0                                                * 44981                          * 44982        users:((&quot;ztunnel&quot;,pid=1,fd=13),(&quot;ztunnel&quot;,pid=1,fd=8),(&quot;ztunnel&quot;,pid=1,fd=6))       
u_seq       ESTAB        0             0                    /var/run/ztunnel/ztunnel.sock 47646                          * 46988                                                                                            
u_str       ESTAB        0             0                                                * 44982                          * 44981        users:((&quot;ztunnel&quot;,pid=1,fd=7))                                                      
u_seq       ESTAB        0             0                                                * 46988                          * 47646        users:((&quot;ztunnel&quot;,pid=1,fd=19)) 

ls -l  /var/run/ztunnel
total 0
srwxr-xr-x    1 root     root             0 Jun  1 04:54 ztunnel.sock

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;샘플 애플리케이션 배포 및 외부 통신 확인&lt;/b&gt;&lt;br /&gt;Istio Ambient Mode의 동작 방식을 이해하기 위해, Bookinfo 샘플 애플리케이션을 배포하고 ztunnel 및 외부 게이트웨이와의 통신 흐름을 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Istio 디렉토리 내 samples/bookinfo 경로에 위치한 샘플 애플리케이션을 Kubernetes 클러스터에 배포한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker exec -it myk8s-control-plane ls -l istio-1.26.0
total 40
-rw-r--r--  1 root root 11357 May  7 11:05 LICENSE
-rw-r--r--  1 root root  6927 May  7 11:05 README.md
drwxr-x---  2 root root  4096 May  7 11:05 bin
-rw-r-----  1 root root   983 May  7 11:05 manifest.yaml
drwxr-xr-x  4 root root  4096 May  7 11:05 manifests
drwxr-xr-x 27 root root  4096 May  7 11:05 samples
drwxr-xr-x  3 root root  4096 May  7 11:05 tools

# Deploy the Bookinfo sample application:
docker exec -it myk8s-control-plane kubectl apply -f istio-1.26.0/samples/bookinfo/platform/kube/bookinfo.yaml

# 확인
kubectl get deploy,pod,svc,ep
docker exec -it myk8s-control-plane istioctl ztunnel-config service
docker exec -it myk8s-control-plane istioctl ztunnel-config workload
docker exec -it myk8s-control-plane istioctl proxy-status

# 통신 확인 : ratings 에서 productpage 페이지
kubectl exec &quot;$(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}')&quot; -c ratings -- curl -sS productpage:9080/productpage | grep -o &quot;&amp;lt;title&amp;gt;.*&amp;lt;/title&amp;gt;&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, 외부 요청을 반복적으로 보내기 위한 디버깅용 netshoot 파드를 생성하고, 지속적인 요청을 보내면서 통신 흐름을 확인할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 요청 테스트용 파드 생성 : netshoot
kubectl create sa netshoot

cat &amp;lt;&amp;lt; EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: netshoot
spec:
  serviceAccountName: netshoot
  nodeName: myk8s-control-plane
  containers:
  - name: netshoot
    image: nicolaka/netshoot
    command: [&quot;tail&quot;]
    args: [&quot;-f&quot;, &quot;/dev/null&quot;]
  terminationGracePeriodSeconds: 0
EOF

# 요청 확인
kubectl exec -it netshoot -- curl -sS productpage:9080/productpage | grep -i title

# 반복 요청
while true; do kubectl exec -it netshoot -- curl -sS productpage:9080/productpage | grep -i title ; date &quot;+%Y-%m-%d %H:%M:%S&quot;; sleep 1; done

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부에서 접속 가능한 Gateway 설정&lt;/b&gt;&lt;br /&gt;이제 외부에서 /productpage에 접근할 수 있도록 Gateway API 리소스를 생성한다. Istio는 Gateway 리소스를 bookinfo-gateway로 정의하며, HTTPRoute 리소스를 통해 다양한 엔드포인트를 productpage 서비스로 연결한다&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;docker exec -it myk8s-control-plane cat istio-1.26.0/samples/bookinfo/gateway-api/bookinfo-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: bookinfo-gateway
spec:
  gatewayClassName: istio
  listeners:
  - name: http
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: bookinfo
spec:
  parentRefs:
  - name: bookinfo-gateway
  rules:
  - matches:
    - path:
        type: Exact
        value: /productpage
    - path:
        type: PathPrefix
        value: /static
    - path:
        type: Exact
        value: /login
    - path:
        type: Exact
        value: /logout
    - path:
        type: PathPrefix
        value: /api/v1/products
    backendRefs:
    - name: productpage
      port: 9080

docker exec -it myk8s-control-plane kubectl apply -f istio-1.26.0/samples/bookinfo/gateway-api/bookinfo-gateway.yaml

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 리소스가 생성되었다면 아래와 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt; kubectl get gateway
NAME               CLASS   ADDRESS           PROGRAMMED   AGE
bookinfo-gateway   istio   192.168.107.101   True         165m
(⎈|kind-myk8s:default)  JSX⚡️  ~/Desktop/1.Projects/istio
 kubectl get HTTPRoute

NAME       HOSTNAMES   AGE
bookinfo               165m
(⎈|kind-myk8s:default)  JSX⚡️  ~/Desktop/1.Projects/istio
 kubectl get svc,ep bookinfo-gateway-istio

NAME                             TYPE           CLUSTER-IP     EXTERNAL-IP       PORT(S)                        AGE
service/bookinfo-gateway-istio   LoadBalancer   10.200.1.237   192.168.107.101   15021:31041/TCP,80:30000/TCP   165m

NAME                               ENDPOINTS                        AGE
endpoints/bookinfo-gateway-istio   10.10.1.16:15021,10.10.1.16:80   165m
(⎈|kind-myk8s:default)  JSX⚡️  ~/Desktop/1.Projects/istio
 kubectl get pod -l gateway.istio.io/managed=istio.io-gateway-controller -owide

NAME                                      READY   STATUS    RESTARTS   AGE    IP           NODE            NOMINATED NODE   READINESS GATES
bookinfo-gateway-istio-6cbd9bcd49-bbsd6   1/1     Running   0          165m   10.10.1.16   myk8s-worker2   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부 접속 테스트&lt;/b&gt;&lt;br /&gt;Gateway가 노출된 외부 IP를 확인하고, 테스트 컨테이너인 mypc에서 curl을 통해 접속을 시도한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;kubectl get svc bookinfo-gateway-istio -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
GWLB=$(kubectl get svc bookinfo-gateway-istio -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
docker exec -it mypc curl $GWLB/productpage -v
docker exec -it mypc curl $GWLB/productpage -I

# 반복 요청 : 아래 mypc 컨테이너에서 반복 요청 계속 해두기!
GWLB=$(kubectl get svc bookinfo-gateway-istio -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
while true; do docker exec -it mypc curl $GWLB/productpage | grep -i title ; date &quot;+%Y-%m-%d %H:%M:%S&quot;; sleep 1; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodePort 방식으로 포트를 30000번으로 열어, 로컬에서 직접 접속할 수 있게 구성한다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 외부 접근 테스트를 위해 로컬 PC에서 30000번 포트를 통해 접속 가능하도록 NodePort 설정을 한다.
kubectl patch svc bookinfo-gateway-istio -p '{&quot;spec&quot;: {&quot;type&quot;: &quot;LoadBalancer&quot;, &quot;ports&quot;: [{&quot;port&quot;: 80, &quot;targetPort&quot;: 80, &quot;nodePort&quot;: 30000}]}}'
kubectl get svc bookinfo-gateway-istio

open &quot;http://127.0.0.1:30000/productpage&quot;

# 반복 요청
while true; do curl -s http://127.0.0.1:30000/productpage | grep -i title ; date &quot;+%Y-%m-%d %H:%M:%S&quot;; sleep 1; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rhMqg/btsOtL6606o/bKU8mTqZkwwmZpJm6CggH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rhMqg/btsOtL6606o/bKU8mTqZkwwmZpJm6CggH0/img.png&quot; data-alt=&quot;kilai 트래픽 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rhMqg/btsOtL6606o/bKU8mTqZkwwmZpJm6CggH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrhMqg%2FbtsOtL6606o%2FbKU8mTqZkwwmZpJm6CggH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;551&quot; height=&quot;236&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;kilai 트래픽 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1497&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o1olW/btsOsjKLQBi/jIyNMQaTMNfpBA2uKDWwQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1olW/btsOsjKLQBi/jIyNMQaTMNfpBA2uKDWwQ0/img.png&quot; data-alt=&quot;bookinfo 웹 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1olW/btsOsjKLQBi/jIyNMQaTMNfpBA2uKDWwQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1olW%2FbtsOsjKLQBi%2FjIyNMQaTMNfpBA2uKDWwQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1497&quot; height=&quot;946&quot; data-origin-width=&quot;1497&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;bookinfo 웹 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;애플리케이션을 Ambient Mesh에 추가하기&quot; data-ke-size=&quot;size23&quot;&gt;애플리케이션을 Ambient Mesh에 추가하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio Ambient 모드에서는 사이드카 없이도 메시 기능을 활용할 수 있다. 이를 위해서는 네임스페이스나 파드에 특정 라벨을 설정해야 한다.&lt;br /&gt;&amp;bull; &lt;code&gt;istio.io/dataplane-mode=ambient&lt;/code&gt; : 이 라벨이 지정된 네임스페이스나 파드는 Ambient 모드에 자동으로 포함된다.&lt;br /&gt;&amp;bull; &lt;code&gt;istio.io/dataplane-mode=none&lt;/code&gt; : 이 라벨이 파드에 설정되어 있으면 Ambient 모드에서 제외된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ambient 모드 적용 전후의 워크로드 상태를 istioctl ztunnel-config workload 명령어로 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 전&lt;/b&gt;&lt;br /&gt;모든 워크로드가 PROTOCOL이 TCP로 되어 있으며, 아직 Ambient 모드(HBONE)가 적용되지 않은 상태다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker exec -it myk8s-control-plane istioctl ztunnel-config workload
# 출력 예시
default  details-v1-766844796b-7cdm7   10.10.1.11   myk8s-worker2   None   TCP
default  productpage-v1-54bb874995-... 10.10.2.5    myk8s-worker    None   TCP
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ambient를 적용하려면 다음과 같이 네임스페이스에 라벨을 설정한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl label namespace default istio.io/dataplane-mode=ambient
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 후&lt;/b&gt;&lt;br /&gt;이후 다시 ztunnel-config workload 명령어를 실행하면 일부 워크로드의 프로토콜이 HBONE으로 바뀐 것을 확인할 수 있다. 이는 해당 워크로드들이 ztunnel을 통해 Ambient 모드로 통신하고 있다는 의미다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker exec -it myk8s-control-plane istioctl ztunnel-config workload

NAMESPACE          POD NAME                                    ADDRESS       NODE                WAYPOINT PROTOCOL
default            details-v1-766844796b-7cdm7                 10.10.1.11    myk8s-worker2       None     HBONE
default            netshoot                                    10.10.0.12    myk8s-control-plane None     HBONE
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ambient 모드가 적용되면 워크로드 간 mTLS 통신이 활성화되고, 기존의 복잡한 사이드카 기반 설정과 달리, 엄청나게 간소화된 정책 구성을 확인할 수 있다.&lt;br /&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MsSFn/btsOsblSSX6/sFrPIhx1TPY60Xs2zieA10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MsSFn/btsOsblSSX6/sFrPIhx1TPY60Xs2zieA10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MsSFn/btsOsblSSX6/sFrPIhx1TPY60Xs2zieA10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMsSFn%2FbtsOsblSSX6%2FsFrPIhx1TPY60Xs2zieA10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;675&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션 파드 내부 정보 확인&lt;/b&gt;&lt;br /&gt;Ambient 모드에 포함된 파드 내부에서는 ztunnel 관련 정보나 메트릭을 직접 확인할 수 없다. 하지만 기본적인 네트워크 설정은 다음 명령어들을 통해 파드 내에서 확인 가능하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl pexec $PPOD -it -T -- bash
-------------------------------------------------------
iptables-save
iptables -t mangle -S
iptables -t nat -S
ss -tnlp
ss -tnp
ss -xnp
ls -l  /var/run/ztunnel
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/var/run/ztunnel 경로는 존재하지만, 실제 트래픽 제어나 메트릭 정보는 ztunnel 파드에 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ztunnel 파드에서 상세 정보 확인&lt;/b&gt;&lt;br /&gt;ztunnel은 각 노드에 하나씩 존재하며, 해당 파드 내부에서만 실제 통신 흐름, iptables 설정, 메트릭 정보 등을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# ztunnel 파드 확인 : 파드 이름 변수 지정
kubectl get pod -n istio-system -l app=ztunnel -owide
kubectl get pod -n istio-system -l app=ztunnel
ZPOD1NAME=$(kubectl get pod -n istio-system -l app=ztunnel -o jsonpath=&quot;{.items[0].metadata.name}&quot;)
ZPOD2NAME=$(kubectl get pod -n istio-system -l app=ztunnel -o jsonpath=&quot;{.items[1].metadata.name}&quot;)
ZPOD3NAME=$(kubectl get pod -n istio-system -l app=ztunnel -o jsonpath=&quot;{.items[2].metadata.name}&quot;)
echo $ZPOD1NAME $ZPOD2NAME $ZPOD3NAME

#
kubectl pexec $ZPOD1NAME -it -T -n istio-system -- bash
-------------------------------------------------------
iptables -t mangle -S
iptables -t nat -S
ss -tnlp
ss -tnp
ss -xnp
ls -l  /var/run/ztunnel

# 메트릭 정보 확인
curl -s http://localhost:15020/metrics | grep '^[^#]'
...

# Viewing Istiod state for ztunnel xDS resources
curl -s http://localhost:15000/config_dump
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ambient 모드가 적용된 이후, 통신 흐름과 보안 상태, 트래픽 메트릭을 다양한 시각화 도구를 통해 확인할 수 있다.&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c34OJR/btsOskJEBiP/r5IW1uMxjGUvgJpe1UFKq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c34OJR/btsOskJEBiP/r5IW1uMxjGUvgJpe1UFKq1/img.png&quot; data-alt=&quot;kiali확인: 자물쇠 모양 확인(mTLS 통신), L4 텔레메트리 수집(TCP Trafiic min/max 정보 확인)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c34OJR/btsOskJEBiP/r5IW1uMxjGUvgJpe1UFKq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc34OJR%2FbtsOskJEBiP%2Fr5IW1uMxjGUvgJpe1UFKq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1818&quot; height=&quot;750&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;kiali확인: 자물쇠 모양 확인(mTLS 통신), L4 텔레메트리 수집(TCP Trafiic min/max 정보 확인)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cY8Xqb/btsOr2WOt2D/lDtdsqY6kKA9VGAo3CfdbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cY8Xqb/btsOr2WOt2D/lDtdsqY6kKA9VGAo3CfdbK/img.png&quot; data-alt=&quot;그라파나: ztunnel 대시보드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cY8Xqb/btsOr2WOt2D/lDtdsqY6kKA9VGAo3CfdbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcY8Xqb%2FbtsOr2WOt2D%2FlDtdsqY6kKA9VGAo3CfdbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;947&quot; height=&quot;868&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그라파나: ztunnel 대시보드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1005&quot; data-origin-height=&quot;945&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KNCvh/btsOszmfMcS/sciSOfvlWFpKv7hlEK5TY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KNCvh/btsOszmfMcS/sciSOfvlWFpKv7hlEK5TY0/img.png&quot; data-alt=&quot;프로메테우스: ztunnel에서 발생하는 TCP 통신 지표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KNCvh/btsOszmfMcS/sciSOfvlWFpKv7hlEK5TY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKNCvh%2FbtsOszmfMcS%2FsciSOfvlWFpKv7hlEK5TY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1005&quot; height=&quot;945&quot; data-origin-width=&quot;1005&quot; data-origin-height=&quot;945&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로메테우스: ztunnel에서 발생하는 TCP 통신 지표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ztunnel-config를 통한 구성 정보 확인&lt;/b&gt;&lt;br /&gt;Ambient Mesh에서는 istioctl ztunnel-config 명령어를 통해 서비스, 워크로드, 인증서, 연결 상태 등 다양한 정보를 조회할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 정보 확인&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker exec -it myk8s-control-plane istioctl ztunnel-config service

NAMESPACE      SERVICE NAME            SERVICE VIP  WAYPOINT ENDPOINTS
default        bookinfo-gateway-istio  10.200.1.237 None     1/1
default        details                 10.200.1.150 None     1/1
...  
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워크로드 상세 정보 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker exec -it myk8s-control-plane istioctl ztunnel-config workload --workload-namespace default --node myk8s-worker2 -o json
[
    {
        &quot;uid&quot;: &quot;Kubernetes//Pod/default/bookinfo-gateway-istio-6cbd9bcd49-bbsd6&quot;,
        &quot;workloadIps&quot;: [
            &quot;10.10.1.16&quot;
        ],
        &quot;protocol&quot;: &quot;TCP&quot;,
        &quot;name&quot;: &quot;bookinfo-gateway-istio-6cbd9bcd49-bbsd6&quot;,
        &quot;namespace&quot;: &quot;default&quot;,
        &quot;serviceAccount&quot;: &quot;bookinfo-gateway-istio&quot;,
        &quot;workloadName&quot;: &quot;bookinfo-gateway-istio&quot;,
        &quot;workloadType&quot;: &quot;pod&quot;,
        &quot;canonicalName&quot;: &quot;bookinfo-gateway-istio&quot;,
        &quot;canonicalRevision&quot;: &quot;latest&quot;,
        &quot;clusterId&quot;: &quot;Kubernetes&quot;,
        &quot;trustDomain&quot;: &quot;cluster.local&quot;,
        &quot;locality&quot;: {},
        &quot;node&quot;: &quot;myk8s-worker2&quot;,
        &quot;status&quot;: &quot;Healthy&quot;,
        &quot;hostname&quot;: &quot;&quot;,
        &quot;capacity&quot;: 1,
        &quot;applicationTunnel&quot;: {
            &quot;protocol&quot;: &quot;&quot;
        }
    },
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증서 확인&lt;/b&gt;&lt;br /&gt;워크로드에 할당된 SPIFFE 기반 인증서를 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# 결과에는 Root/Leaf 인증서 타입, 유효 기간, SPIFFE 식별자 등이 포함된다.
docker exec -it myk8s-control-plane istioctl ztunnel-config certificate --node myk8s-worker

CERTIFICATE NAME                                              TYPE     STATUS        VALID CERT     SERIAL NUMBER                        NOT AFTER                NOT BEFORE
spiffe://cluster.local/ns/default/sa/bookinfo-productpage     Leaf     Available     true           e35e4fbbc0b5f38b32df275f14a01e1e     2025-06-09T19:02:36Z     2025-06-08T19:00:36Z
spiffe://cluster.local/ns/default/sa/bookinfo-productpage     Root     Available     true           1255f6e6fa41fe3ead58d591b24973fc     
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연결 정보&lt;/b&gt;&lt;br /&gt;ztunnel이 현재 유지하고 있는 L4/HBONE 연결 정보를 출력한다. 각 커넥션은 Inbound/Outbound 방향으로 구분된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; docker exec -it myk8s-control-plane istioctl ztunnel-config connections --node myk8s-worker --raw

WORKLOAD                                DIRECTION LOCAL          REMOTE           REMOTE TARGET PROTOCOL
productpage-v1-54bb874995-zrl85.default Inbound   10.10.2.5:9080 10.10.1.16:33552               HBONE
productpage-v1-54bb874995-zrl85.default Inbound   10.10.2.5:9080 10.10.1.16:33558               HBONE
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정책 및 로깅 수준 확인&lt;/b&gt;&lt;br /&gt;현재 적용된 정책이나 ztunnel의 로그 레벨을 조회할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 정책 확인
docker exec -it myk8s-control-plane istioctl ztunnel-config policy
NAMESPACE POLICY NAME ACTION SCOPE

# 로그 확인 
docker exec -it myk8s-control-plane istioctl ztunnel-config log
ztunnel-6ddsp.istio-system:
current log level is hickory_server::server::server_future=off,info
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;productpage Pod 내부에서 트래픽 리디렉션 확인&quot; data-ke-size=&quot;size23&quot;&gt;productpage Pod 내부에서 트래픽 리디렉션 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Pod 접속 및 iptables 확인&lt;/b&gt;&lt;br /&gt;먼저 productpage Pod에 진입한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;PPOD=$(kubectl get pod -l app=productpage -o jsonpath='{.items[0].metadata.name}')
kubectl pexec $PPOD -it -T -- bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iptables-save 명령어로 전체 룰을 확인하거나, 특정 테이블만 필터링해 raw, mangle, nat 체인을 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. DNS 트래픽 처리 로직 (raw + mangle)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;raw 테이블&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DNS 트래픽(UDP 53)에 대해 connection tracking zone을 설정해 추적할 수 있게 한다.&lt;/li&gt;
&lt;li&gt;마크(0x539) 기반 조건으로 리디렉션 여부를 제어한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-A ISTIO_OUTPUT -p udp -m mark --mark 0x539/0xfff -m udp --dport 53 -j CT --zone 1
-A ISTIO_PRERT -p udp -m mark ! --mark 0x539/0xfff -m udp --sport 53 -j CT --zone 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mangle 테이블&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;conntrack 마크(0x111)를 복원하고,&lt;/li&gt;
&lt;li&gt;특정 마크가 붙은 트래픽에 대해 이후 체인에서 리디렉션 예외로 처리할 수 있게 준비한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-A ISTIO_OUTPUT -m connmark --mark 0x111/0xfff -j CONNMARK --restore-mark ...
-A ISTIO_PRERT -m mark --mark 0x539/0xfff -j CONNMARK --set-xmark 0x111/0xfff
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 트래픽 리디렉션 로직 (nat)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;DNS 리디렉션&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UDP 및 TCP 기반 DNS 요청을 Envoy DNS 프록시 포트인 15053으로 리디렉션한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-A ISTIO_OUTPUT ! -o lo -p udp --dport 53 -j REDIRECT --to-ports 15053
-A ISTIO_OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports 15053
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일반 TCP 트래픽&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마크나 특정 IP(예: 169.254.7.127)가 붙지 않은 경우, 다음 포트로 리디렉션됨:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;15001: outbound&lt;/li&gt;
&lt;li&gt;15006: inbound&lt;/li&gt;
&lt;li&gt;15008: 프로메테우스 등 보안 예외 대상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-A ISTIO_OUTPUT -p tcp --dport != 15001 -j REDIRECT --to-ports 15001
-A ISTIO_PRERT -p tcp --dport != 15008 -j REDIRECT --to-ports 15006
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 리디렉션 대상 포트 리스닝 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ss -tnlp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 예시:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;15001, 15006, 15008, 15053 포트가 열려 있으며 Envoy에서 리디렉션된 트래픽을 수신한다.&lt;/li&gt;
&lt;li&gt;9080은 앱 자체(gunicorn)가 수신 중.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 암호화 여부 확인&lt;/b&gt;&lt;br /&gt;다음 명령으로 ztunnel의 포트인 15008 또는 Envoy의 15001, 15006 포트를 모니터링하여 평문 트래픽 여부를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;apk update &amp;amp;&amp;amp; apk add ngrep
ngrep -tW byline -d eth0 '' 'tcp port 15008'
ngrep -tW byline -d eth0 '' 'tcp port 15001'
ngrep -tW byline -d eth0 '' 'tcp port 15006'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 평문 데이터가 잡히지 않는다면 ztunnel을 통한 mTLS 암호화가 적용되고 있음을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. ztunnel 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;ls -l /var/run/ztunnel
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 디렉터리가 존재하면 ambient 모드에서 ztunnel 프로세스가 해당 pod에 주입되어 있음을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;Ambient Mesh에서 mTLS 활성 여부 검증하기&quot; data-ke-size=&quot;size23&quot;&gt;Ambient Mesh에서 mTLS 활성 여부 검증하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio Ambient 모드에서는 ztunnel을 통해 워크로드 간 트래픽이 암호화된다. 다음 방법들을 통해 mTLS가 실제로 적용되었는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 워크로드가 HBONE 프로토콜을 사용하는지 확인한다. HBONE은 Ambient Mesh 구조에서 사용하는 방식으로, 설정되어 있다면 기본적으로 mTLS가 적용된 상태다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker exec -it myk8s-control-plane istioctl ztunnel-config workload
NAMESPACE          POD NAME                                    ADDRESS       NODE                WAYPOINT PROTOCOL
default            bookinfo-gateway-istio-6cbd9bcd49-bbsd6     10.10.1.16    myk8s-worker2       None     TCP
default            cnsenter-kad0b53iju                         10.10.2.7     myk8s-worker        None     HBONE
default            details-v1-766844796b-7cdm7                 10.10.1.11    myk8s-worker2       None     HBONE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd2NLi/btsOttZXbK8/ijFwb2REbDWKXuAAkKKLF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd2NLi/btsOttZXbK8/ijFwb2REbDWKXuAAkKKLF1/img.png&quot; data-alt=&quot;prometheus: mtls 쿼리 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd2NLi/btsOttZXbK8/ijFwb2REbDWKXuAAkKKLF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd2NLi%2FbtsOttZXbK8%2FijFwb2REbDWKXuAAkKKLF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2542&quot; height=&quot;294&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;prometheus: mtls 쿼리 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ztunnel 로그로 mTLS 피어 ID 확인&lt;/b&gt;&lt;br /&gt;mTLS가 적용되었다면 ztunnel 로그에 트래픽 송수신 정보와 함께 spiffe:// 형식의 인증 주체 ID(SPIFFE ID)가 기록된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;#
kubectl -n istio-system logs -l app=ztunnel | grep -E &quot;inbound|outbound&quot;
2024-08-21T15:32:05.754291Z info access connection complete src.addr=10.42.0.9:33772 src.workload=&quot;curl-7656cf8794-6lsm4&quot; src.namespace=&quot;default&quot;
src.identity=&quot;spiffe://cluster.local/ns/default/sa/curl&quot; dst.addr=10.42.0.5:15008 dst.hbone_addr=10.42.0.5:9080 dst.service=&quot;details.default.svc.cluster.local&quot;
dst.workload=&quot;details-v1-857849f66-ft8wx&quot; dst.namespace=&quot;default&quot; dst.identity=&quot;spiffe://cluster.local/ns/default/sa/bookinfo-details&quot;
direction=&quot;outbound&quot; bytes_sent=84 bytes_recv=358 duration=&quot;15ms&quot;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdZfjF/btsOsVJltUz/d0Fu41eClCiFrLJGR8s7uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdZfjF/btsOsVJltUz/d0Fu41eClCiFrLJGR8s7uk/img.png&quot; data-alt=&quot;kiali : principal 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdZfjF/btsOsVJltUz/d0Fu41eClCiFrLJGR8s7uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdZfjF%2FbtsOsVJltUz%2Fd0Fu41eClCiFrLJGR8s7uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1688&quot; height=&quot;1114&quot; data-origin-width=&quot;1688&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;kiali : principal 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;internal-embed media-embed image-embed is-loaded&quot;&gt;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;tcpdump로 패킷 암호화 여부 확인&lt;/b&gt;&lt;br /&gt;쿠버네티스 노드 또는 ztunnel이 주입된 pod 내부에서 tcpdump로 HBONE 포트(15008)와 앱 포트(예: 9080)를 감시하여 암호화 여부를 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;15008 포트를 통해 주고받는 트래픽에서 평문 데이터가 보이지 않는다면 mTLS가 적용된 상태&lt;/li&gt;
&lt;li&gt;반대로 평문 텍스트(HTTP 헤더 등)가 보인다면 암호화되지 않았거나 ambient가 적용되지 않은 워크로드일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 워크로드 이름 확인
DPOD=$(kubectl get pods -l app=details -o jsonpath=&quot;{.items[0].metadata.name}&quot;)

# tcpdump 실행
kubectl pexec $DPOD -it -T -- sh -c 'tcpdump -nAi eth0 port 9080 or port 15008'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실습을 통해 HBONE 여부로 ambient 적용 상태를 판단하고, SPIFFE ID 및 암호화 트래픽 여부로 실제 mTLS가 작동하고 있는지를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;L4 보안 정책 적용하기&quot; data-ke-size=&quot;size23&quot;&gt;L4 보안 정책 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Istio의 L4 보안 정책은 Ambient Mesh 모드에서 ztunnel이 직접 지원한다.앰비언트 구조에서는 &lt;b&gt;ztunnel&lt;/b&gt;과 &lt;b&gt;웨이포인트 프록시(Waypoint Proxy)&lt;/b&gt; 가 계층화되어 있어, 특정 워크로드에 대해 L7 기능(HTTP 라우팅, L7 RBAC 등)을 적용할지 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 L7 기반의 정책이나 트래픽 라우팅 기능이 필요하다면 워크로드에 웨이포인트를 배포하면 된다. 이처럼 정책이 ztunnel(L4)과 웨이포인트(L7) 양쪽에서 시행될 수 있기 때문에 각 레벨에 적용되는 정책을 구분하고 명확히 이해할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L4 Authorization Policy 적용&lt;/b&gt;&lt;br /&gt;다음 정책은 productpage 서비스에 대해 netshoot 서비스 계정만 접근을 허용하는 예제다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# L4 Authorization Policy 신규 생성
# Explicitly allow the netshoot and gateway service accounts to call the productpage service:
kubectl apply -f - &amp;lt;&amp;lt;EOF
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: productpage-viewer
  namespace: default
spec:
  selector:
    matchLabels:
      app: productpage
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - cluster.local/ns/default/sa/netshoot
EOF

# L4 Authorization Policy 생성 확인
kubectl get authorizationpolicy
NAME                 AGE
productpage-viewer   5s

# 적용 후 ztunnel 로그에서 정책 적용 여부를 확인
kubectl logs ds/ztunnel -n istio-system -f | grep -E RBAC
Found 3 pods, using pod/ztunnel-6ddsp
2025-06-08T22:20:50.603334Z     info    xds:xds{id=8}   handling RBAC update productpage-viewe
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정책 동작 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# L4 Authorization Policy 동작 확인
## 거부 동작 확인 (외부 요청)
GWLB=$(kubectl get svc bookinfo-gateway-istio -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
while true; do docker exec -it mypc curl $GWLB/productpage | grep -i title ; date &quot;+%Y-%m-%d %H:%M:%S&quot;; sleep 1; done

2025-06-09 07:24:50

## 허용 동작 확인 (netshoot에서 요청)
kubectl exec -it netshoot -- curl -sS productpage:9080/productpage | grep -i title
while true; do kubectl exec -it netshoot -- curl -sS productpage:9080/productpage | grep -i title ; date &quot;+%Y-%m-%d %H:%M:%S&quot;; sleep 1; done

&amp;lt;title&amp;gt;Simple Bookstore App&amp;lt;/title&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정책 업데이트&lt;/b&gt;&lt;br /&gt;추가로 gateway 서비스 계정도 허용 대상에 포함:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# L4 Authorization Policy 업데이트
kubectl apply -f - &amp;lt;&amp;lt;EOF
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: productpage-viewer
  namespace: default
spec:
  selector:
    matchLabels:
      app: productpage
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - cluster.local/ns/default/sa/netshoot
        - cluster.local/ns/default/sa/bookinfo-gateway-istio
EOF

kubectl logs ds/ztunnel -n istio-system -f | grep -E RBAC


# 허용 확인!
GWLB=$(kubectl get svc bookinfo-gateway-istio -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
while true; do docker exec -it mypc curl $GWLB/productpage | grep -i title ; date &quot;+%Y-%m-%d %H:%M:%S&quot;; sleep 1; done

&amp;lt;title&amp;gt;Simple Bookstore App&amp;lt;/title&amp;gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Kubernetes/Istio</category>
      <category>istio ambient</category>
      <category>waypoint</category>
      <category>ztunnel</category>
      <author>장성필(hackjap)</author>
      <guid isPermaLink="true">https://hackjsp.tistory.com/101</guid>
      <comments>https://hackjsp.tistory.com/101#entry101comment</comments>
      <pubDate>Mon, 9 Jun 2025 08:28:32 +0900</pubDate>
    </item>
  </channel>
</rss>