0. Contents
1. Overview
배경
- 02편의 PR 경로는 plain 과 Kustomize 만 지원한다
- Helm 앱을 지원하려면 고객 차트를
helm template으로 렌더링해야 한다. 04편의 값 찾기는 후보마다 렌더링을 돌린다 - 고객 차트는 우리가 쓰지 않은 입력이다. 템플릿은 프로그램처럼 돌고, 스키마와 링크는 파일을 읽는다
이 글의 질문
- 렌더링을 왜 에이전트와 다른 파드에서 하는가
- helm 이 고객 차트 때문에 무엇을 읽고 어디에 닿는가
- 결과를 Argo CD 와 어떻게 맞추는가
- 네트워크를 막았을 때 차트는 누가 받는가
2. Pod Separation — 파드 분리
2.1 에이전트 파드의 제약
- 에이전트는 모든 고객이 함께 쓰는 파드 한 대이고, 메모리 제한은 768Mi 다
- 방별 대화 잠금과 승인 대기가 프로세스 메모리에 있다. 파드가 재시작되면 다른 고객의 대화까지 끊기고 승인 대기도 사라진다
- 이 파드에는 고객 클러스터 접속 정보가 지나간다. 고객 차트를 같은 공간에서 돌리면 렌더링 사고가 접속 정보까지 닿는다
2.2 렌더링 앱의 언어
렌더링 앱은 helm 을 실행하는 접수창구다. 실제 렌더링과 그 메모리는 helm 프로세스의 몫이라, 앱 언어를 바꿔도 렌더링 시간과 메모리는 그대로다.
helm 을 Go 라이브러리로 앱 안에 넣으면 프로세스를 띄우는 시간은 줄어든다. 대신 도는 템플릿을 도중에 끊을 수 없고, 메모리가 넘치면 앱까지 함께 죽는다. 그래서 프로세스로 실행하고, 앱은 에이전트와 같은 파이썬으로 두었다.
2.3 실패의 구분
에이전트 쪽 render() 는 예외를 내지 않는다. 연결 거부, 응답 중 끊김, 시간 초과는 모두 결과로 돌아온다.
| 구분 | reason | 사용자에게 보일 뜻 |
|---|---|---|
| 서비스 쪽 | unavailable busy internal | 렌더링 실패, 잠시 뒤 다시 시도 |
| 차트 쪽 | timeout memory_exceeded chart_has_link helm_failed 등 | 이 차트로는 렌더링할 수 없음 |
3. Helm Input Risks — helm 입력의 위험
3.1 재현 결과
helm 4.2 로 직접 재현했다.
| 입력 | helm 의 동작 |
|---|---|
스키마 $ref: file:///etc/hosts | 파일을 읽고 내용을 JSON 으로 해석하다 오류를 낸다 |
스키마 $ref: http://… | 그 주소로 접속한다 |
스키마 $schema: http://… | 메타 스키마를 받으러 그 주소로 접속한다 |
| 폴더 차트의 심볼릭 링크 | found symbolic link 를 알리고 차트 밖 파일을 템플릿으로 읽는다 |
3.2 스키마 검사
values.schema.json은 하위 차트마다 있을 수 있다. 안쪽charts/*.tgz도 helm 이 읽으므로 풀지 않고 같은 규칙으로 읽는다- 다음 겹으로 helm 의 프록시를 닿지 않는 주소로 둔다. 바깥 주소로 가는 요청이 그 주소에서 끝난다
3.3 차트 풀기
| 입력 | 처리 |
|---|---|
| 심볼릭 링크, 하드 링크 | 어디를 가리키든 거절 |
.. 절대 경로, 역슬래시 | 거절 |
| 장치 파일, 같은 경로 두 번 | 거절 |
| 압축 폭탄 | 푸는 바이트를 읽으면서 세어 64MiB 에서 중단 |
| 깊은 하위 차트 | 네 단계까지 |
3.4 쓰지 않는 옵션
--enable-dns,--dry-run=server: 클러스터나 DNS 를 읽는다--post-renderer, 플러그인: 임의 실행 파일을 돌린다--dependency-update: 차트가 정한 저장소에서 받는다. CVE-2025-53547 의 경로다- 조건 값은 형식 검사를 통과해야 인자가 된다.
--post-renderer=…를 릴리스 이름으로 보내면 400 이 되고, 받은 값은 응답에 되돌려 싣지 않는다
4. Render Limits — 렌더링 제한
4.1 시작값
맥에서 잰 값은 04편 6장의 측정이다.
| 항목 | 맥에서 잰 값 (공개 차트 21개, helm 직접 실행) | 시작값 | 넘으면 |
|---|---|---|---|
| 시간 | 중앙값 0.083초, 최대 0.6초 | 5초 | 프로세스 종료, timeout |
| 출력 | 최대 5.7MB | 20MB | 읽는 도중 종료, output_too_large |
| 프로세스 메모리 | 최대 137MB | 200MiB | 프로세스 종료, memory_exceeded |
| 동시 실행 | 2개 | 기다리지 않고 429 busy | |
| 파드 메모리 | 512Mi | 파드 재시작 |
값은 전부 환경변수로 바꾼다. 요청 본문을 읽기 전에 자리를 먼저 잡아, 넘치는 요청은 메모리를 쓰지 않는다.
4.2 프로세스 종료
처음 구현은 출력 제한을 넘기면 서비스가 영원히 멈췄다. 5번 상자의 동작을 몰랐기 때문이고, 큰 출력을 실제로 넣은 시험에서 드러났다.
4.3 파드 격리
automountServiceAccountToken: false,enableServiceLinks: false: 토큰과 다른 Service 주소를 주지 않는다readOnlyRootFilesystem, uid 1000, capabilities 전부 제거, seccompRuntimeDefault- 쓸 수 있는 곳은 emptyDir
/work뿐이고, 요청마다 임시 폴더를 만들어 끝나면 지운다 - emptyDir 은 컨테이너 재시작에도 남는다. 앞 컨테이너가 죽으며 남긴 values 를 시작할 때 지운다
- 렌더링 결과, values, helm stderr 는 로그에 남기지 않는다. 사유와 시간, 크기만 남긴다
- CPU 제한 1코어를 둔다. 같은 노드의 에이전트가 함께 느려지는 것을 막기 위해서다
5. Argo CD Alignment — Argo CD 조건 맞추기
5.1 렌더링 조건
5.2 values 파일
- 렌더링 파드는 경로가 아니라 내용을 받는다. 파일 이름은 서비스가
000.yaml처럼 짓는다. 그래서 values 경로 조작(CVE-2022-24348)이 이 파드에서는 생기지 않는다 - 경로를 읽는 일은 에이전트가 레포 안에서만 한다. 상대 경로는 차트 폴더,
/로 시작하면 레포 루트,$이름/…은 ref 소스 기준이고, 레포 밖이나 밖을 가리키는 링크는 거절한다 [a, b, a]는-f a -f b다. Argo CD 의appendUnique가 같은 파일을 처음 자리에 한 번만 쓴다- 인라인은
valuesObject가 비어 있어도values보다 우선한다
같은 결과를 낼 수 없는 Application 은 다른 결과를 내지 않고 거절한다. helm.parameters, 원격 values, glob, 환경변수가 든 경로가 여기에 해당한다.
5.3 같은 결과의 확인
공개 차트 21개를 서비스와 helm 4.2.1 직접 실행으로 각각 렌더링해 비교했다. 맥의 컨테이너에서 쟀다.
| 결과 | 차트 수 | 내용 |
|---|---|---|
| 바이트까지 같음 | 14 | |
| 무작위 값만 다름 | 7 | 비밀번호, 인증서, 테스트 파드 이름 |
| 그 밖의 차이 | 0 | 객체로 풀어 칸마다 비교 |
5.4 Helm 버전
- 렌더링 파드의 helm 은 dev Argo CD v3.5.3 의 repo-server 와 같은 4.2.1 로 고정했다. 버전과 sha256 을 이미지 빌드에서 대조하고, 응답 머리에 버전을 돌려준다
- helm 3.22.1 과 비교하면 21개 중 4개 차트에서 무작위 값이 아닌 차이가 있었다. 대부분
limits: null같은 null 값의 처리다 - 고객 Argo CD 의 helm 이 다르면 비교가 어긋날 수 있다. 이 차이는 남아 있다
6. Network Isolation — 네트워크 격리
6.1 꺼져 있던 단속
격리 정책을 쓰기 전에 dev 클러스터를 먼저 확인했고, vpc-cni 의 NetworkPolicy 단속은 꺼져 있었다.
| 위치 | 값 |
|---|---|
| 애드온 설정 | enableNetworkPolicy 없음 |
ConfigMap amazon-vpc-cni | enable-network-policy-controller: false |
| 노드 에이전트 인자 | --enable-network-policy=false |
argocd 네임스페이스에는 NetworkPolicy 4개가 있었지만 아무것도 막지 않고 있었다. 객체가 있는 것만으로는 적용 여부를 알 수 없다.
6.2 단속을 켜기 전 점검
단속을 켜면 있던 정책 4개가 함께 적용된다.
6.3 렌더링 파드 정책
- 처음 설계는 차트를 받으려고 DNS 와 공인 주소 443 을 열어 두는 안이었다
- 렌더링 파드가 어디에도 나가지 않는 쪽이 경계가 단순하므로 전부 막았다
6.4 차트 내려받기의 위치
나가는 길을 막으면서 내려받기는 에이전트로 옮겼다. 고객이 정한 주소에 접속 정보가 있는 파드가 나가는 셈이므로 코드로 막는다.
저장소 목록(index.yaml)은 YAML 파서에 넣지 않고, 필요한 칸만 줄 단위로 읽는다.
| 입력 | 파서에 넣었을 때 (에이전트 이미지에서 측정) | 줄 단위 읽기 |
|---|---|---|
| 괄호 2만 겹 | 0.79초 | 바로 끝남 |
| 괄호 5만 겹 | 4.8초 | 바로 끝남 |
| 괄호 40만 겹 | 2분 넘게 끝나지 않아 중단 | 바로 끝남 |
| 앨리어스 겹치기 | 값을 글자로 바꿀 때 펼쳐짐 | 글자일 뿐 |
이 변경은 리뷰의 “깊은 중첩이면 프로세스가 스택 넘침으로 죽는다"는 지적에서 시작했다. 재현해 보니 프로세스는 죽지 않았고, 시간이 깊이의 제곱으로 늘어 전체 시간 제한을 넘는 문제였다. 공개 차트 21개 중 20개를 이 방식으로 받았다. 나머지 하나는 목록의 주소가 OCI 라 지원 범위 밖이다.
6.5 적용 순서
| 순서 | 적용 | 이유 |
|---|---|---|
| 1 | ECR 만들기 | push 역할의 plan 이 이 저장소를 조회한다. 한 PR 에 두면 CI plan 이 실패한다 |
| 2 | push 역할, 레포 변수 | 이미지 CI 가 이 역할을 쓴다 |
| 3 | 단속 켜기, AppProject 허용 | 허용 전에 정책이 들어오면 앱 전체 동기화가 실패한다 |
| 4 | 파드 배포 | 첫 이미지 전까지 ImagePullBackOff 이고 다른 파드에는 영향이 없다 |
| 5 | 이미지 CI | digest 자동 커밋으로 파드가 뜬다 |
7. Results — 결과
7.1 나쁜 차트
dev 클러스터의 실제 에이전트 파드에서 호출했다. 모든 경우 에이전트는 /health 200, 재시작 0회였고 렌더링 파드도 재시작하지 않았다.
| 차트 | 결과 | 시간 |
|---|---|---|
| 무한 반복에 가까운 템플릿 | timeout | 5.03초 |
| 큰 출력 30MB | memory_exceeded | 4.96초 |
| 메모리 폭주 | memory_exceeded | 0.41초 |
| 심볼릭 링크 | chart_has_link | 0.02초 |
스키마 $ref | schema_external_ref | 0.02초 |
| 셋째 동시 요청 | busy | 즉시 |
파드까지 죽는 경우는 일회용 kind 에서 시험했다. 프로세스 감시를 끄고 메모리를 채우면 렌더링 파드가 OOMKilled(137)로 재시작했고, 에이전트에는 unavailable 로 돌아왔으며 몇 초 뒤 다시 렌더링됐다. 렌더링 중 파드 삭제와 파드 0개도 같은 결과였다.
7.2 실제 차트의 시간
t3a.medium 노드, CPU 제한 1코어에서 각 3회 쟀다.
| 차트 | 출력 | 시간 |
|---|---|---|
| kyverno | 5.7MB | 1.72초 |
| harbor | 54KB | 1.28초 |
| kube-prometheus-stack | 5.2MB | 0.78초 |
| argo-cd | 1.9MB | 0.75초 |
| external-secrets | 2.0MB | 0.67초 |
| podinfo | 5KB | 0.15초 |
맥 컨테이너의 서비스 시간(kyverno 0.36초)과 dev(1.72초)를 비교하면 3~5배 느리지만 5초 안에 든다. 시작값을 맥에서 잰 숫자로만 정하면 여유를 잘못 볼 수 있다.
큰 출력 차트는 맥에서 output_too_large(당시 프로세스 제한 220MiB) 였고, dev 에서는 memory_exceeded(200MiB) 로 끝났다. 어느 쪽이든 그 프로세스만 끝나지만, 출력 20MB 제한보다 메모리 제한이 먼저 걸릴 수 있다.
8. Discussion
8.1 확인 범위
| 내용 | 확인 방법 |
|---|---|
| 차트 풀기, 스키마 검사, 인자, 내려받기 | 단위 테스트 |
| 나쁜 차트, 격리, 나가는 길 차단 | dev 클러스터 |
| 파드 OOM, 렌더링 중 삭제, 파드 0개 | 일회용 kind |
| 서비스와 helm 직접 실행의 동일성 | 공개 차트 21개, 맥의 컨테이너 |
| 스키마 메모리 고갈(CVE-2025-55199) | 재현하지 않았다. 패치된 helm 과 메모리 제한에 맡긴다 |
8.2 남은 한계
- 렌더링 파드는 아직 값 찾기와 PR 경로에 연결되지 않았다. 지금은 Helm 앱을 PR 로 고칠 수 없다
kustomize build는 아직 에이전트 파드에서 돈다- 렌더링 파드와 단속은 dev 에만 있다
Chart.yaml의 의존성이charts/에 없으면missing_dependency로 실패한다. Argo CD 는 이때helm dependency build를 돌린다- OCI 저장소와 인증이 필요한 저장소는 받지 못한다
- 큰 요청과 큰 출력의 버퍼가 겹치면 512Mi 를 넘을 수 있다. 그때는 렌더링 파드만 재시작한다
핵심 메시지
- 격리 단위: 위험한 입력을 다루는 파드
- 공용 파드가 죽는 일과 렌더링이 실패하는 일을 나눈다
- 막는 순서: helm 실행 전 검사, 실행 중 제한, 바깥의 네트워크
- 프록시 설정 하나에 기대지 않는다
- 정책의 효력: 객체가 아니라 단속 여부
- 정책 4개가 있어도 CNI 가 꺼져 있으면 아무것도 막지 않는다
- 재현 우선: 지적의 원인을 측정으로 확인
- 깊은 중첩의 실제 문제는 스택이 아니라 제곱 시간이었다
