# ArgoCD가 모르는 도구로 매니페스트를 만들기 - Config Management Plugin
ArgoCD는 Helm, Kustomize, Jsonnet, 그리고 평범한 YAML 디렉터리를 기본으로 이해합니다. 문제는 실제 레포가 그 넷 중 하나로 딱 떨어지지 않을 때입니다. helmfile로 여러 릴리스를 묶어 관리하거나, Kustomize 안에서 Helm 차트를 인플레이트하거나, cdk8s로 코드에서 매니페스트를 만드는 경우가 그렇습니다.
이때 필요한 것이 Config Management Plugin(CMP)입니다. 개념은 단순합니다. "레포를 주면 표준 출력으로 Kubernetes YAML을 뱉는 명령"을 등록하는 것입니다. 그런데 이 단순한 개념을 실제로 붙이면 조용히 실패하는 지점이 여럿 있습니다. 이 글에서는 CMP가 동작하는 구조, 안 붙는 이유들, 그리고 애초에 CMP를 안 쓰는 선택지를 정리합니다.
# 1. 오래된 문서를 따라 하면 안 되는 이유
인터넷에 남아 있는 CMP 설정 대부분은 argocd-cm ConfigMap에 configManagementPlugins 키를 넣는 방식입니다. 이 방식은 더 이상 존재하지 않습니다.
The
configManagementPluginsConfigMap approach was removed in version 2.8.
지금은 사이드카 방식 하나뿐입니다. 검색으로 찾은 설정이 적용되지 않는다면 먼저 이것부터 확인해야 합니다. 두 방식은 겉모습이 비슷해서(init/generate/discover 필드가 같음) 잘못된 문서를 따라가고 있다는 사실을 알아채기 어렵습니다. 구분법은 명확합니다. ConfigMap에 플러그인 정의를 적고 있다면 폐기된 방식입니다.
# 2. 사이드카가 하는 일
새 방식의 구조는 이렇습니다. argocd-repo-server 파드에 컨테이너를 하나 더 붙이고, 그 컨테이너가 argocd-cmp-server를 엔트리포인트로 실행합니다. repo-server는 매니페스트가 필요할 때 유닉스 소켓 위의 gRPC로 사이드카에게 요청하고, 사이드카가 레포 내용을 받아 명령을 실행해 결과를 돌려줍니다.
argocd-repo-server 파드
├── repo-server (본체) - Git clone, 캐시, 표준 도구(helm/kustomize) 실행
│ │ gRPC over /var/run/argocd/cmp-server.sock
│ ▼
└── cmp-사이드카 - argocd-cmp-server + 내가 넣은 도구
└── /home/argocd/cmp-server/config/plugin.yaml 을 읽는다
핵심은 레포 파일을 복사받아 사이드카 안에서 실행한다는 점입니다. 그래서 사이드카 이미지에 필요한 바이너리(helmfile, helm, kustomize, python 등)가 전부 들어 있어야 합니다. repo-server 본체에 설치해 봐야 사이드카는 그것을 볼 수 없습니다.
플러그인 정의는 사이드카 안 정해진 경로에 있어야 합니다.
The plugin configuration file must be located at
/home/argocd/cmp-server/config/plugin.yaml.
# 3. plugin.yaml 쓰기
helmfile을 예로 들면 이렇게 됩니다.
apiVersion: argoproj.io/v1alpha1
kind: ConfigManagementPlugin
metadata:
name: helmfile
spec:
version: v1.0
init:
command: ["helmfile"]
args: ["repos"]
generate:
command: ["sh", "-c"]
args:
- |
helmfile template \
--environment "${ARGOCD_ENV_ENVIRONMENT:-default}" \
--include-crds \
--quiet 2>/dev/null
discover:
fileName: "helmfile.yaml"
필드별로 짚어 볼 점이 있습니다.
generate의 출력은 오직 매니페스트여야 합니다. repo-server는 표준 출력을 통째로 YAML로 파싱합니다. 도구가 진행 상황이나 경고를 표준 출력에 찍으면 그 줄이 매니페스트의 일부로 해석되어 파싱 오류가 납니다. helmfile의 --quiet, 그리고 2>/dev/null이나 2>&1 1>&3류의 리디렉션이 필요한 이유입니다. 로그는 stderr로 보내야 합니다. 이것이 CMP를 처음 붙일 때 가장 자주 부딪히는 지점이고, 오류 메시지가 error converting YAML to JSON 같은 형태로 나와 원인이 잘 안 보입니다.
version을 지정하면 참조 이름이 바뀝니다. spec.version이 있으면 Application에서 helmfile-v1.0으로 불러야 합니다. 이름만 적으면 매칭되지 않습니다.
init은 매번 실행됩니다. 이름 때문에 한 번만 도는 것처럼 보이지만 매니페스트 생성 요청마다 init → generate 순으로 돕니다. helmfile repos처럼 네트워크를 타는 작업을 여기 두면 리프레시마다 외부 레지스트리를 찌르게 됩니다. 6절의 타임아웃 문제와 바로 연결됩니다.
# 4. 어떤 Application이 이 플러그인을 쓸지 정하기
두 가지 방법이 있고 성격이 다릅니다.
자동 발견(discover). discover.fileName의 글롭 패턴이 레포에 맞으면 자동으로 그 플러그인이 선택됩니다. discover.find.command로 임의 판정 로직을 넣을 수도 있습니다(종료 코드 0이면 지원).
discover:
find:
glob: "**/helmfile.yaml" # 하위 디렉터리까지
# 또는
command: ["sh", "-c", "test -f helmfile.yaml"]
명시 지정. Application에서 플러그인 이름을 직접 적습니다.
spec:
source:
repoURL: https://git.example.com/infra.git
path: envs/prod
plugin:
name: helmfile-v1.0
env:
- name: ENVIRONMENT
value: prod
자동 발견은 편하지만 사이드카가 여럿일 때 위험합니다. 두 플러그인의 발견 조건이 겹치면 어느 쪽이 잡을지 예측하기 어렵고, 나중에 플러그인을 하나 추가했을 때 기존 Application의 렌더링 도구가 조용히 바뀔 수 있습니다. 사이드카가 하나뿐이면 자동 발견, 둘 이상이면 명시 지정이 안전한 기준입니다.
여기서 흔한 함정이 환경 변수입니다. Application의 plugin.env에 ENVIRONMENT를 넣었는데 플러그인 안에서 $ENVIRONMENT가 비어 있는 경우가 있습니다.
Custom environment variables are prefixed with
ARGOCD_ENV_before reaching the plugin.
사용자가 지정한 변수는 접두사가 붙어서 전달됩니다. ENVIRONMENT로 넣었으면 플러그인에서는 ARGOCD_ENV_ENVIRONMENT로 읽어야 합니다. 앞의 plugin.yaml 예시가 그렇게 되어 있는 이유입니다. 반면 ArgoCD가 기본 제공하는 ARGOCD_APP_NAME, ARGOCD_APP_NAMESPACE, ARGOCD_APP_REVISION 같은 변수는 접두사 없이 그 이름 그대로입니다. 두 계열이 섞여 있어 헷갈리기 쉽습니다.
# 5. 볼륨을 빠뜨리면 조용히 실패한다
사이드카에는 마운트가 넷 필요합니다.
| 마운트 경로 | 용도 | 빠뜨리면 |
|---|---|---|
/var/run/argocd | repo-server와의 gRPC 소켓 | 사이드카가 등록되지 않음 |
/home/argocd/cmp-server/plugins | 플러그인 소켓 디렉터리 | 통신 실패 |
/home/argocd/cmp-server/config/plugin.yaml | 플러그인 정의 | 플러그인이 인식되지 않음 |
/tmp | 레포 파일 전개용 임시 공간 | 간헐적 렌더링 오류 |
마지막 /tmp가 중요합니다. 문서는 repo-server의 /tmp와 분리된 볼륨을 쓰라고 합니다. 같은 볼륨을 공유하면 본체와 사이드카가 같은 경로에 파일을 쓰면서 서로를 덮습니다. 증상이 "가끔 렌더링이 이상하다"로 나타나 재현이 어렵습니다.
repo-server 디플로이먼트에 붙이는 형태는 이렇습니다.
spec:
template:
spec:
volumes:
- name: cmp-plugin
configMap:
name: cmp-plugin-helmfile
- name: cmp-tmp # 사이드카 전용 /tmp
emptyDir: {}
containers:
- name: helmfile-plugin
image: registry.example.com/argocd-cmp-helmfile:1.0.0
command: ["/var/run/argocd/argocd-cmp-server"]
securityContext:
runAsNonRoot: true
runAsUser: 999
volumeMounts:
- name: var-files
mountPath: /var/run/argocd
- name: plugins
mountPath: /home/argocd/cmp-server/plugins
- name: cmp-plugin
mountPath: /home/argocd/cmp-server/config/plugin.yaml
subPath: plugin.yaml
- name: cmp-tmp
mountPath: /tmp
command가 /var/run/argocd/argocd-cmp-server인 것에 주의합니다. 이 바이너리는 사이드카 이미지가 아니라 마운트된 볼륨에서 옵니다. 그래서 사이드카 이미지는 도구만 담고 있으면 되고, ArgoCD 버전이 올라가도 바이너리가 자동으로 맞습니다. 대신 var-files 마운트를 빠뜨리면 컨테이너가 아예 시작되지 않습니다.
# 6. 타임아웃과 성능
CMP를 붙이고 나면 성능 문제가 새로 생깁니다. 문서가 정한 상한이 있습니다.
argocd-repo-serverexecutes config management tools such ashelmorkustomizeand enforces a 90 second timeout.
90초입니다. helmfile처럼 여러 차트를 순회하며 각각 helm template을 도는 도구는 이 선을 쉽게 넘습니다. init에서 리포지터리를 갱신하는 네트워크 작업까지 포함되니 더 그렇습니다. 늘리는 방법은 있습니다.
# repo-server와 사이드카 양쪽에 같은 값을 준다
env:
- name: ARGOCD_EXEC_TIMEOUT
value: "5m"
여기에 컨트롤러 쪽 타임아웃이 하나 더 있습니다.
The app reconciliation fails with
Context deadline exceedederror if the manifest generation is taking too much time. ... increase the value of--repo-server-timeout-secondsand consider scaling up theargocd-repo-serverdeployment.
즉 두 군데를 같이 올려야 합니다. 하나만 올리면 다른 쪽에서 잘립니다. 오류 메시지가 서로 달라서(context deadline exceeded vs 플러그인 오류) 어느 쪽에서 잘렸는지 구분할 수 있습니다.
메모리도 문제가 됩니다. 여러 Application이 동시에 리프레시되면 사이드카에서 렌더링이 병렬로 돕니다.
The
--parallelismlimitflag controls how many manifests generations are running concurrently and helps avoid OOM kills.
CMP는 외부 프로세스를 띄우므로 표준 도구보다 메모리 변동이 큽니다. --parallelismlimit을 걸지 않으면 리프레시가 몰리는 순간 repo-server 파드가 OOM으로 죽고, 죽으면 모든 Application이 동시에 재시도해 상황이 악화됩니다.
캐시는 완충 역할을 합니다. ArgoCD는 커밋 SHA를 키로 생성 결과를 캐시하므로, 같은 커밋에 대한 반복 리프레시는 렌더링을 다시 돌지 않습니다. 문제는 커밋이 바뀌는 순간 모든 Application이 동시에 캐시 미스가 되는 경우입니다. 모노레포에서 한 커밋이 수십 개 Application에 걸리면 여기서 몰립니다.
# 7. 그래서 CMP를 써야 하는가
CMP는 동작하지만 비용이 명확합니다. 정리하면 이렇습니다.
| CMP | 렌더링을 CI로 옮기기 | |
|---|---|---|
| 렌더링 시점 | ArgoCD 리프레시마다 | 커밋 시 한 번 |
| repo-server 부하 | 큼 (타임아웃·OOM 관리 필요) | 없음 |
| Git에 남는 것 | 소스(helmfile.yaml) | 최종 매니페스트 |
| 배포될 내용 확인 | 렌더 후에야 알 수 있음 | PR diff로 바로 보임 |
| 도구 버전 관리 | 사이드카 이미지 | CI 이미지 |
| 운영 복잡도 | 사이드카 + 볼륨 + 타임아웃 | 브랜치 하나 추가 |
세 번째 열이 이른바 rendered manifests 패턴입니다. CI가 helmfile template 결과를 별도 브랜치에 커밋하고, ArgoCD는 그 브랜치를 평범한 YAML 디렉터리로 바라봅니다. ArgoCD 입장에서는 플러그인이 전혀 필요 없습니다.
이 방식의 실질적 이점은 성능이 아니라 가시성입니다. values를 한 줄 고쳤을 때 실제로 어떤 리소스가 바뀌는지 PR diff에 그대로 나타납니다. CMP 방식에서는 ArgoCD가 diff를 보여 주기 전까지 렌더 결과를 알 수 없습니다. 값 하나가 수십 개 리소스를 바꾸는 차트에서는 차이가 큽니다.
대가는 커밋 노이즈입니다. 렌더된 매니페스트 브랜치는 사람이 읽을 대상이 아니고 히스토리가 지저분해집니다. 소스 커밋과 렌더 커밋의 대응을 추적하는 규칙도 필요합니다.
기준을 하나 정하면, 도구가 빠르고 Application 수가 적으면 CMP가 간편하고, 렌더링이 느리거나 한 커밋이 많은 Application에 영향을 주면 CI로 옮기는 편이 낫습니다. 90초 타임아웃을 늘려야 하는 상황이 왔다면 이미 후자를 검토할 시점입니다.
보안 관점도 있습니다. CMP는 레포에 있는 내용을 근거로 repo-server 사이드카 안에서 명령을 실행합니다. 레포에 쓰기 권한이 있는 사람은 사실상 그 사이드카에서 코드를 실행할 수 있습니다. 사이드카를 비특권으로 돌리고(runAsNonRoot), 클러스터 자격 증명을 마운트하지 않고, 신뢰 경계를 레포 단위로 명확히 해야 합니다.
# 8. 직접 확인하는 방법
플러그인이 어디까지 붙었는지 단계별로 좁힐 수 있습니다.
# 1. 사이드카가 떴는가
kubectl -n argocd get pod -l app.kubernetes.io/name=argocd-repo-server \
-o jsonpath='{.items[0].spec.containers[*].name}{"\n"}'
# 2. 플러그인이 등록됐는가 - 소켓이 생겨야 한다
kubectl -n argocd exec deploy/argocd-repo-server -c helmfile-plugin -- \
ls -l /home/argocd/cmp-server/plugins/
# 3. plugin.yaml이 제대로 마운트됐는가
kubectl -n argocd exec deploy/argocd-repo-server -c helmfile-plugin -- \
cat /home/argocd/cmp-server/config/plugin.yaml
# 4. 사이드카 안에서 도구가 실제로 도는가
kubectl -n argocd exec deploy/argocd-repo-server -c helmfile-plugin -- \
helmfile --version
# 5. ArgoCD가 최종적으로 어떤 매니페스트를 만들었는가
argocd app manifests myapp
# 6. 렌더링 오류 원인 - 사이드카 로그에 stderr가 남는다
kubectl -n argocd logs deploy/argocd-repo-server -c helmfile-plugin --tail=100
2번이 비면 마운트 문제(5절), 4번이 실패하면 이미지 문제, 5번에서 파싱 오류가 나면 stdout 오염(3절), 5번이 타임아웃이면 6절입니다.
렌더 결과를 로컬에서 미리 맞춰 보는 것도 유용합니다.
# 사이드카와 같은 이미지로 같은 명령을 돌려 본다
docker run --rm -v "$PWD:/src" -w /src \
registry.example.com/argocd-cmp-helmfile:1.0.0 \
sh -c 'helmfile template --environment prod --include-crds --quiet 2>/dev/null' \
| kubectl apply --dry-run=client -f - >/dev/null && echo OK
kubectl apply --dry-run=client를 통과하면 최소한 유효한 매니페스트라는 뜻이고, 통과하지 못하면 stdout이 오염됐을 가능성이 높습니다.
# 9. 트러블슈팅
| 증상 | 원인 | 조치 |
|---|---|---|
| ConfigMap에 넣은 플러그인이 무시됨 | 2.8에서 제거된 방식 | 사이드카 방식으로 전환 |
| 사이드카 컨테이너가 시작 실패 | var-files 미마운트로 바이너리 없음 | /var/run/argocd 마운트 추가 |
| 플러그인을 못 찾음 | version이 있으면 이름-버전으로 참조 | Application의 plugin.name 수정 |
error converting YAML to JSON | 도구 로그가 stdout으로 섞임 | 로그를 stderr로, --quiet 사용 |
| 환경 변수가 비어 있음 | 사용자 변수는 ARGOCD_ENV_ 접두사 | 플러그인에서 접두사 붙여 읽기 |
| 간헐적 렌더링 오류 | repo-server와 /tmp 공유 | 사이드카 전용 emptyDir |
| 90초에서 잘림 | 기본 실행 타임아웃 | ARGOCD_EXEC_TIMEOUT 상향 |
context deadline exceeded | 컨트롤러 쪽 타임아웃 | --repo-server-timeout-seconds 상향 |
| repo-server가 OOM으로 재시작 | 렌더링 동시 실행 | --parallelismlimit, 리소스 상향 |
| 플러그인이 예상과 다른 것이 선택됨 | 자동 발견 조건 중복 | Application에 명시 지정 |
# 10. 마무리
- 지금 유효한 CMP는 사이드카 방식 하나뿐입니다. ConfigMap에 플러그인을 정의하는 문서는 2.8 이전 것입니다.
generate의 표준 출력은 매니페스트 전용입니다. 도구 로그가 한 줄만 섞여도 전체 파싱이 깨집니다.- 사용자 환경 변수는
ARGOCD_ENV_접두사가 붙어 전달되고, ArgoCD 기본 변수는 안 붙습니다. 두 계열을 구분해야 합니다. - 90초 실행 타임아웃과 컨트롤러 타임아웃은 별개입니다. 하나만 올리면 다른 쪽에서 잘립니다.
- 타임아웃을 늘려야 할 만큼 렌더링이 무겁다면, 렌더링을 CI로 옮겨 ArgoCD에게는 평범한 YAML만 보여 주는 쪽을 검토할 시점입니다. 성능뿐 아니라 PR diff로 실제 변경을 볼 수 있다는 점이 더 큰 이득입니다.
기존 릴리스를 GitOps로 넘기는 과정 자체는 수동 설치된 컴포넌트를 Helm 릴리스로 인수하고 GitOps에 넘기기 (opens new window)에, 넘긴 뒤 생기는 동기화 충돌은 ArgoCD ignoreDifferences가 동작하지 않는 세 가지 이유 (opens new window)에 정리했습니다.