13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] Kubernetes

  • [Linux] Linux Namespace 격리 기술, 실제 적용과 검증 방법

    [Linux] Linux Namespace 격리 기술, 실제 적용과 검증 방법

    Linux Namespace 격리 기술, 실제 적용과 검증 방법

    Linux Namespace는 요즘 컨테이너 기술 이야기할 때 거의 빠지지 않는 핵심 요소입니다. Docker를 쓰든, containerd를 쓰든, Kubernetes 위에서 파드를 띄우든 결국 밑바닥에는 이런 격리 기술이 깔려 있거든요. 근데 운영하다 보면 막연히 “컨테이너는 분리돼 있다” 정도로만 이해해서는 한계가 옵니다. 장애가 났을 때 왜 프로세스는 안 보이는지, 왜 네트워크가 따로 노는지, 왜 마운트가 호스트랑 다르게 보이는지 알아야 하더라고요.

    저도 처음엔 이게 뭔가 싶었습니다. 컨테이너는 많이 올려봤는데, 정작 Linux Namespace를 직접 까보지 않으면 문제 원인을 제대로 못 잡겠더라고요. 특히 홈랩에서 테스트할 때 네트워크 네임스페이스를 잘못 건드려서 통신이 안 붙고, 마운트 네임스페이스 때문에 파일이 보였다 안 보였다 해서 삽질 좀 했습니다 ㅎㅎ 이번 글에서는 이론만 나열하지 않고, Linux Namespace를 실제로 어떻게 적용하고 검증하는지, 그리고 현업이나 홈랩에서 어디에 유용한지 경험 기준으로 풀어보겠습니다.

    Linux Namespace 격리 구조와 컨테이너 기술 아키텍처 개요 이미지

    프로세스, 네트워크, 마운트가 각각 분리되는 구조를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 Linux Namespace를 이해해야 하나요

    쉽게 말해 Linux Namespace는 커널(Kernel)이 프로세스에게 보여주는 세상을 따로 나누는 기능이거든요. 같은 서버 위에서 돌아가더라도 A 프로세스는 자기 PID 목록만 보고, B 프로세스는 자기 네트워크 인터페이스만 보게 만들 수 있습니다. 이게 컨테이너 기술의 출발점입니다.

    여기서 중요한 포인트! 가상머신(Virtual Machine)은 커널까지 통째로 분리하는 방식이고, Namespace는 같은 커널을 공유하면서 보이는 범위만 나누는 방식입니다. 그래서 더 가볍고 빠릅니다. 대신 보안 경계(Security Boundary)로 과신하면 안 됩니다. 저도 처음엔 “분리됐으니 완전히 안전하겠지”라고 생각했는데, 실제로 써보니 Namespace는 격리의 한 축일 뿐이고 cgroups, capabilities, seccomp 같은 요소랑 같이 봐야 하더라고요.

    2. Linux Namespace 핵심 개념 정리

    대표적인 Namespace는 아래처럼 이해하시면 됩니다.

    종류 영문 무엇을 격리하나 현실적인 사용 예
    PID Process ID Namespace 프로세스 번호와 프로세스 트리 컨테이너 내부에서 1번 프로세스만 보이게 구성
    NET Network Namespace 네트워크 인터페이스, 라우팅, 포트 컨테이너별 가상 NIC와 독립 라우팅
    MNT Mount Namespace 마운트 지점과 파일시스템 뷰 호스트와 다른 루트 파일시스템 제공
    UTS UNIX Timesharing System Namespace 호스트명, 도메인명 컨테이너별 hostname 분리
    IPC Inter-Process Communication Namespace 공유 메모리, 세마포어, 메시지 큐 프로세스 간 IPC 격리
    USER User Namespace UID/GID 매핑 컨테이너 root를 호스트 비특권 사용자로 매핑

    한 줄로 줄이면 이렇습니다. Linux Namespace는 프로세스가 보는 시스템 자원을 논리적으로 쪼개는 장치거든요. 컨테이너 런타임은 이걸 조합해서 “내 것처럼 보이지만 사실은 제한된 환경”을 만들어냅니다.

    3. 실제 적용 사례: 컨테이너 기술에서 어떻게 쓰이나요

    가장 흔한 사례는 역시 Docker나 containerd 같은 런타임입니다. 예를 들어 웹 애플리케이션 컨테이너 하나를 띄운다고 해보죠. 그 안의 애플리케이션은 자기 PID 공간만 보고, 자기 네트워크 인터페이스만 보고, 자기 루트 파일시스템만 봅니다. 사용자는 그냥 컨테이너가 하나 올라간 걸로 보지만, 내부적으로는 여러 Namespace가 같이 엮여 있죠.

    제가 홈랩에서 많이 해보는 패턴은 이렇습니다.

    1. 리버스 프록시(Reverse Proxy) 컨테이너는 외부와 붙게 둡니다.
    2. 백엔드 애플리케이션 컨테이너는 별도 네트워크 네임스페이스에 둡니다.
    3. 로그 수집기나 모니터링 에이전트는 필요한 마운트만 노출합니다.
    4. 가능하면 User Namespace까지 고려해서 호스트 권한을 줄입니다.

    이 방식이 왜 좋냐면, 장애 범위가 줄어듭니다. 어떤 애플리케이션이 오작동해서 내부 프로세스를 마구 생성하더라도, 적어도 다른 워크로드의 프로세스 공간까지 바로 오염시키지는 않거든요. 물론 이것만으로 충분하진 않지만, 운영 안정성에서 체감 차이가 꽤 큽니다.

    4. 실전 구현: unshare로 Namespace 직접 만들기

    이제 이론 말고 직접 보겠습니다. 저는 Namespace를 설명할 때 항상 <code>unshare부터 보여드립니다. 이걸 한 번 손으로 쳐보면 감이 확 옵니다. 아래 예시는 PID, UTS, Mount Namespace를 분리해서 새 쉘을 띄우는 방식입니다.

    sudo unshare --fork --pid --mount --uts /bin/bash

    이 상태에서 호스트명도 바꿔보겠습니다.

    hostname ns-lab
    hostname
    ps -ef

    여기서 ps -ef를 보면 호스트 전체 프로세스가 아니라 새 Namespace 기준의 프로세스만 보여집니다. 처음 보면 “어? 왜 이렇게 적지?” 싶거든요. 그게 정상입니다.

    마운트도 분리해보죠.

    mount -t proc proc /proc
    mount | grep proc

    Mount Namespace를 분리한 뒤 /proc를 다시 마운트하지 않으면 프로세스 정보가 기대와 다르게 보일 수 있습니다. 이 부분에서 많이 헷갈립니다. 저도 예전에 “PID Namespace가 안 먹었나?” 하고 한참 봤었는데, 알고 보니 /proc를 새로 안 붙였던 거였습니다.

    Linux Namespace 실습에서 PID와 hostname 분리를 확인하는 터미널 이미지

    실습 중간에 확인해야 하는 핵심 명령과 분리된 PID 공간이 보이는 터미널 예시입니다.

    4-1. 네트워크 Namespace 실습

    네트워크는 조금 더 재미있습니다. 별도 네임스페이스를 만들고 veth pair(가상 이더넷 쌍)로 연결하면 거의 미니 컨테이너 네트워크처럼 테스트할 수 있죠.

    sudo ip netns add ns1
    sudo ip link add veth-host type veth peer name veth-ns
    sudo ip link set veth-ns netns ns1
    sudo ip addr add 10.10.10.1/24 dev veth-host
    sudo ip link set veth-host up
    sudo ip netns exec ns1 ip addr add 10.10.10.2/24 dev veth-ns
    sudo ip netns exec ns1 ip link set lo up
    sudo ip netns exec ns1 ip link set veth-ns up
    sudo ip netns exec ns1 ping -c 3 10.10.10.1

    이 실습이 좋은 이유는 컨테이너 기술의 네트워크 격리 원리를 거의 그대로 보여주기 때문입니다. 브리지(Bridge), NAT, 포트 포워딩까지 붙이면 Docker 네트워크 구조를 훨씬 잘 이해하게 됩니다.

    4-2. 간단한 프로세스 격리 확인용 C 코드

    시스템 프로그래밍 관점에서 보면 clone() 계열 시스템 콜로 Namespace를 만드는 흐름을 이해하는 것도 중요합니다. 아래 코드는 개념 확인용으로 아주 단순화한 예시입니다.

    #define _GNU_SOURCE
    #include <sched.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    #define STACK_SIZE 1024 * 1024
    
    static int child_func(void *arg) {
        sethostname("ns-child", 8);
        system("hostname; ps -ef");
        return 0;
    }
    
    int main() {
        char *stack = malloc(STACK_SIZE);
        if (!stack) return 1;
    
        pid_t pid = clone(child_func, stack + STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL);
        if (pid == -1) return 1;
    
        waitpid(pid, NULL, 0);
        free(stack);
        return 0;
    }

    실무에서 직접 이런 코드를 매번 짜진 않지만, 런타임이 내부적으로 어떤 일을 하는지 이해하는 데는 꽤 도움이 됩니다.

    5. 주의사항과 트러블슈팅: 여기서 많이 막힙니다

    실제로 써보니 Linux Namespace는 개념보다 디버깅이 더 중요하더라고요. 아래는 제가 자주 겪었던 문제들입니다.

    • ⚠️ /proc 재마운트 누락: PID Namespace를 만들었는데 프로세스가 이상하게 보이면 먼저 확인하세요.
    • ⚠️ loopback 미활성화: Network Namespace 안에서 lo를 올리지 않으면 localhost 통신이 어색하게 깨집니다.
    • ⚠️ 권한 문제: User Namespace를 섞으면 UID/GID 매핑 때문에 파일 접근이 예상과 다를 수 있습니다.
    • ⚠️ 네트워크 라우팅 누락: veth만 연결해놓고 라우팅이나 NAT를 안 잡으면 외부 통신이 안 됩니다.

    특히 네트워크 부분은 진짜 많이 삽질합니다. 저는 예전에 “분명 인터페이스는 올라왔는데 왜 패킷이 안 나가지?” 하고 tcpdump만 한참 봤거든요. 알고 보니 호스트 쪽 IP 포워딩(IP Forwarding)이 빠져 있었습니다. 그래서 아래처럼 확인하는 습관을 들였습니다.

    sysctl net.ipv4.ip_forward
    ip netns exec ns1 ip route
    ip addr show veth-host

    보안 측면에서도 한 가지 강조하고 싶습니다. Namespace는 강력하지만 만능은 아니거든요. 격리 기술이라고 해서 무조건 안전한 게 아니고, 커널 취약점이나 잘못된 capability 설정이 있으면 경계가 흐려질 수 있습니다. 그래서 현업에서는 보통 cgroups, seccomp, AppArmor 또는 SELinux 같은 보안 메커니즘과 함께 갑니다.

    Linux Namespace 기반 네트워크 격리와 veth 연결 구성도

    호스트와 네임스페이스가 veth로 연결되고 패킷이 흐르는 경로를 이해하기 위한 구성도입니다.

    6. 검증과 결과 확인: 분리됐는지 어떻게 보나

    설정만 해놓고 끝내면 안 됩니다. 반드시 검증해야 합니다. 저는 보통 아래 순서로 봅니다.

    1. 프로세스가 분리됐는지 ps, /proc로 확인합니다.
    2. 호스트명이 분리됐는지 hostname으로 확인합니다.
    3. 네트워크 인터페이스가 분리됐는지 ip addr로 확인합니다.
    4. 마운트 뷰가 다른지 mount 출력으로 비교합니다.
    5. 필요하면 네임스페이스 핸들을 lsns로 확인합니다.
    lsns
    sudo ip netns exec ns1 ip addr
    sudo ip netns exec ns1 hostname
    sudo ip netns exec ns1 ps -ef

    이렇게 보면 각 격리가 실제로 먹었는지 금방 드러납니다. lsns는 운영 중 문제 파악할 때 꽤 유용하더라고요. 어떤 프로세스가 어떤 Namespace에 속했는지 감을 잡는 데 도움이 되거든요.

    실무 관점의 결과를 정리하면 이렇습니다.

    검증 항목 성공 시 기대 결과 실패 시 의심 포인트
    PID 분리 내부 프로세스만 보임 /proc 재마운트 누락
    hostname 분리 Namespace 내부 이름만 변경됨 UTS 미분리
    네트워크 분리 별도 인터페이스/라우팅 표시 veth 연결, lo 활성화 누락
    마운트 분리 호스트와 다른 마운트 목록 Mount Namespace 미적용
    Linux Namespace 적용 결과를 검증하는 운영 확인 화면

    분리된 Namespace가 실제로 적용됐는지 확인하는 검증 결과 예시 이미지입니다.

    7. 실제 운영에서 어디에 써먹나

    혹시 이런 경험 있으신가요? 개발팀은 “컨테이너니까 독립적이다”라고 생각하는데, 운영팀은 “그래도 결국 같은 커널인데?”라는 불안이 남습니다. 둘 다 맞는 말이거든요. 그래서 Namespace를 이해하면 커뮤니케이션이 훨씬 쉬워집니다.

    제가 현장에서 보거나 직접 적용해본 패턴은 대체로 이렇습니다.

    • 멀티테넌트(Multi-tenant) 환경에서 워크로드 간 기본 격리
    • CI 러너나 빌드 작업의 프로세스/파일시스템 분리
    • 네트워크 실험 환경 구성과 장애 재현
    • 보안 테스트용 최소 권한 실행 환경 만들기

    특히 홈랩에서는 이게 정말 좋습니다. VM 여러 대 띄우기 부담스러울 때 Namespace 기반으로 작은 실험 환경을 빠르게 만들 수 있거든요. 물론 커널 공유라는 특성 때문에 VM을 완전히 대체하진 못하지만, 문제 재현과 구조 학습에는 진짜 편하더라고요.

    8. 정리와 다음 단계

    Linux Namespace를 이해하면 컨테이너가 갑자기 훨씬 덜 추상적으로 보입니다. 그냥 “신기한 배포 단위”가 아니라, PID Namespace, Network Namespace, Mount Namespace 같은 커널 기능의 조합으로 보이기 시작하거든요. 저도 처음엔 Docker 명령만 외웠는데, 바닥 기술을 알고 나니 장애 대응 속도가 확실히 빨라졌습니다. 드디어 됐다! 싶은 순간이 오더라고요.

    오늘 정리한 핵심만 다시 적어보면 이렇습니다.

    • Linux Namespace는 프로세스가 보는 시스템 자원을 분리합니다.
    • 컨테이너 기술은 여러 Namespace를 조합해 가벼운 실행 환경을 만듭니다.
    • 실전에서는 네트워크, 마운트, 권한 매핑에서 가장 많이 막힙니다.
    • 검증 명령을 반드시 함께 익혀야 운영에서 써먹을 수 있습니다.

    다음 단계로는 cgroups와 함께 보면 좋습니다. Namespace가 “보이는 범위”를 나눈다면, cgroups는 CPU와 메모리 같은 자원 사용량을 통제하거든요. 다음 글에서 다룰 예정입니다. 그리고 이전 글에서 컨테이너 런타임 구조를 정리했다면 같이 보시면 연결이 더 잘 됩니다.

    Linux Namespace 종류와 실제 적용 포인트를 정리한 요약 인포그래픽

    PID, NET, MNT 등 주요 Namespace와 운영 포인트를 빠르게 복습할 수 있는 요약 이미지입니다.

    9. 자주 묻는 질문

    Q1. Namespace만 쓰면 컨테이너를 직접 만든 셈인가요?

    완전히 그렇진 않습니다. Namespace는 핵심 구성요소지만, 실제 컨테이너 환경에는 cgroups, 루트 파일시스템, capability 제어, 보안 정책 등이 함께 필요하죠.

    Q2. Docker를 쓰는데도 Namespace를 따로 알아야 하나요?

    네, 운영이나 장애 대응을 생각하면 알아두는 게 좋습니다. 특히 네트워크 안 붙음, 파일 안 보임, 프로세스 이상 동작 같은 문제는 바닥 기술을 이해해야 빨리 풀린다고 봅니다.

    Q3. User Namespace는 꼭 써야 하나요?

    상황에 따라 다릅니다. 보안상 이점은 분명하지만, 권한 매핑과 파일 접근에서 복잡도가 올라갑니다. 운영 환경에서는 호환성과 보안 요구사항을 함께 봐야 합니다.

  • [Linux] cgroup vs namespace: 컨테이너 격리의 핵심 비교 분석

    [Linux] cgroup vs namespace: 컨테이너 격리의 핵심 비교 분석

    [리눅스 커널] cgroup vs namespace: 컨테이너 격리 핵심 비교

    컨테이너 기술을 조금만 깊게 파보면 결국 cgroup vs namespace 이야기로 귀결되더라고요. 저도 처음 Docker를 쓸 때는 그냥 “컨테이너는 가볍고 빠르다” 정도로만 이해했었는데, 실제로 장애를 따라가다 보니 왜 어떤 프로세스는 CPU를 독식하고, 왜 어떤 프로세스는 다른 프로세스 목록을 못 보는지 여기서 갈리더라고요. 쉽게 말해 namespace(네임스페이스, 리소스를 보이는 범위로 분리)는 “무엇을 볼 수 있느냐”를 나누고, cgroup(컨트롤 그룹, 자원 사용량을 제어하는 기능)은 “얼마나 쓸 수 있느냐”를 관리합니다. 이 차이를 이해하면 컨테이너 기술, 격리, 리눅스 커널의 구조가 한 번에 정리됩니다.

    실제로 홈랩에서 테스트 환경을 만들다가, 프로세스는 잘 분리됐는데 메모리 제한이 안 먹어서 삽질 좀 했습니다 ㅎㅎ 그때 확실히 느꼈어요. 컨테이너 격리는 한 가지 기능으로 되는 게 아니라, 여러 커널 기능을 조합해서 만들어지는 거였거든요.

    cgroup vs namespace 기반 컨테이너 격리 구조를 보여주는 리눅스 커널 아키텍처 이미지

    namespace와 cgroup이 각각 어떤 역할로 컨테이너 격리를 만드는지 보여주는 개요 이미지입니다.

    1. 왜 cgroup과 namespace를 같이 봐야 할까요?

    여기서 중요한 게 있어요. 많은 분들이 컨테이너를 “가벼운 VM”처럼 이해하시는데, 엄밀히 보면 다릅니다. VM은 하이퍼바이저 위에 게스트 OS가 따로 올라가고, 컨테이너는 호스트의 리눅스 커널을 공유합니다. 그러니 격리도 커널 기능에 의존할 수밖에 없죠.

    • namespace: 프로세스가 보는 세계를 분리합니다.
    • cgroup: 프로세스가 쓰는 자원을 제어합니다.
    • capabilities(리눅스 권한 비트 분리), seccomp(시스템 콜 필터링), LSM 계열 기능은 추가 보안 계층으로 붙습니다.

    즉, 컨테이너 기술에서 격리는 한 방에 끝나는 개념이 아니라 층층이 쌓이는 구조예요. 저도 처음엔 namespace만 알면 다 되는 줄 알았는데, 운영 환경에서는 cgroup 쪽을 훨씬 자주 보게 되더라고요. 특히 CPU 폭주나 메모리 OOM(Out Of Memory) 이슈는 거의 cgroup 쪽에서 원인을 찾게 됩니다.

    2. namespace(네임스페이스) 쉽게 이해하기

    쉽게 말해 namespace는 “같은 커널 위에 있지만 서로 다른 방에 들어가 있는 상태”를 만드는 기능입니다. 같은 호스트인데도 프로세스마다 보이는 PID, 네트워크 인터페이스, 마운트 포인트가 달라질 수 있어요.

    대표적인 namespace 종류

    • PID namespace: 프로세스 번호 공간을 분리합니다.
    • NET namespace: 네트워크 인터페이스, 라우팅 테이블, 포트를 분리합니다.
    • MNT namespace: 마운트 지점을 분리합니다.
    • UTS namespace: 호스트명, 도메인명 같은 시스템 식별 정보를 분리합니다.
    • IPC namespace: 프로세스 간 통신 자원을 분리합니다.
    • USER namespace: 사용자와 그룹 ID 매핑을 분리합니다.

    예를 들어 PID namespace 안에서는 프로세스가 자기 자신을 PID 1로 볼 수도 있어요. 컨테이너 안에서 <code>ps를 쳤을 때 호스트 전체 프로세스가 안 보이는 이유가 바로 이겁니다. 실제로 써보니까 이 개념 하나만 이해해도 “왜 컨테이너 안에서는 세상이 작아 보이는지”가 확 들어오더라고요.

    3. cgroup(컨트롤 그룹)은 뭐가 다를까요?

    namespace가 시야를 나눈다면, cgroup은 행동 한도를 정합니다. CPU를 얼마나 쓸지, 메모리를 얼마나 먹을지, I/O를 어느 정도까지 허용할지를 관리하는 쪽이죠. 그래서 cgroup은 격리라기보다 제어와 회계(accounting) 관점이 훨씬 강합니다.

    운영에서 흔히 보는 상황이 이거예요. 애플리케이션 하나가 메모리를 계속 먹습니다. namespace만 있으면 자기 공간 안에서만 보일 뿐이지, 호스트 메모리는 그대로 압박할 수 있거든요. 반대로 cgroup으로 제한을 걸어두면 그 프로세스 그룹은 정해진 범위를 넘기기 어려워집니다. 장애 대응할 때 이 차이가 꽤 크더라고요.

    항목 namespace cgroup
    주요 목적 리소스 가시성 분리 자원 사용량 제어 및 계측
    핵심 질문 무엇을 볼 수 있나? 얼마나 쓸 수 있나?
    예시 다른 PID/네트워크를 못 봄 CPU, 메모리 사용량 제한
    영향 범위 프로세스 관점의 논리적 격리 호스트 자원 배분 정책
    컨테이너에서의 역할 독립된 환경처럼 보이게 함 과도한 자원 사용을 막음

    한 줄 정리: namespace만 있으면 “따로 있는 것처럼 보이는 상태”이고, cgroup까지 있어야 “민폐를 덜 끼치는 상태”가 돼요. 이거 진짜 중요합니다.

    4. 실전 구현: namespace부터 직접 확인해보겠습니다

    제가 직접 해보니 추상 개념으로만 볼 때보다 unshare로 눈으로 확인하는 게 훨씬 빠르더라고요. 아래 예시는 리눅스에서 새로운 UTS, PID, mount namespace를 만들어보는 흐름입니다.

    1. 현재 호스트 이름과 프로세스 상태를 확인합니다.
    2. unshare로 새 namespace를 만듭니다.
    3. 새 환경 안에서 hostname과 프로세스 목록이 어떻게 달라지는지 봅니다.
    hostname
    ps -ef | head
    
    sudo unshare --uts --pid --mount --fork bash
    
    hostname container-lab
    mount -t proc proc /proc
    ps -ef
    hostname

    여기서 --fork를 주는 이유는 새 PID namespace 안에서 새 프로세스를 시작하기 위해서예요. 그리고 mount -t proc proc /proc를 안 하면 ps 출력이 기대와 다를 수 있거든요. 저도 처음엔 이걸 빼먹어서 “왜 분리 안 됐지?” 하고 한참 봤었습니다.

    cgroup vs namespace 실습 중 namespace 분리 셸 구성을 표현한 이미지

    namespace 실습 흐름과 분리된 셸 진입 과정을 보여주는 이미지입니다.

    현재 시스템의 namespace 확인

    lsns

    lsns를 보면 현재 시스템에 어떤 namespace가 잡혀 있는지 확인할 수 있어요. 컨테이너 런타임이 떠 있는 서버에서는 생각보다 다양한 namespace가 보이더라고요.

    5. 실전 구현: cgroup으로 자원 제한 걸어보기

    이제 cgroup입니다. 최근 리눅스 배포판은 보통 cgroup v2 기준으로 보는 게 편해요. 시스템마다 마운트 구조가 조금 다를 수는 있지만, 기본 개념은 비슷합니다. CPU와 메모리 제한을 설정할 수 있고, 프로세스를 특정 cgroup 디렉터리 아래로 넣어 관리하죠.

    mount | grep cgroup
    cat /sys/fs/cgroup/cgroup.controllers
    
    sudo mkdir /sys/fs/cgroup/demo-limit
    echo $$ | sudo tee /sys/fs/cgroup/demo-limit/cgroup.procs

    위처럼 현재 셸을 특정 cgroup에 넣을 수 있어요. 다만 실제 제한을 적용하려면 컨트롤러 활성화와 권한 구조를 이해해야 해서, 운영 서버에서는 systemd 기반 도구를 쓰는 편이 안전합니다.

    systemd-run으로 메모리 제한 테스트

    systemd-run --user --scope -p MemoryMax=200M -p CPUQuota=50% bash
    
    cat /sys/fs/cgroup/$(systemctl --user show --property=ControlGroup --value)/memory.max

    환경에 따라 사용자 세션에서 안 될 수도 있어요. 그런 경우는 시스템 범위에서 실행하거나 테스트 VM에서 보는 쪽이 낫습니다. 핵심은 cgroup이 프로세스를 안 보이게 만드는 기능이 아니라, 자원 사용 정책을 강제하는 기능이라는 점이에요.

    Docker로 보면 훨씬 익숙해요.

    docker run --rm -it --memory=256m --cpus=1 alpine sh
    
    cat /sys/fs/cgroup/memory.max 2>/dev/null || true
    cat /sys/fs/cgroup/cpu.max 2>/dev/null || true

    컨테이너 런타임은 이런 옵션을 받아 내부적으로 cgroup 설정과 namespace 구성을 함께 처리해요. 우리가 편하게 docker run 한 줄로 쓰는 뒤에서 리눅스 커널이 꽤 많은 일을 하는 셈이죠.

    6. cgroup vs namespace 비교를 실무 관점에서 정리해보면

    혹시 이런 경험 있으신가요? 컨테이너는 분명 분리돼 있는데, 한 컨테이너가 CPU를 잡아먹어서 같은 노드의 다른 워크로드까지 느려지는 상황 말이에요. 이럴 때 namespace를 아무리 잘 나눠도 해결이 안 됩니다. 반대로 cgroup만 있고 namespace가 약하면 프로세스 시야가 너무 넓어서 격리 체감이 떨어져요.

    • namespace가 강한 상황: 프로세스, 네트워크, 파일시스템 관점에서 독립된 환경처럼 보입니다.
    • cgroup이 강한 상황: noisy neighbor(시끄러운 이웃, 자원 독식 워크로드) 문제를 줄이기 좋습니다.
    • 둘 다 필요: 컨테이너 기술에서 실용적인 격리는 대부분 둘의 조합이에요.

    제가 홈랩 쿠버네티스 환경을 만질 때도 결국 보는 건 두 가지였어요. “이 파드는 무엇을 볼 수 있지?” 그리고 “이 파드는 얼마나 먹지?” 질문이 딱 namespace와 cgroup으로 나뉘더라고요.

    7. ⚠️ 주의사항과 트러블슈팅

    여기서부터는 제가 실제로 많이 헷갈렸던 부분입니다. 처음엔 개념보다 환경 차이 때문에 더 막히더라고요.

    1) ps 결과가 기대와 다를 때

    PID namespace를 만들었는데도 프로세스가 그대로 보이는 경우가 있어요. 보통 /proc를 새로 마운트하지 않아서 그렇습니다.

    mount -t proc proc /proc

    2) cgroup 디렉터리는 있는데 제한이 안 걸릴 때

    cgroup v1과 v2 차이, systemd 관리 방식, 권한 문제 때문일 수 있어요. 특히 배포판마다 기본 구성이 다르니 무작정 예전 블로그 예제를 복붙하면 안 맞는 경우가 있습니다. 저도 예전 자료 따라 했다가 파일 이름이 달라서 한참 헤맸습니다.

    3) 컨테이너 보안과 자원 제어를 같은 문제로 보면 꼬여요

    이건 실무에서 꽤 중요한 부분이에요.

    • namespace는 보이는 범위를 줄이는 쪽입니다.
    • cgroup은 성능 안정성과 자원 관리 쪽입니다.
    • seccomp, AppArmor, SELinux 같은 보안 계층은 또 별개입니다.

    즉, 보안 사고를 막는 이야기와 리소스 폭주를 막는 이야기를 한 덩어리로 보면 판단이 흐려집니다.

    4) USER namespace는 특히 신중하게 봐야 해요

    USER namespace는 루트 권한 매핑 문제와 연결되기 때문에 환경에 따라 정책 차이가 커요. 실습은 가능하지만, 운영 서버에서는 배포판 기본 정책과 런타임 설정을 반드시 같이 봐야 합니다.

    cgroup vs namespace 비교 중 cgroup v2 자원 제어 구조를 설명하는 이미지

    cgroup v2 구조와 자원 제한이 적용되는 핵심 지점을 시각화한 이미지입니다.

    8. 검증과 결과 확인

    설정은 했는데 실제로 격리가 됐는지 확인을 안 하면 의미가 없죠. 저는 보통 아래처럼 확인합니다.

    1. namespace 분리 여부 확인
    2. cgroup 제한 값 확인
    3. 실제 워크로드를 넣어 동작 확인
    lsns
    
    cat /proc/self/cgroup
    
    cat /sys/fs/cgroup/cpu.max 2>/dev/null || true
    cat /sys/fs/cgroup/memory.max 2>/dev/null || true

    Docker 환경이라면 다음도 자주 봐요.

    docker inspect <container_id>
    docker stats

    검증 포인트는 단순합니다.

    • 프로세스 목록이 호스트와 다르게 보이는가?
    • 호스트명이나 네트워크 인터페이스가 분리되어 보이는가?
    • 메모리/CPU 제한이 파일 시스템이나 런타임 정보에 반영되는가?
    • 부하를 줬을 때 제한 정책이 실제로 동작하는가?

    실제로 써보니까, 격리는 “설정했다”보다 “증명했다”가 더 중요하더라고요. 특히 장애 분석 문서 남길 때 이 확인 단계가 있어야 나중에 팀원과 같은 그림을 볼 수 있어요.

    cgroup vs namespace 검증 결과를 운영 화면처럼 보여주는 이미지

    격리 검증 결과를 확인하는 장면을 담은 이미지입니다.

    9. 정리: 컨테이너 기술에서 둘 중 뭐가 더 중요할까요?

    제 답은 늘 같습니다. 둘 중 하나가 아니라 둘 다예요. namespace는 컨테이너를 컨테이너답게 보이게 만들고, cgroup은 컨테이너를 운영 가능한 상태로 만들어줍니다. 하나는 시야를 분리하고, 다른 하나는 자원을 통제하죠.

    질문 봐야 할 기능 실무 해석
    왜 다른 프로세스가 안 보일까? namespace 격리된 실행 환경
    왜 메모리가 256MB 이상 못 올라갈까? cgroup 자원 제한 정책
    왜 한 컨테이너가 노드 전체를 먹지 못할까? cgroup 멀티테넌시 안정성
    왜 컨테이너 안 hostname이 다를까? namespace 독립된 시스템처럼 보이기

    저도 처음엔 cgroup vs namespace를 경쟁 관계처럼 외웠었는데, 실제로는 역할 분담에 더 가깝거든요. 그래서 컨테이너 기술을 공부할 때는 둘을 따로 암기하기보다, 하나의 워크로드가 커널 위에서 어떻게 분리되고 제한되는지 흐름으로 보는 게 훨씬 이해가 잘 됩니다.

    다음 글에서는 seccomp(시스템 콜 필터링)와 capabilities(권한 비트 세분화)까지 이어서, 왜 “컨테이너는 격리되지만 완전한 보안 경계로 보면 안 된다”는 말이 나오는지 다뤄볼 예정입니다. 이전 글에서 다룬 Docker 기본 구조와 함께 보시면 더 잘 연결되실 겁니다.

    cgroup vs namespace 차이를 한눈에 정리한 비교 인포그래픽 이미지

    cgroup과 namespace의 핵심 차이를 요약한 비교 인포그래픽 이미지입니다.

    자주 묻는 질문(FAQ)

    Q1. namespace만 있으면 컨테이너가 완성되나요?

    아니에요. namespace만으로는 자원 제한이 부족합니다. 실무에서는 cgroup, capabilities, seccomp 같은 요소가 함께 들어갑니다.

    Q2. cgroup은 보안 기능인가요?

    주된 목적은 자원 제어와 계측이에요. 보안에 간접적으로 도움이 될 수는 있지만, 보안 격리 그 자체로 보기엔 부족합니다.

    Q3. 리눅스 커널을 공유하면 위험하지 않나요?

    맞아요. 그래서 컨테이너는 VM과 다른 보안 모델을 가져요. 격리 수준과 운영 목적을 구분해서 설계해야 합니다.

    Q4. 쿠버네티스에서도 결국 같은 원리인가요?

    네, 상위 오케스트레이션이 붙어도 아래에서는 리눅스 커널의 namespace와 cgroup 같은 기능을 활용합니다.

  • [k8s] ArgoCD 도입 실패 사례 분석: GitOps 전환 시 흔한 실수와 방지 전략

    [k8s] ArgoCD 도입 실패 사례 분석: GitOps 전환 시 흔한 실수와 방지 전략

    [DevOps] ArgoCD 도입 실패 사례 분석과 GitOps 실수 방지

    ArgoCD 도입 실패 이야기는 생각보다 흔합니다. 저도 처음엔 GitOps(깃옵스, Git을 단일 진실 공급원으로 삼는 운영 방식)만 붙이면 배포가 깔끔해질 줄 알았거든요. 그런데 실제로 해보니, ArgoCD(아르고CD, Kubernetes 선언형 배포 도구)를 넣는 순간 오히려 배포가 더 복잡해지는 팀도 많았습니다. 특히 CI/CD 전환 사례를 보면, 기술 문제가 아니라 역할 분리, 저장소 구조, 승인 흐름 같은 운영 설계에서 먼저 무너지는 경우가 많더라고요. 이번 글은 제가 현업과 홈랩에서 반복해서 본 ArgoCD 도입 실패 패턴을 중심으로, 왜 그런 일이 생기는지와 어떻게 피해야 하는지를 정리해보겠습니다.

    혹시 이런 경험 있으신가요? 배포 자동화는 넣었는데 누가 어떤 값을 바꿨는지 더 헷갈리고, 장애가 나면 Git이 문제인지 클러스터가 문제인지부터 추적하게 되는 상황 말입니다. 여기서 중요한 포인트는, ArgoCD 자체가 문제라기보다 GitOps 실수가 누적되면서 운영 복잡도가 폭발한다는 점입니다.

    ArgoCD 도입 실패와 GitOps 전환 흐름을 설명하는 아키텍처 이미지

    Git 저장소, CI 파이프라인, ArgoCD, Kubernetes 클러스터 사이의 흐름과 실패 포인트를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 ArgoCD 도입 실패가 반복될까요?

    쉽게 말해 ArgoCD는 배포 버튼을 없애는 도구가 아니라, 변경 관리 방식 자체를 바꾸는 도구입니다. 그래서 예전 CI/CD에서는 잘 통하던 습관이 GitOps로 오면 바로 문제를 만듭니다. 예를 들면 이런 식입니다.

    • 운영값을 Git이 아니라 사람 손으로 클러스터에서 직접 수정함
    • 애플리케이션 코드 저장소와 배포 매니페스트 저장소의 책임이 불명확함
    • 자동 동기화(auto-sync)를 켰는데 승인 절차는 그대로 수동 운영을 기대함
    • Helm(헬름, Kubernetes 패키지 관리 도구) 값 파일이 환경별로 제멋대로 늘어남
    • 장애가 나도 누가 마지막 변경을 만들었는지 추적이 어려움

    저도 처음엔 이게 뭔가 싶었는데, 결국 핵심은 하나였습니다. ArgoCD는 배포 도구이면서 동시에 운영 규율을 강제하는 도구라는 점입니다. 팀이 그 규율을 합의하지 않은 상태에서 도입하면, ArgoCD 문제점처럼 보이는 현상이 사실은 프로세스 문제로 터집니다.

    2. GitOps와 ArgoCD를 아주 쉽게 설명해보면

    GitOps는 “실제 운영 상태는 Git에 적힌 선언대로 맞춘다”는 철학입니다. ArgoCD는 그 선언과 클러스터 상태를 계속 비교해서 drift(드리프트, 선언과 실제 상태가 어긋난 상태)를 감지하고 맞춰주는 역할을 하죠.

    예전 방식은 대체로 이랬습니다. CI 서버가 빌드하고, 누군가 kubectl(쿠버네티스 CLI)이나 Helm으로 운영 클러스터에 직접 배포합니다. 반면 GitOps 방식은 배포 변경도 Pull Request(PR, 변경 제안 요청)로 남고, 승인 후 Git이 바뀌면 ArgoCD가 그걸 따라갑니다. 감사 추적(audit trail, 변경 이력 추적)이 되는 대신, 우회 수정이 어려워집니다. 이 점이 장점이자 초반 저항 포인트입니다.

    항목 기존 CI/CD GitOps + ArgoCD
    배포 트리거 파이프라인 또는 운영자 수동 실행 Git 변경 반영
    변경 이력 파이프라인 로그 중심 Git 커밋과 PR 중심
    긴급 수정 클러스터에서 직접 수정 가능 직접 수정 시 drift 발생
    권한 통제 클러스터 권한 중심 저장소 권한과 승인 흐름 중요
    실패 원인 스크립트 불안정, 환경 차이 저장소 구조, 책임 분리 실패

    그래서 ArgoCD 도입 실패를 줄이려면 설치보다 운영 모델을 먼저 설계해야 합니다. 이걸 건너뛰면 나중에 진짜 많이 돌아갑니다. 저도 삽질 좀 했습니다 ㅎㅎ

    3. 실제로 많이 본 실패 사례 4가지

    3-1. 저장소 구조를 너무 늦게 정한 경우

    가장 흔한 실수입니다. 앱 소스와 배포 설정이 한 저장소에 섞여 있거나, 반대로 너무 잘게 쪼개져서 어디를 기준으로 봐야 하는지 모호한 경우죠. 처음엔 편해 보였는데, 팀이 커질수록 충돌이 심해집니다. 누가 이미지 태그를 관리하고, 누가 replica 수를 바꾸고, 누가 Ingress(인그레스, 외부 트래픽 진입점)를 수정하는지 경계가 안 잡히거든요.

    3-2. 자동 동기화만 켜고 승인 체계를 안 만든 경우

    auto-sync는 정말 편합니다. 드디어 됐다! 싶은 순간이 오거든요. 근데 여기서 바로 사고가 납니다. 운영 반영 전 검토가 필요한 팀인데도 자동 배포를 먼저 켜버리면, 잘못된 값 하나가 바로 프로덕션으로 갑니다. 도구는 빨라졌는데 프로세스는 그대로인 상태죠.

    3-3. Secret 관리 방식을 정하지 않은 경우

    GitOps 실수에서 빠지지 않는 항목입니다. 민감 정보(Secret)를 평문으로 저장하면 안 되고, 그렇다고 운영자가 클러스터에만 몰래 만들어두면 Git과 실제 상태가 분리됩니다. 저는 이 부분에서 팀마다 가장 오래 멈추는 걸 많이 봤습니다. Sealed Secrets(시일드 시크릿)나 External Secrets Operator 같은 접근을 검토하되, 핵심은 “Git에 무엇을 남기고 실제 값은 어디서 주입할지”를 팀 단위로 합의하는 겁니다.

    3-4. Drift를 장애로 볼지, 운영 유연성으로 볼지 합의가 없는 경우

    운영자가 급한 패치를 직접 넣는 문화가 남아 있으면 ArgoCD는 계속 OutOfSync(아웃오브싱크) 상태를 만들 겁니다. 이걸 경고로 볼지, 바로 되돌릴지, 특정 리소스는 예외로 둘지 정하지 않으면 경보만 쌓이고 신뢰가 깨집니다. 이 지점이 대표적인 ArgoCD 문제점처럼 보이는데, 사실은 정책 부재에 가깝습니다.

    4. 실전 구현: 실패 확률을 낮추는 최소 전환 절차

    제가 직접 해보니, 처음부터 전 서비스에 GitOps를 거는 것보다 작은 서비스 하나로 운영 규칙을 검증하는 게 훨씬 낫더라고요. 아래는 많이 무리하지 않는 최소 절차입니다.

    1. 배포 대상 네임스페이스(namespace) 하나를 파일럿으로 고릅니다.
    2. 애플리케이션 코드 저장소와 배포 매니페스트 저장소의 책임을 분리합니다.
    3. ArgoCD 프로젝트(AppProject)로 허용 대상 클러스터/네임스페이스를 제한합니다.
    4. 자동 동기화는 바로 켜지 말고, 먼저 수동 sync로 운영 흐름을 익힙니다.
    5. 변경 승인 기준을 PR 템플릿과 리뷰 규칙으로 문서화합니다.
    6. Drift 발생 시 대응 절차를 정합니다.

    예시로는 아래처럼 시작하면 무난합니다.

    kubectl create namespace argocd
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    kubectl get pods -n argocd

    설치 자체보다 더 중요한 건 프로젝트 경계를 먼저 두는 일입니다.

    apiVersion: argoproj.io/v1alpha1
    kind: AppProject
    metadata:
      name: sample-project
      namespace: argocd
    spec:
      description: sample project for controlled GitOps rollout
      sourceRepos:
        - 'https://github.com/example/platform-manifests.git'
      destinations:
        - namespace: sample-app
          server: 'https://kubernetes.default.svc'
      clusterResourceWhitelist:
        - group: '*'
          kind: '*'

    그 다음 애플리케이션은 이렇게 붙일 수 있습니다.

    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: sample-app
      namespace: argocd
    spec:
      project: sample-project
      source:
        repoURL: 'https://github.com/example/platform-manifests.git'
        targetRevision: main
        path: apps/sample-app/overlays/prod
      destination:
        server: 'https://kubernetes.default.svc'
        namespace: sample-app
      syncPolicy: {}

    여기서 일부러 자동 동기화 설정을 비워둔 것이 포인트입니다. 처음엔 수동 sync로 팀이 변경 흐름을 이해하게 만드는 게 좋습니다. 자동화는 익숙해진 뒤에 켜도 늦지 않습니다.

    ArgoCD 도입 실패를 줄이기 위한 AppProject와 저장소 구조 구성 이미지

    AppProject 경계, Application 연결, PR 승인 후 반영되는 흐름을 시각적으로 설명하는 이미지입니다.

    5. CI/CD 전환 사례에서 특히 조심해야 할 체크포인트

    기존 파이프라인에서 이미지 빌드까지는 잘 돌아가는데 GitOps로 넘기면서 꼬이는 경우가 많습니다. 보통 CI는 artifact(아티팩트, 배포 가능한 결과물)를 만들고, CD는 배포를 실행합니다. GitOps에선 이 경계가 바뀝니다. CI가 직접 배포하지 않고, 이미지 태그나 차트 값을 Git에 반영하는 식으로 역할이 이동하죠.

    예를 들면 이런 방식입니다.

    # CI job example
    export IMAGE_TAG=${GIT_COMMIT_SHA}
    sed -i "s/tag: .*/tag: ${IMAGE_TAG}/" apps/sample-app/overlays/prod/values.yaml
    git add apps/sample-app/overlays/prod/values.yaml
    git commit -m "chore: deploy sample-app ${IMAGE_TAG}"
    git push origin main

    이 방식은 단순하지만, 바로 main 브랜치에 반영하면 위험합니다. 제가 추천하는 건 별도 배포 브랜치나 PR 자동 생성 방식입니다. 사람이 마지막으로 한 번 더 보고 머지하는 흐름이 초반 안정화에 꽤 도움이 됩니다.

    • 프로덕션 반영은 PR 승인 후에만 가능하게 설정
    • 리뷰어를 플랫폼 담당자와 서비스 담당자로 분리
    • 롤백 기준을 Git revert 중심으로 문서화
    • 수동 kubectl 적용은 예외 상황에서만 허용

    이런 기본선만 있어도 CI/CD 전환 사례에서 실패 확률이 꽤 내려갑니다.

    6. ⚠️ 트러블슈팅: 제가 실제로 많이 본 문제와 해결법

    여기서부터는 정말 많이 부딪히는 부분들입니다. 처음엔 저도 “왜 sync는 성공인데 앱은 안 뜨지?” 같은 상황을 자주 만났거든요.

    증상 원인 해결 방향
    OutOfSync가 계속 발생 운영자가 클러스터에서 직접 수정 직접 수정 금지 원칙 정리, 예외 리소스만 ignore 설정 검토
    Sync 성공인데 서비스 장애 매니페스트 문법은 맞지만 런타임 의존성 누락 readinessProbe, Secret 참조, ConfigMap 값 검증 강화
    배포가 너무 자주 일어남 이미지 태그나 공통 값 파일이 과도하게 변경됨 환경별 경로 분리, 변경 범위 최소화
    리뷰 병목 발생 모든 변경이 한 저장소에 몰림 팀 단위 경계 재설계, CODEOWNERS 활용
    롤백이 헷갈림 Git revert와 수동 핫픽스가 섞임 롤백 절차를 Git 기준으로 단일화

    6-1. ignoreDifferences 남용

    ArgoCD에는 특정 필드 차이를 무시하는 기능이 있습니다. 편하긴 한데, 이걸 남용하면 drift 감지 의미가 사라집니다. 정말 컨트롤러가 자동으로 바꾸는 필드처럼 불가피한 경우에만 제한적으로 쓰는 게 맞습니다.

    6-2. Helm 값 파일 난립

    환경이 늘수록 values 파일이 너무 많아집니다. dev, stage, prod는 그렇다 쳐도 팀별 패치, 긴급 패치, 지역별 패치가 늘어나면 나중에 아무도 구조를 설명 못 합니다. 저도 홈랩에서 비슷하게 풀었다가, 결국 Kustomize(커스터마이즈, 오버레이 기반 설정 관리)와 역할을 분리하면서 정리했었습니다.

    6-3. 알림 없이 운영

    Sync 실패나 Health degraded(헬스 저하) 이벤트를 아무도 못 받으면, ArgoCD UI를 매번 열어보기 전까지 장애를 놓치게 됩니다. 알림 체계는 초반부터 붙이는 게 좋습니다. 최소한 운영 채널로 실패 이벤트는 전달되게 구성해두세요.

    7. 검증: 전환이 잘 되고 있는지 어떻게 확인할까

    성공 기준도 미리 정의해야 합니다. 그냥 “ArgoCD가 떴다”로 끝내면 안 됩니다. 저는 보통 아래 항목으로 봅니다.

    1. 배포 변경이 모두 PR과 커밋으로 추적되는가
    2. 직접 클러스터 수정 비율이 줄고 있는가
    3. 장애 시 마지막 변경점 확인 시간이 짧아졌는가
    4. 롤백 절차가 Git revert 기준으로 일관되게 동작하는가
    5. 서비스별 소유권(owner)이 저장소 구조와 일치하는가

    ArgoCD UI에서 Health, Sync 상태를 보는 것도 좋지만, 그것만으론 부족합니다. 진짜 중요한 건 운영팀과 개발팀이 같은 변경 이력을 보고 같은 언어로 이야기하게 됐는지입니다. 이게 되면 도입이 절반은 성공한 겁니다. 실제로 써보니까 배포 속도보다 커뮤니케이션 비용이 더 크게 줄더라고요.

    ArgoCD 도입 실패 검증을 위한 Sync와 Health 상태 대시보드 이미지

    배포 상태 확인, 이상 징후 탐지, 롤백 판단 포인트를 한눈에 보여주는 결과 검증 이미지입니다.

    8. 정리 FAQ: 많이 받는 질문

    Q1. ArgoCD는 무조건 자동 동기화로 써야 하나요?

    아닙니다. 초반에는 수동 sync가 오히려 안전합니다. 팀이 GitOps 흐름에 익숙해지고 승인 절차가 자리 잡으면 그때 auto-sync를 검토해도 됩니다.

    Q2. 기존 CI/CD를 전부 버려야 하나요?

    그건 아닙니다. 빌드와 테스트는 CI가 계속 담당하고, 배포 실행만 Git 기반으로 넘기는 식이 일반적입니다.

    Q3. ArgoCD 문제점은 결국 도구 한계 아닌가요?

    일부는 맞지만, 현장에서 보이는 대부분은 정책과 구조 문제였습니다. 특히 저장소 책임 분리와 승인 체계가 약하면 같은 문제가 반복됩니다.

    Q4. 작은 팀도 GitOps가 필요할까요?

    작은 팀도 필요할 수 있습니다. 다만 처음부터 무겁게 가지 말고, 단일 서비스와 단순한 저장소 구조로 시작하는 편이 좋습니다.

    ArgoCD 도입 실패 방지 전략과 GitOps 실수 비교 요약 이미지

    도입 전후 비교, 실패 패턴, 예방 전략을 요약한 인포그래픽 형태의 정리 이미지입니다.

    9. 마무리: ArgoCD 도입 실패를 줄이는 진짜 핵심

    ArgoCD 도입 실패는 대개 설치 실패가 아니라 운영 모델 설계 실패입니다. GitOps는 도구를 붙이는 프로젝트가 아니라, 변경을 기록하고 승인하고 되돌리는 방식을 다시 정의하는 작업이거든요. 저도 처음엔 UI가 예쁘고 sync가 자동으로 돌아가니까 금방 안정화될 줄 알았는데, 실제론 저장소 구조와 팀 규칙을 먼저 잡아야 효과가 났습니다.

    정리하면 이렇습니다. GitOps 실수를 줄이려면 작은 범위로 시작하고, 수동 sync로 흐름을 익히고, 저장소 책임과 승인 규칙을 먼저 고정해야 합니다. 그리고 drift, Secret, 롤백 기준을 문서로 남겨야 합니다. 이 4가지만 해도 현장에서 체감 차이가 큽니다.

    다음 글에서는 App of Apps 패턴과 멀티 클러스터 운영에서 어디까지 표준화해야 하는지 다뤄볼 예정입니다. 이전 글에서 다뤘던 Kubernetes 배포 기본기와 함께 보시면 흐름이 더 잘 잡히실 겁니다. 혹시 지금 ArgoCD 도입 실패를 겪고 계시다면, 설치 로그보다 먼저 운영 규칙부터 점검해보세요. 그게 생각보다 훨씬 빠른 지름길입니다. 🎉

  • [CI/CD] Kubernetes 환경 Jenkins: 1년 운영하며 겪은 성능 최적화와 안정화 사례

    [CI/CD] Kubernetes 환경 Jenkins: 1년 운영하며 겪은 성능 최적화와 안정화 사례

    [CI/CD] Kubernetes 환경 Jenkins: 1년 운영하며 겪은 성능 최적화와 안정화 사례

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘은 Kubernetes(쿠버네티스) 환경에서 Jenkins(젠킨스)를 1년 넘게 운영하며 겪었던 삽질과 그 과정에서 얻은 성능 최적화, 안정화 노하우를 풀어볼까 합니다. 많은 분들이 CI/CD(지속적 통합/지속적 배포) 파이프라인 구축에 Jenkins를 사용하고 계실 텐데, 특히 Kubernetes 위에서 Jenkins를 돌리면서 “이게 맞나?” 싶었던 경험들 있으실 거예요. 제가 딱 그랬거든요. 😅

    처음엔 마냥 좋다고 생각했던 Jenkins on Kubernetes가 생각보다 많은 운영 난이도를 요구했습니다. 툭하면 죽는 빌드 에이전트, 느려터진 빌드 시간, 예상치 못한 마스터 다운까지… 정말이지 밤낮없이 씨름했던 기억이 생생합니다. 이 글에서는 제가 직접 부딪히며 해결했던 문제들과 그 과정을 여러분들께 멘토처럼 상세히 알려드릴게요. 혹시 비슷한 고민을 하고 계셨다면, 제 경험이 조금이나마 도움이 되기를 바랍니다! 🙏

    Kubernetes 환경 Jenkins 아키텍처 개요: 마스터와 동적 에이전트 파드들의 상호작용

    Jenkins on Kubernetes, 왜 선택했을까요? (개념 설명)

    Jenkins는 워낙 유명한 CI/CD 자동화 도구죠. 프로젝트 빌드, 테스트, 배포 등 개발 프로세스의 여러 단계를 자동화해주는 역할을 합니다. 그런데 이걸 왜 굳이 Kubernetes 위에서 돌리냐고요? 간단히 말해서, 유연성과 확장성 때문입니다.

    • 동적 에이전트 프로비저닝 (Dynamic Agent Provisioning): Kubernetes의 가장 큰 장점 중 하나인데요, 빌드가 필요할 때만 Jenkins Agent(젠킨스 에이전트) Pod(파드)를 생성하고, 빌드가 끝나면 자동으로 제거할 수 있습니다. 덕분에 리소스를 효율적으로 사용할 수 있고, 동시에 여러 빌드를 처리할 때도 필요한 만큼 에이전트를 늘릴 수 있죠.
    • 높은 가용성 (High Availability): Kubernetes는 컨테이너화된 애플리케이션의 고가용성을 보장합니다. Jenkins Master(젠킨스 마스터) Pod가 문제가 생겨도 Kubernetes가 자동으로 다른 노드에 재시작해주니, 서비스 중단 시간을 최소화할 수 있습니다.
    • 환경 일관성 (Environment Consistency): 모든 빌드가 컨테이너 안에서 이뤄지므로, 개발 환경과 동일한 환경에서 빌드 및 테스트를 진행할 수 있어서 “내 로컬에서는 되는데 서버에서는 안 돼요!” 하는 문제를 줄일 수 있습니다.

    이런 장점들 덕분에 저도 Kubernetes 환경으로 Jenkins를 옮기기로 결정했었죠. 하지만 현실은 녹록지 않았습니다. 😅

    실전 구현: Jenkins Kubernetes 플러그인과 Pod Template

    Kubernetes에서 Jenkins를 운영하려면 Jenkins Kubernetes Plugin(젠킨스 쿠버네티스 플러그인)이 필수인데요. 이 플러그인이 Jenkins Master와 Kubernetes 클러스터 간의 통신을 담당하면서 동적으로 에이전트 Pod를 생성하고 관리하는 핵심 역할을 수행합니다. 플러그인 설치 후, 가장 중요한 설정은 Pod Template(파드 템플릿)입니다.

    Pod Template은 Jenkins Agent Pod가 어떤 이미지로, 어떤 리소스를 가지고 생성될지 정의하는 YAML 설정이거든요. 처음에는 기본 설정으로 시작했지만, 곧바로 성능 병목에 부딪혔습니다. 다음은 제가 주로 사용했던 Pod Template의 핵심 부분입니다.

    apiVersion: v1
    kind: Pod
    spec:
      containers:
      - name: jnlp
        image: jenkins/inbound-agent:4.11.2-1-jdk11
        resources:
          requests:
            cpu: "500m"
            memory: "1Gi"
          limits:
            cpu: "1"
            memory: "2Gi"
        # ... (생략) ...
      - name: build-tools
        image: my-private-registry/custom-build-image:latest
        command: ["cat"]
        tty: true
        resources:
          requests:
            cpu: "1"
            memory: "2Gi"
          limits:
            cpu: "2"
            memory: "4Gi"
        # ... (생략) ...
      volumes:
      - name: jenkins-workspace
        emptyDir: {}
      # ... (생략) ...
    

    여기서 `jnlp` 컨테이너는 Jenkins와 통신하는 기본 에이전트고, `build-tools`는 실제 빌드 도구들이 들어간 커스텀 이미지더라고요. 여기서 중요한 부분은 바로 resources 섹션입니다. 처음에는 이 부분을 대충 설정했다가 빌드 에이전트들이 자꾸 죽는 현상을 겪었어요. ⚠️

    Jenkins UI Kubernetes 플러그인 설정 화면: Pod Template 구성 예시

    Jenkins UI 내 Kubernetes 플러그인 설정 화면: Pod Template 구성 예시

    ⚠️ 1년 운영하며 겪은 삽질과 성능 최적화/안정화 사례

    자, 이제 본론입니다. 1년 동안 겪었던 문제들과 그 해결책들을 공유해 드릴게요.

    1. 빌드 에이전트 OOMKilled (메모리 부족) 현상

    가장 흔하게 겪었던 문제입니다. 빌드 도중에 에이전트 Pod가 갑자기 OOMKilled(Out Of Memory Killed, 메모리 부족으로 종료됨) 상태가 되면서 빌드가 실패하는 거죠. 처음엔 원인을 몰라 헤맸는데, 알고 보니 Pod Template의 resources.limits.memory 설정이 너무 낮았던 것이었습니다.

    • 문제점: 빌드 프로세스가 예상보다 많은 메모리를 사용하면서 Kubernetes가 Pod를 강제로 종료시킴.
    • 해결책: resources.requests와 resources.limits를 현실적으로 설정하는 것이 중요합니다. requests는 Pod가 스케줄링될 때 필요한 최소한의 리소스, limits는 Pod가 최대로 사용할 수 있는 리소스예요. 처음에 빌드를 여러 번 돌려보면서 실제로 필요한 메모리 양을 측정했습니다. 빌드 과정에서 peak 메모리 사용량을 모니터링해서 넉넉하게 잡아주는 게 중요하더라고요.
        resources:
          requests:
            cpu: "1"
            memory: "2Gi" # 최소 2GB 요청
          limits:
            cpu: "2"
            memory: "4Gi" # 최대 4GB까지 사용 허용
    

    💡 팁: requests는 Kubernetes 스케줄러가 노드를 선택하는 기준이 되고, limits는 CGroup(컨트롤 그룹)에 의해 Pod의 최대 리소스 사용량을 제한합니다. 너무 타이트하면 OOMKilled 되고, 너무 넉넉하면 리소스 낭비가 될 수 있으니 적절한 튜닝이 필요합니다.

    2. 느린 이미지 풀링 (Image Pull) 시간

    동적 에이전트의 가장 큰 장점 중 하나지만, 매번 빌드 에이전트 Pod가 생성될 때마다 필요한 Docker Image(도커 이미지)를 다운로드(Pull)해야 한다는 단점도 있습니다. 특히 빌드 에이전트 이미지가 크거나, 여러 개의 이미지를 사용하는 경우 빌드 시작 시간이 너무 길어지는 문제가 발생했습니다.

    • 문제점: 빌드 시작 시 Docker Image Pull 시간 때문에 전체 빌드 시간이 길어짐.
    • 해결책:
      1. 로컬 Docker Registry(도커 레지스트리) 사용: 사설 Docker Registry를 클러스터 내부에 구축하고, 이미지를 미리 이곳에 캐싱해두면 Pull 속도가 훨씬 빨라집니다.
      2. Node에 Image Pre-pulling: Kubernetes Worker Node(워커 노드)에 DaemonSet(데몬셋)을 이용해서 자주 사용하는 에이전트 이미지를 미리 Pull 해두는 방법도 효과적이었습니다. 이렇게 하면 Pod가 스케줄링될 때 이미지가 이미 로컬에 있어 바로 실행될 수 있죠.
      3. Jenkins Agent 이미지 최적화: 빌드에 필요한 최소한의 도구만 포함된 경량 이미지를 만들었습니다. 불필요한 레이어를 줄이고, 베이스 이미지를 최신으로 유지하는 것도 중요하더라고요.

    3. Jenkins Master의 안정성 확보

    아무리 에이전트가 잘 돌아가도 Master가 불안정하면 모든 CI/CD 파이프라인이 멈춥니다. Master Pod의 잦은 재시작이나 데이터 손실은 정말 끔찍하죠.

    • 문제점: Master Pod의 리소스 부족, 데이터 손실 위험.
    • 해결책:
      1. Persistent Volume Claim (PVC) 사용: Jenkins Master의 /var/jenkins_home 디렉터리는 빌드 설정, 플러그인, 빌드 이력 등 중요한 데이터가 저장되는 곳이거든요. 반드시 Persistent Volume Claim(PVC, 영구 볼륨 클레임)을 사용하여 데이터를 영구적으로 저장해야 합니다. 저는 NFS(네트워크 파일 시스템) 기반의 PV(영구 볼륨)를 사용했는데, 클라우드 환경에서는 EBS, Azure Disk, GCE Persistent Disk 등을 활용할 수 있습니다.
      2. Master Pod 리소스 충분히 할당: Master Pod도 적절한 CPU와 Memory를 할당해야 합니다. 너무 적으면 UI가 느려지거나, 플러그인 로딩 중 OOMKilled 될 수 있습니다.
      3. Configuration as Code (CasC) 도입: Jenkins 설정을 YAML 파일로 관리하는 CasC(Configuration as Code)는 Master의 안정성을 높이는 데 크게 기여했습니다. 설정을 Git에 버전 관리하고, Jenkins 재시작 시 자동으로 적용되도록 하여 휴먼 에러를 줄이고 재현 가능한 환경을 구축할 수 있었습니다.
    최적화 전후 Jenkins 빌드 시간 및 Kubernetes 리소스 사용량 비교 Grafana 대시보드

    최적화 후 빌드 시간 단축 및 리소스 사용량 안정화 결과

    4. 네트워크 성능 문제 (대용량 아티팩트 전송)

    빌드 결과물(Artifacts, 아티팩트)이 크거나, 빌드 과정에서 외부 리소스(Maven Repository 등)를 자주 다운로드받는 경우 네트워크 병목이 발생할 수 있습니다.

    • 문제점: 대용량 아티팩트 전송 및 외부 리소스 다운로드로 인한 빌드 지연.
    • 해결책:
      1. 클러스터 내부 캐싱 프록시: Maven, npm 등의 패키지 매니저를 위한 캐싱 프록시(예: Sonatype Nexus, Artifactory)를 클러스터 내부에 구축하여 외부 네트워크 트래픽을 줄였습니다.
      2. 빠른 스토리지 사용: PVC에 사용하는 스토리지가 느리면 빌드 과정에서 파일 I/O(입출력) 성능 저하가 발생합니다. 가능한 한 SSD 기반의 고성능 스토리지를 사용하는 것이 좋습니다.
      3. 불필요한 아티팩트 최소화: 빌드 후 Jenkins에 저장하거나 전송하는 아티팩트의 크기를 최소화하도록 파이프라인을 최적화했습니다.

    검증 및 결과: 드디어 안정화! 🎉

    위에 언급된 최적화들을 적용하고 나니, 거짓말처럼 Jenkins 환경이 안정화되기 시작했습니다. 빌드 실패율은 현저히 줄었고, 빌드 시간도 평균 30% 이상 단축되는 성과를 얻을 수 있었습니다. 특히 동적 에이전트의 효율적인 리소스 사용 덕분에 클러스터 비용도 절감할 수 있었죠.

    • 빌드 성공률: 60% → 95% 이상으로 향상
    • 평균 빌드 시간: 5분 → 3분 내외로 단축 (프로젝트별 상이)
    • 리소스 사용 효율: 유휴 에이전트 감소로 클러스터 리소스 활용률 증가

    이러한 개선 사항들은 Prometheus(프로메테우스)와 Grafana(그라파나)를 통해 모니터링하면서 실시간으로 확인했습니다. 특히 빌드 성공/실패율, 큐(Queue)에 대기 중인 빌드 수, 에이전트 Pod의 리소스 사용량 등을 꾸준히 트래킹하면서 추가적인 개선점을 찾아나갔습니다. 📈

    Kubernetes Jenkins 운영 최적화 주요 포인트 요약 인포그래픽

    Kubernetes Jenkins 운영 최적화 주요 포인트 요약 인포그래픽

    마무리: 멘토로서의 조언과 다음 단계

    Kubernetes 환경에서 Jenkins를 운영하는 것은 분명 도전적인 일입니다. 처음에는 “이거 왜 이렇게 어렵지?” 싶다가도, 하나하나 문제를 해결해나가면서 시스템이 안정화되는 모습을 보면 정말 뿌듯하더라고요. 제가 13년 동안 인프라 엔지니어로 일하면서 느낀 건, 삽질은 결국 성장의 밑거름이 된다는 겁니다.

    이 글에서 다룬 내용들이 여러분의 Kubernetes Jenkins 운영에 조금이나마 도움이 되었으면 좋겠습니다. 혹시 더 깊은 질문이나 다른 경험담이 있다면 언제든지 댓글로 남겨주세요! 저도 처음엔 헷갈렸던 부분이 많았거든요.

    다음 단계로는 GitOps(깃옵스) 철학을 기반으로 Argo CD(아르고 CD) 같은 도구를 Jenkins와 연동하여 배포 파이프라인을 더욱 자동화하고 고도화하는 방안을 고민하고 있습니다. CI/CD 여정은 끝이 없는 것 같아요. 계속해서 배우고 실험하며 더 나은 방법을 찾아나가야겠죠? 😊

  • [k8s] Argo CD 멀티 클러스터 동기화 문제 해결: 실제 운영 사례와 디버깅 팁

    [k8s] Argo CD 멀티 클러스터 동기화 문제 해결: 실제 운영 사례와 디버깅 팁

    Argo CD 멀티 클러스터 동기화 문제 해결: 실제 운영 사례와 디버깅 팁

    안녕하세요, 13년차 서버실 지킴이입니다. 요즘 쿠버네티스(Kubernetes) 환경에서 GitOps(깃옵스)를 도입하는 기업들이 정말 많죠? 저도 홈랩과 회사 프로젝트에서 Argo CD(아르고 CD)를 활용해서 여러 쿠버네티스 클러스터를 관리하고 있는데요. 처음엔 “와, 이거 정말 편하겠다!” 싶었는데, 막상 실제 운영 환경에서 멀티 클러스터(Multi-Cluster) 동기화 문제를 만나면 머리가 지끈거릴 때가 많더라고요.

    특히 여러 클러스터에 동일한 애플리케이션을 배포하거나, 환경별로 미묘하게 다른 설정을 적용해야 할 때 ApplicationSet(애플리케이션셋)을 쓰면 정말 유용하거든요. 그런데 간혹 특정 클러스터만 동기화가 안 되거나, 알 수 없는 에러로 삽질을 거듭하곤 해요. 제가 직접 겪었던 Argo CD 멀티 클러스터 동기화 문제들과 그 해결 과정을 솔직하게 공유해볼까 해요. 혹시 비슷한 경험으로 고생하고 계시다면, 이 글이 작은 힌트라도 되었으면 좋겠네요! 💡

    Argo CD 멀티 클러스터 관리 아키텍처: Git을 중심으로 Argo CD가 여러 쿠버네티스 클러스터에 애플리케이션을 배포하고 동기화하는 흐름을 보여줍니다.

    Argo CD, GitOps, 그리고 멀티 클러스터, 이게 뭔가요?

    본격적인 문제 해결에 앞서, 핵심 개념들을 간단히 짚고 넘어갈게요. “아, 이건 다 아는데!” 하시는 분들도 계시겠지만, 혹시 처음 접하시는 분들을 위해 쉽게 풀어 설명해 드릴게요.

    • Argo CD (아르고 CD): 쿠버네티스 환경에서 GitOps를 구현하기 위한 대표적인 선언적(Declarative) CD(Continuous Delivery, 지속적 배포) 툴입니다. Git 저장소를 애플리케이션의 ‘원본 소스(Single Source of Truth)’로 삼아, 쿠버네티스 클러스터의 실제 상태가 Git에 정의된 상태와 일치하도록 지속적으로 동기화(Synchronization)를 시도하죠. 즉, Git에 코드를 푸시하면 Argo CD가 알아서 클러스터에 배포해주는 방식입니다.

    • GitOps (깃옵스): Git을 운영의 중심(Operational Hub)으로 삼아 인프라와 애플리케이션의 모든 상태를 관리하는 방식입니다. 모든 변경 사항이 Git에 기록되므로, 누가 언제 무엇을 변경했는지 추적하기 쉽고, 문제가 생겼을 때 쉽게 롤백(Rollback)할 수 있다는 장점이 있습니다. “Git에서 정의한 대로 클러스터 상태를 유지한다”는 철학이죠.

    • 멀티 클러스터 (Multi-Cluster): 여러 개의 쿠버네티스 클러스터를 동시에 관리하는 환경을 의미합니다. 개발, 스테이징, 프로덕션 환경을 분리하거나, 재해 복구(Disaster Recovery)를 위해 여러 리전에 클러스터를 운영할 때 등 다양한 이유로 멀티 클러스터 환경을 구축합니다. Argo CD는 하나의 중앙 집중식(Centralized) 컨트롤 플레인(Control Plane)으로 여러 원격 클러스터(Remote Cluster)를 관리할 수 있게 해줍니다.

    Argo CD 멀티 클러스터 설정과 흔한 문제 시나리오

    Argo CD를 이용해 멀티 클러스터를 관리하려면, 먼저 Argo CD 컨트롤 플레인이 설치된 클러스터(보통 ‘관리 클러스터’)에 다른 클러스터들을 등록해야 합니다. argocd cluster add 명령어가 이걸 해주죠. 이때 Argo CD가 원격 클러스터에 접근할 수 있는 ServiceAccount와 ClusterRoleBinding을 생성해줍니다.

    그리고 여러 클러스터에 애플리케이션을 배포할 때는 주로 ApplicationSet을 사용합니다. 예를 들어, 다음과 같은 ApplicationSet으로 여러 클러스터에 nginx-app을 배포한다고 가정해봅시다.

    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: my-nginx-appset
    spec:
      generators:
      - clusters: {}
      template:
        metadata:
          name: '{{name}}-nginx-app'
        spec:
          project: default
          source:
            repoURL: https://github.com/my-org/my-gitops-repo.git
            targetRevision: HEAD
            path: apps/nginx
          destination:
            server: '{{server}}'
            namespace: default
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
    

    이렇게 설정하면 Argo CD가 등록된 모든 클러스터에 apps/nginx 경로의 매니페스트를 배포하려고 시도합니다. 그런데 여기서 문제가 발생하곤 해요. 어떤 클러스터는 잘 동기화(SYNCED)되는데, 다른 클러스터는 계속 OutOfSync 상태이거나 Failed 상태를 벗어나지 못하는 거죠. “아니, 분명 다 똑같이 설정했는데 왜 이럴까?” 하는 생각이 절로 듭니다. 저도 처음엔 정말 막막했는데, 몇 가지 공통적인 원인이 있더라고요. 😅

    Argo CD UI의 ApplicationSet 대시보드: 문제 발생 상황

    Argo CD UI의 ApplicationSet 대시보드: 여러 클러스터에 배포된 애플리케이션들의 동기화 상태를 한눈에 볼 수 있으며, 문제가 발생한 애플리케이션을 쉽게 식별할 수 있습니다.

    ⚠️ 실제 운영 사례로 본 Argo CD 동기화 문제와 디버깅 팁

    제가 13년 동안 쌓은 삽질 경험을 바탕으로, Argo CD 멀티 클러스터 환경에서 자주 발생하는 동기화 문제와 그 해결책을 알려드릴게요. 하나씩 체크하다 보면 분명 원인을 찾을 수 있을 겁니다!

    1. RBAC (Role-Based Access Control) 권한 문제
      가장 흔하면서도 놓치기 쉬운 문제예요. Argo CD 컨트롤 플레인이 관리 대상 클러스터에 배포할 권한이 없는 경우입니다. argocd cluster add 명령어가 자동으로 필요한 권한을 생성해주지만, 간혹 수동으로 클러스터를 추가했거나, 보안 정책상 특정 권한이 누락된 경우가 있거든요.

      💡 디버깅 팁:

      • 관리 대상 클러스터에서 kubectl get serviceaccount argocd-manager -n argocd 명령어로 argocd-manager ServiceAccount가 잘 생성되었는지 확인하세요.
      • 이 ServiceAccount에 연결된 ClusterRoleBinding과 ClusterRole의 권한을 확인해보세요. 보통 cluster-admin 권한이 부여되는데, 만약 특정 리소스에 대한 권한만 있다면 해당 리소스 배포가 실패할 수 있거든요. kubectl describe clusterrole argocd-manager-role 명령으로 권한 목록을 볼 수 있습니다.
      • 특히 ServiceAccount가 아닌 다른 방식으로 인증(예: kubeconfig 파일)을 사용한다면, 해당 kubeconfig 파일에 정의된 사용자에게 충분한 권한이 있는지 확인해야 합니다.
    2. 네트워크 연결 문제
      Argo CD 서버가 관리 대상 클러스터의 쿠버네티스 API 서버에 접근할 수 없는 경우네요. 방화벽, VPN, VPC 피어링 등 네트워크 구성을 꼼꼼히 확인해야 합니다. “어제까진 잘 됐는데?” 하다가 네트워크 변경 때문에 막히는 경우가 꽤 있거든요.

      💡 디버깅 팁:

      • Argo CD UI에서 해당 클러스터의 상태가 Unavailable인지 확인하세요.
      • Argo CD 컨트롤러 파드(Pod)에서 관리 대상 클러스터의 API 서버로 curl 같은 명령어로 접속을 시도해보세요. 파드에 접속하는 방법은 kubectl exec -it <argocd-repo-server-pod> -n argocd -- bash 후 curl -k https://<cluster-api-server-ip>:<port>/healthz 와 같이 시도해볼 수 있습니다.
      • argocd cluster add 시 입력한 API 서버 주소가 올바른지 다시 한번 확인해보세요. 내부망 주소여야 할 때 외부망 주소를 입력하거나, 그 반대인 경우도 있거든요.
    3. ApplicationSet 설정 오류
      ApplicationSet의 generator나 template 설정이 잘못된 경우예요. 특히 cluster 필터링이나 Git 저장소 경로(path)를 잘못 지정했을 때 문제가 발생하곤 합니다.

      💡 디버깅 팁:

      • argocd appset get <application-set-name> 명령으로 ApplicationSet의 현재 상태와 생성된 Application 목록을 확인해보세요.
      • ApplicationSet의 template 섹션에서 source.path가 Git 저장소의 실제 경로와 일치하는지 확인하세요. 대소문자나 오타가 있을 수 있거든요.
      • generator에서 특정 클러스터만 선택하도록 설정했다면, 해당 클러스터의 라벨(labels)이 selector와 정확히 일치하는지 확인해보세요.
    4. 배포하려는 리소스 매니페스트 오류
      Git 저장소에 있는 쿠버네티스 매니페스트(YAML 파일) 자체에 문제가 있는 경우네요. 문법 오류, 잘못된 필드 이름, 존재하지 않는 StorageClass 참조 등이 있을 수 있거든요.

      💡 디버깅 팁:

      • Argo CD UI에서 Application 상태를 확인하고, Events(이벤트) 탭이나 Logs(로그) 탭을 자세히 살펴보세요. 어떤 리소스에서 어떤 에러가 발생했는지 상세하게 나옵니다. 예를 들어, "admission webhook denied the request" 같은 메시지는 Admission Controller(어드미션 컨트롤러)나 OPA Gatekeeper 같은 정책 엔진에 의해 배포가 거부되었음을 의미할 수 있어요.
      • 해당 클러스터에 직접 kubectl apply -f <problematic-manifest.yaml> --dry-run=client 로 배포를 시도하여 문법 오류 등을 미리 확인해보세요.
      • kubectl describe <resource-type> <resource-name> -n <namespace> 명령으로 대상 클러스터에 배포된(혹은 배포 실패한) 리소스의 상세 상태를 확인하세요.
    5. Argo CD 컨트롤러 파드 문제
      아주 드물지만, Argo CD 자체의 argocd-application-controller나 argocd-repo-server 파드에 문제가 생겨 동기화가 제대로 작동하지 않을 수 있어요. 리소스 부족, 네트워크 문제, 버그 등이 원인일 수 있거든요.

      💡 디버깅 팁:

      • kubectl get pods -n argocd 명령으로 Argo CD 관련 파드들이 모두 Running 상태인지 확인해보세요.
      • 문제 있어 보이는 파드의 로그를 확인해보세요: kubectl logs -f <argocd-pod-name> -n argocd. 특히 argocd-application-controller 로그에 동기화 관련 오류 메시지가 나타날 수 있거든요.
      • 파드를 재시작하거나, 리소스(CPU, Memory)를 늘려주는 것도 방법이 될 수 있습니다.

    ✅ 문제 해결 후 검증하기

    위의 팁들을 바탕으로 문제를 해결했다면, 이제 제대로 동기화가 되는지 확인해야겠죠? 드디어 SYNCED 상태를 봤을 때의 그 쾌감이란! 🎉

    1. Argo CD UI/CLI 확인
      가장 먼저 Argo CD UI에 접속해서 해당 Application 또는 ApplicationSet의 상태가 Synced 그리고 Healthy로 표시되는지 확인합니다. CLI를 선호한다면 argocd app list 나 argocd app get <application-name> 명령어를 사용해보세요.

    2. 대상 클러스터 직접 확인
      Argo CD UI/CLI에서 Synced로 표시되더라도, 혹시 모르니 해당 클러스터에 접속해서 kubectl get <resource-type> -n <namespace> 명령으로 실제로 리소스가 배포되었는지, 상태는 어떤지 직접 확인하는 게 좋습니다. 특히 Deployment나 StatefulSet의 파드들이 정상적으로 Running 상태인지 확인해 주세요.

    3. 새로운 변경사항 푸시 테스트
      Git 저장소에 간단한 변경사항(예: ConfigMap 내용 변경)을 푸시해서 Argo CD가 변경을 감지하고 자동으로 동기화하는지 확인해봅니다. 이 과정이 원활하게 진행된다면 성공적으로 문제를 해결한 거예요!

    Argo CD UI의 성공적인 동기화 대시보드

    성공적인 Argo CD 동기화 대시보드: 모든 애플리케이션이 문제없이 배포되고 동기화되어 안정적인 운영 상태를 보여줍니다.

    마무리하며: 삽질은 경험이 되고, 경험은 자산이 됩니다.

    오늘은 Argo CD 멀티 클러스터 동기화 문제 해결에 대한 저의 경험과 디버깅 팁을 공유해드렸습니다. 솔직히 저도 처음엔 정말 막막해서 밤늦게까지 삽질 좀 했거든요. “왜 안 되는 거지?” 하면서 클러스터 여기저기를 들쑤시고 다녔던 기억이 생생해요. 😂

    하지만 이런 삽질 과정이 결국 저의 노하우가 되고, 여러분 같은 동료 인프라 엔지니어분들께 도움이 되는 자산이 된다는 사실을 깨달았습니다. Argo CD는 정말 강력한 툴이지만, 그만큼 복잡한 환경에서는 예상치 못한 문제들이 발생할 수 있어요. 중요한 건 문제를 만났을 때 당황하지 않고, 차근차근 원인을 찾아 해결해나가는 자세라고 생각합니다.

    이번 글이 Argo CD 멀티 클러스터 환경에서 고군분투하는 분들께 조금이나마 도움이 되었기를 바랍니다. 다음 글에서는 Argo CD의 Progressive Delivery(점진적 배포) 전략인 Argo Rollouts에 대해 다뤄볼게요. 그때까지 GitOps 즐겁게 운영하시길 바랍니다! 궁금한 점이 있다면 언제든 댓글 남겨주세요! 😊

    Argo CD 멀티 클러스터 동기화 문제 해결 체크리스트: RBAC, 네트워크, ApplicationSet 설정, 매니페스트 오류, Argo CD 파드 상태 등 주요 점검 사항을 요약한 인포그래픽입니다.

  • [k8s] Kubernetes 스토리지: Longhorn vs Rook-Ceph 성능 벤치마크

    [k8s] Kubernetes 스토리지: Longhorn vs Rook-Ceph 성능 벤치마크

    안녕하세요, 13년차 서버실 지킴이입니다. 🤓

    Kubernetes(쿠버네티스) 환경에서 애플리케이션을 운영하다 보면, 영구 스토리지(Persistent Storage)의 중요성을 정말 뼈저리게 느끼게 됩니다. Pod(파드)가 재시작되거나 다른 노드로 옮겨가도 데이터는 안전하게 보존되어야 하니까요. 특히 저처럼 홈랩(Homelab)에서 이것저것 실험하며 삽질을 즐기는 사람이라면, 안정적이고 성능 좋은 스토리지를 찾는 데 꽤 많은 시간을 투자하게 되죠. 저도 그랬거든요! 🤔

    오늘은 제가 직접 홈랩에서 Longhorn(롱혼)과 Rook-Ceph(룩-세프) 두 가지 Kubernetes 영구 스토리지 솔루션을 설치하고, FIO(Flexible I/O Tester) 벤치마크 툴로 성능을 비교해본 경험을 공유해드리려고 합니다. 과연 어떤 녀석이 제 환경에 더 적합했을까요? 저의 삽질 여정을 따라오시면서 여러분의 Kubernetes 스토리지 선택에 도움이 되시길 바랍니다!

    왜 영구 스토리지가 필요할까요?

    Kubernetes는 기본적으로 컨테이너화된 애플리케이션을 배포하고 관리하는 데 최적화되어 있어요. 그런데 컨테이너는 휘발성(ephemeral)이라는 특징을 가지고 있거든요. 즉, 컨테이너가 죽거나 재시작되면 그 안에 저장된 데이터는 사라져 버린다는 뜻이죠. 데이터베이스나 파일 서버처럼 데이터를 영구적으로 보관해야 하는 애플리케이션에는 치명적일 수 있습니다.

    이런 문제를 해결하기 위해 Kubernetes에서는 영구 볼륨(Persistent Volume, PV)과 영구 볼륨 클레임(Persistent Volume Claim, PVC)이라는 개념을 도입했어요. 쉽게 말해, 스토리지 시스템으로부터 저장 공간을 할당받아 Pod가 데이터를 안정적으로 쓰고 읽을 수 있게 해주는 기능이라고 보시면 됩니다. 그리고 이런 영구 볼륨을 동적으로 프로비저닝(provisioning)해주는 역할을 하는 것이 바로 CSI(Container Storage Interface) 드라이버를 기반으로 하는 분산 스토리지 솔루션들이죠. 오늘 다룰 Longhorn과 Rook-Ceph가 바로 그런 친구들입니다.

    Kubernetes 영구 스토리지 아키텍처 개요

    Kubernetes 클러스터에서 영구 스토리지가 어떻게 작동하는지 대략적인 아키텍처를 그려봤어요. Pod가 PVC를 요청하면, CSI 드라이버가 백엔드 스토리지 시스템에 PV를 생성하고 Pod에 연결해주는 방식이죠. 이 과정에서 Longhorn이나 Rook-Ceph 같은 분산 스토리지 솔루션이 그 백엔드 역할을 하는 겁니다.

    Longhorn과 Rook-Ceph, 넌 누구니?

    본격적인 벤치마크에 앞서, 두 솔루션에 대해 간략하게 알아볼까요? 저도 처음엔 이름만 듣고 뭐가 뭔지 헷갈렸거든요. 😅

    Longhorn (롱혼)

    • 특징: Rancher Labs에서 개발한 오픈소스 분산 블록 스토리지 솔루션이에요. Kubernetes 위에서 컨테이너 형태로 동작하며, 각 노드의 로컬 스토리지를 모아 분산 볼륨을 생성합니다. CSI 드라이버를 기본으로 제공해서 Kubernetes와 통합이 정말 쉬워요. 제가 직접 써보니, 설치도 비교적 간단하고 웹 UI가 직관적이어서 관리하기 편하더라고요. 홈랩이나 소규모 환경에 많이 추천되는 이유를 알겠더군요.
    • 장점: 가벼움, 설치 및 관리 용이, 스냅샷/백업/복원 기능 내장, DR(재해 복구) 지원.

    Rook-Ceph (룩-세프)

    • 특징: Rook는 Kubernetes 네이티브 스토리지 오케스트레이터이고, Ceph(세프)는 강력하고 성숙한 오픈소스 분산 스토리지 시스템이에요. Rook는 Ceph를 Kubernetes 클러스터 위에 배포하고 관리하는 오퍼레이터(Operator) 역할을 합니다. 오브젝트 스토리지(Object Storage), 블록 스토리지(Block Storage), 파일 스토리지(File Storage)를 모두 지원하는 만능 재주꾼이죠. 다만, Ceph 자체가 워낙 복잡한 시스템이라 Rook를 통해 배포해도 초기 설정과 관리에 공수가 좀 들어가는 편이에요. 대규모 프로덕션 환경에서 많이 사용됩니다.
    • 장점: 뛰어난 확장성, 높은 가용성, 다양한 스토리지 타입 지원, 강력한 성능(하드웨어에 따라).

    두 솔루션의 주요 특징을 표로 정리해봤어요.

    구분 Longhorn Rook-Ceph
    개발사/기반 Rancher Labs / 분산 블록 스토리지 Rook (오퍼레이터) + Ceph (분산 스토리지)
    스토리지 타입 블록 스토리지 블록, 오브젝트, 파일 스토리지
    설치 난이도 쉬움 (Helm) 중간 ~ 어려움 (kubectl apply)
    관리 UI 직관적인 웹 UI Ceph Dashboard (별도 설치), Grafana 연동
    확장성 중간 (수십 TB 규모) 매우 높음 (페타바이트 규모)
    주요 사용처 홈랩, 소규모 개발/테스트, 엣지 컴퓨팅 대규모 프로덕션, 클라우드 인프라

    벤치마크 환경 구축하기

    저의 홈랩 환경은 다음과 같았습니다. 실제 벤치마크 결과는 하드웨어 스펙, 네트워크 환경, Kubernetes 설정 등에 따라 크게 달라질 수 있다는 점을 미리 말씀드려요! ⚠️

    • Kubernetes Cluster: 3 Node (1 Master, 2 Worker)
    • OS: Ubuntu Server 22.04 LTS
    • CPU: Intel i5-10400 (각 노드당 6코어 12스레드)
    • RAM: 32GB (각 노드당)
    • Storage: NVMe SSD 500GB (각 노드당, 스토리지용으로 할당)
    • Network: 1Gbps Ethernet

    벤치마크 툴로는 FIO (Flexible I/O Tester)를 사용했어요. FIO는 디스크 I/O 성능을 측정하는 데 널리 사용되는 강력한 도구예요. 순차 읽기/쓰기, 랜덤 읽기/쓰기 등 다양한 패턴으로 I/O를 발생시켜 실제 워크로드와 유사한 환경을 만들 수 있거든요.

    Longhorn 설치 및 설정

    Longhorn은 Helm(헬름) 차트를 이용하면 정말 쉽게 설치할 수 있어요. 저도 처음엔 혹시나 복잡할까 봐 걱정했는데, 생각보다 너무 간단해서 놀랐어요. 🎉

    1. Helm Repository 추가:
      helm repo add longhorn https://charts.longhorn.io
      helm repo update
      
    2. Longhorn 설치: (기본 설정으로 설치했습니다)
      helm install longhorn longhorn/longhorn --namespace longhorn-system --create-namespace
      
    3. Longhorn UI 접속:

      설치가 완료되면, Longhorn UI에 접속할 수 있도록 Ingress(인그레스, 외부 트래픽 진입점)나 NodePort(노드포트)를 설정해줘요. 저는 간단하게 NodePort로 열어서 접속했어요.

      kubectl -n longhorn-system get svc longhorn-frontend
      # 출력되는 NodePort를 확인하여 접속
      
    4. StorageClass 확인 및 볼륨 생성:

      Longhorn이 설치되면 기본적으로 longhorn이라는 StorageClass(스토리지클래스)가 생성돼요. 이걸 이용해서 PVC를 생성하고 Pod에 마운트하면 끝입니다.

      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: longhorn-pvc
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: longhorn
        resources:
          requests:
            storage: 10Gi
      

    Longhorn 웹 UI에 접속하면 현재 클러스터의 스토리지 상태, 노드별 디스크 사용량, 볼륨 목록 등을 한눈에 확인할 수 있어요. 시각적으로 깔끔하고 직관적이라서 관리하기 정말 편하더라고요. 💡

    Longhorn 웹 UI 대시보드 화면

    Longhorn 대시보드 화면이에요. 생성된 볼륨, 노드의 스토리지 상태 등을 쉽게 확인할 수 있어요.

    Rook-Ceph 설치 및 설정

    Rook-Ceph는 Longhorn보다 설정할 부분이 많아서, 처음엔 “이게 다 뭔가…” 싶었어요. 하지만 공식 문서(Official Documentation)를 차근차근 따라가면 충분히 설치할 수 있어요. 저는 helm 대신 kubectl apply -f 방식으로 진행했습니다.

    1. Rook Operator 배포:

      먼저 Rook Operator를 배포해요. 이 오퍼레이터가 Ceph 클러스터를 관리하는 역할을 합니다.

      git clone --single-branch --branch v1.11.x https://github.com/rook/rook.git
      cd rook/deploy/examples
      kubectl create -f crds.yaml -f common.yaml -f operator.yaml
      
    2. Ceph Cluster 배포:

      Ceph 클러스터를 배포해요. 이 과정에서 어떤 디스크를 Ceph 스토리지로 사용할지 지정해줘야 해요. 저는 각 워커 노드의 NVMe SSD를 사용하도록 설정했습니다.

      kubectl create -f cluster.yaml
      

      cluster.yaml 파일에서 storage 섹션에 useAllNodes: true와 useAllDevices: true를 설정하여 모든 노드의 모든 디바이스를 사용하도록 했어요. 실제 운영 환경에서는 특정 디스크만 사용하도록 명시하는 것이 좋습니다.

    3. Ceph Block Pool 및 StorageClass 생성:

      Ceph 클러스터가 안정화되면, 블록 스토리지를 위한 CephBlockPool(세프 블록 풀)과 StorageClass를 생성해요.

      kubectl create -f csi/rbd/storageclass.yaml
      

      storageclass.yaml 파일은 다음과 비슷하게 생겼을 겁니다.

      apiVersion: ceph.rook.io/v1
      kind: CephBlockPool
      metadata:
        name: replicapool
        namespace: rook-ceph
      spec:
        failureDomain: host
        replicated:
          size: 3 # 데이터 복제본 수
      ---
      apiVersion: storage.k8s.io/v1
      kind: StorageClass
      metadata:
        name: rook-ceph-block
      provisioner: rook-ceph.rbd.csi.ceph.com
      parameters:
        clusterID: rook-ceph
        pool: replicapool
        imageFormat: "2"
        imageFeatures: "layering"
        csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
        csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
        csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
        csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
      reclaimPolicy: Delete
      allowVolumeExpansion: true
      mountOptions:
        - discard
      
    4. PVC 생성:

      Longhorn과 마찬가지로, 생성된 rook-ceph-block StorageClass를 이용해 PVC를 생성해요.

      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: rook-ceph-pvc
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: rook-ceph-block
        resources:
          requests:
            storage: 10Gi
      
    Rook-Ceph on Kubernetes 클러스터 구성도

    Rook-Ceph 클러스터의 내부 구성도예요. Ceph의 여러 구성요소(MON, OSD, MGR)가 Pod 형태로 Kubernetes 클러스터 위에서 동작하는 것을 볼 수 있어요.

    FIO 벤치마크 실행 및 결과 분석

    두 스토리지 솔루션의 PVC를 각각 생성한 후, FIO를 실행할 Pod를 띄워 벤치마크를 진행했어요. 저는 주로 랜덤 읽기/쓰기(Random Read/Write)와 순차 읽기/쓰기(Sequential Read/Write) 성능을 중점적으로 살펴봤습니다. 블록 크기(Block Size)는 4KB와 1MB로 다양하게 테스트해봤거든요.

    FIO Pod를 위한 YAML 예시예요. 이 Pod가 PVC를 마운트하여 FIO 테스트를 수행합니다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: fio-test-pod
    spec:
      restartPolicy: Never
      containers:
      - name: fio-container
        image: ghcr.io/fio/fio:latest # FIO 이미지를 사용합니다.
        command: ["/bin/bash", "-c"]
        args:
          - |
            mkdir -p /mnt/data
            fio --name=random-write --ioengine=libaio --iodepth=64 --rw=randwrite --bs=4k --size=1G --numjobs=1 --runtime=60 --group_reporting --filename=/mnt/data/testfile
            fio --name=random-read --ioengine=libaio --iodepth=64 --rw=randread --bs=4k --size=1G --numjobs=1 --runtime=60 --group_reporting --filename=/mnt/data/testfile
            fio --name=sequential-write --ioengine=libaio --iodepth=16 --rw=write --bs=1M --size=2G --numjobs=1 --runtime=60 --group_reporting --filename=/mnt/data/testfile_seq
            fio --name=sequential-read --ioengine=libaio --iodepth=16 --rw=read --bs=1M --size=2G --numjobs=1 --runtime=60 --group_reporting --filename=/mnt/data/testfile_seq
        volumeMounts:
        - name: test-volume
          mountPath: /mnt/data
      volumes:
      - name: test-volume
        persistentVolumeClaim:
          claimName: longhorn-pvc # 또는 rook-ceph-pvc
    

    여러 번의 테스트를 통해 제가 관찰한 결과는 대략 이랬어요. (⚠️ 이 수치는 제 특정 홈랩 환경에서 얻은 결과이며, 환경에 따라 크게 달라질 수 있음을 다시 한번 강조합니다!)

    테스트 항목 (4KB BS) Longhorn (평균) Rook-Ceph (평균) 비고
    랜덤 쓰기 IOPS 약 1,500 ~ 2,500 약 3,000 ~ 5,000 Rook-Ceph가 우위
    랜덤 쓰기 대역폭 (MB/s) 약 6 ~ 10 약 12 ~ 20 Rook-Ceph가 우위
    랜덤 읽기 IOPS 약 2,000 ~ 3,500 약 4,000 ~ 6,500 Rook-Ceph가 우위
    랜덤 읽기 대역폭 (MB/s) 약 8 ~ 14 약 16 ~ 26 Rook-Ceph가 우위
    테스트 항목 (1MB BS) Longhorn (평균) Rook-Ceph (평균) 비고
    순차 쓰기 대역폭 (MB/s) 약 80 ~ 120 약 150 ~ 250 Rook-Ceph가 우위
    순차 읽기 대역폭 (MB/s) 약 100 ~ 150 약 200 ~ 300 Rook-Ceph가 우위

    결과를 보면, 제 홈랩 환경에서는 Rook-Ceph가 전반적으로 Longhorn보다 더 높은 IOPS와 대역폭 성능을 보여줬어요. 특히 작은 블록 크기의 랜덤 I/O에서 Ceph의 강점이 두드러지더라고요. Longhorn은 복제본(replica) 수가 늘어날수록 성능 하락이 좀 더 눈에 띄는 경향이 있었고, Ceph는 안정적으로 성능을 유지했습니다.

    Longhorn과 Rook-Ceph FIO 벤치마크 결과 비교 그래프

    Longhorn과 Rook-Ceph의 FIO 벤치마크 결과를 시각적으로 비교한 그래프예요. Rook-Ceph가 전반적으로 높은 성능을 보였습니다.

    삽질 경험과 트러블슈팅 ⚠️

    솔직히 말씀드리면, 순조롭게만 진행된 건 아니었어요. 삽질 좀 했습니다 ㅎㅎ. 몇 가지 기억에 남는 경험을 공유해볼게요.

    • Longhorn: 네트워크 대역폭과 복제본 수
      처음 Longhorn을 설치하고 테스트했을 때, 생각보다 성능이 안 나와서 당황했어요. 알고 보니, Longhorn은 네트워크를 통해 볼륨 데이터를 복제(replication)하기 때문에, 노드 간 네트워크 대역폭이 정말 중요하더라고요. 제 1Gbps 네트워크 환경에서는 복제본 수를 3개로 설정했을 때 쓰기 성능이 꽤 많이 떨어지는 것을 확인했어요. 복제본 수가 늘어날수록 안정성은 높아지지만, 그만큼 네트워크 오버헤드(overhead)가 커져서 성능에 영향을 미친다는 점! 홈랩에서는 2개로 줄여서 사용하니 훨씬 나아졌습니다.
    • Rook-Ceph: OSD 인식 문제와 초기 설정
      Rook-Ceph는 초기 설정이 Longhorn보다 훨씬 복잡했어요. 특히 OSD(Object Storage Daemon)가 디스크를 제대로 인식하지 못해서 한참을 헤맸거든요. 디스크가 이미 파티셔닝(partitioning)되어 있거나, 이전에 다른 용도로 사용된 흔적이 남아있으면 Rook-Ceph가 제대로 디스크를 잡지 못하는 경우가 있더라고요. ceph-volume lvm zap --destroy /dev/sdX 같은 명령어로 디스크를 완전히 초기화해주거나, lsblk -f 명령어로 디스크의 파일 시스템(filesystem) 정보를 꼼꼼히 확인해서 해결했어요. Ceph의 CRD(Custom Resource Definition)와 설정 파일에 대한 이해가 필요하다는 것을 다시 한번 깨달았습니다.

    결론 및 선택 가이드

    제 홈랩 벤치마크 경험을 바탕으로 Longhorn과 Rook-Ceph에 대한 저의 생각을 정리해봤어요.

    솔루션 장점 단점 추천 시나리오
    Longhorn
    • 설치 및 관리가 매우 쉬움
    • 직관적인 웹 UI 제공
    • 스냅샷, 백업, 재해 복구 기능 내장
    • 가벼운 리소스 사용
    • 네트워크 대역폭에 민감 (복제본 수 증가 시)
    • Rook-Ceph 대비 낮은 최대 성능
    • 블록 스토리지에 한정
    • 홈랩, 개발/테스트 환경
    • 소규모 프로덕션 (수십 TB 이하)
    • 간편한 관리와 사용성을 중시할 때
    Rook-Ceph
    • 최고 수준의 성능 (적절한 하드웨어 시)
    • 뛰어난 확장성 (페타바이트 규모)
    • 블록, 오브젝트, 파일 스토리지 모두 지원
    • 엔터프라이즈급 안정성과 기능
    • 설치 및 설정 난이도가 높음
    • 초기 학습 곡선이 김
    • 상대적으로 많은 리소스 사용
    • 문제 발생 시 트러블슈팅 복잡
    • 대규모 프로덕션 환경
    • 고성능, 고가용성이 필수적인 경우
    • 다양한 스토리지 타입이 필요한 경우

    제 홈랩에서는 아무래도 설치 및 관리의 용이성 때문에 Longhorn에 손이 더 자주 가더라고요. 하지만 성능 자체만 놓고 본다면 Rook-Ceph가 확실히 우위를 점했어요. 결국 어떤 Kubernetes 스토리지 솔루션을 선택할지는 여러분의 환경과 요구사항에 따라 달라진다는 것이 저의 결론입니다.

    여러분도 직접 설치해보시고, 여러분의 워크로드에 맞는 벤치마크를 수행해보시면 좋은 인사이트를 얻으실 수 있을 거예요. 저의 삽질 경험이 여러분의 시행착오를 조금이나마 줄여줄 수 있다면 좋겠습니다! 😊

    다음 글에서는 Kubernetes에서 Grafana(그라파나)와 Prometheus(프로메테우스)를 이용해 스토리지 성능을 모니터링하는 방법에 대해 다뤄볼 예정이에요. 기대해주세요!

    Longhorn과 Rook-Ceph 중 어떤 솔루션이 당신에게 맞을지 요약한 인포그래픽입니다.

  • [k8s] Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    [k8s] Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    안녕하세요, 13년차의 서버실 운영자입니다. 오늘은 제가 홈랩과 회사에서 Argo CD 멀티 클러스터 환경을 구축하면서 겪었던 일들과, 여러분이 효율적인 GitOps 베스트 프랙티스를 적용할 수 있도록 돕는 GitOps 체크리스트를 공유해드리려고 합니다.

    요즘 Kubernetes 다중 클러스터 관리는 선택이 아니라 필수가 되어가고 있죠. 개발, 스테이징, 프로덕션 환경이 따로 있거나, DR(재해 복구)을 위해 여러 리전에 클러스터를 두는 경우가 많습니다. 근데 이 클러스터들을 일일이 관리하는 게 보통 일이 아니더라고요. 직접 해보니 삽질 좀 했습니다. (ㅎㅎ)

    이런 고민을 하시는 분들을 위해, Argo CD를 활용한 멀티 클러스터 GitOps의 개념부터 실전 전략, 그리고 제가 직접 겪었던 트러블슈팅 경험까지 솔직하게 풀어보겠습니다. 이 글을 통해 여러분의 Argo CD 배포 전략이 한층 더 견고해지길 바랍니다. 드디어 배포가 편해지는 그 날을 위해!

    Argo CD 멀티 클러스터 GitOps 아키텍처 다이어그램

    Argo CD를 이용한 멀티 클러스터 GitOps의 전체 아키텍처는 위 그림처럼 구성할 수 있습니다. 중앙의 Argo CD가 여러 클러스터를 관리하는 형태죠.

    Argo CD와 GitOps, 그리고 멀티 클러스터 관리 핵심 개념

    우선, 핵심 개념부터 쉽고 편하게 짚고 넘어가 볼까요? 제가 처음 Argo CD를 접했을 때를 생각하면서 설명해 드릴게요.

    GitOps: Git이 진리의 원천입니다

    GitOps(깃옵스)는 말 그대로 Git을 운영(Operations)의 중심으로 삼는 방식입니다. 모든 인프라와 애플리케이션의 상태를 Git 리포지토리에 선언적으로 정의하고, 이 Git 리포지토리의 변경사항이 자동으로 실제 환경에 반영되도록 하거든요. 쉽게 말해, kubectl apply -f를 사람이 직접 하는 게 아니라, Git에 커밋만 하면 시스템이 알아서 해주는 거죠. 제가 처음 이걸 알았을 때, ‘와, 이거 진짜 편하겠다!’ 싶었어요.

    GitOps의 장점은 명확합니다:

    • 버전 관리(Version Control): 모든 변경 이력을 Git에서 확인할 수 있어요. 누가 언제 뭘 바꿨는지 투명하게 알 수 있죠.
    • 자동화(Automation): 수동 작업이 줄어들어 휴먼 에러를 최소화하고, 배포 속도를 높일 수 있습니다.
    • 복원력(Resilience): 문제가 발생하면 Git의 이전 상태로 쉽게 롤백(Rollback)할 수 있습니다.
    • 협업(Collaboration): 개발팀과 운영팀이 Git을 통해 더 효율적으로 협업할 수 있습니다.

    Argo CD: GitOps를 Kubernetes에 현실로 만들어주는 도구

    그럼 Argo CD(아르고 CD)는 뭘까요? Argo CD는 선언적(Declarative) GitOps 지속적 배포(Continuous Delivery) 도구거든요. Kubernetes(쿠버네티스) 애플리케이션의 배포와 라이프사이클 관리를 Git에서 정의된 상태에 따라 자동으로 동기화(Sync)해주는 역할을 해요. 제가 써보니까, 복잡한 배포 파이프라인을 구축하는 수고를 엄청나게 덜어주더라고요.

    멀티 클러스터 관리: 복잡성을 단순화하기

    여러 개의 Kubernetes 클러스터를 관리하는 건 마치 여러 대의 서버를 손으로 직접 관리하는 것과 비슷해요. 하나만 관리할 때는 괜찮지만, 두 개, 세 개… 점점 늘어나면 감당하기 어려워집니다. Kubernetes 다중 클러스터 관리는 이런 복잡성을 줄이고 일관성을 유지하기 위한 전략이 필요해요. Argo CD는 이 문제를 ApplicationSet(애플리케이션셋)이라는 기능을 통해 아주 우아하게 해결해 줍니다. 처음엔 이게 뭔가 싶었는데, 써보니 진짜 물건이더라고요!

    실전 구현: Argo CD 멀티 클러스터 환경 설정 베스트 프랙티스

    이제 이론을 바탕으로 실제로 어떻게 구성하는지 알아볼 시간입니다. 제가 홈랩에서 여러 시도를 해보고, 회사에서 적용하면서 얻은 노하우들을 풀어드릴게요.

    1. Argo CD Control Plane 설치

    먼저, Argo CD가 설치될 메인 클러스터, 즉 Control Plane(컨트롤 플레인) 클러스터가 필요합니다. 이곳에서 모든 배포를 관장하게 됩니다. 기본적인 Argo CD 설치는 공식 문서에 잘 나와 있는데, 간단히 요약해볼게요.

    
    kubectl create namespace argocd
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    

    설치 후에는 Argo CD UI에 접속하여 초기 비밀번호를 설정하고 로그인할 수 있습니다.

    2. Managed Clusters(관리 대상 클러스터) 등록

    다음으로, Argo CD가 관리할 다른 Kubernetes 클러스터들을 등록해야 합니다. 저는 개발, 스테이징, 프로덕션 클러스터를 각각 등록했거든요. Argo CD는 kubectl 명령어를 통해 쉽게 클러스터를 등록할 수 있도록 해줍니다.

    
    # 먼저 Argo CD CLI를 설치합니다.
    brew install argocd # macOS 기준
    
    # Argo CD API 서버에 로그인합니다.
    argocd login <ARGOCD_SERVER_IP_OR_HOSTNAME>
    
    # 관리 대상 클러스터의 kubeconfig 컨텍스트로 전환합니다.
    kubectl config use <MANAGED_CLUSTER_CONTEXT_NAME>
    
    # 현재 컨텍스트의 클러스터를 Argo CD에 등록합니다.
    argocd cluster add <MANAGED_CLUSTER_CONTEXT_NAME>
    

    이 명령어를 실행하면 Argo CD가 해당 클러스터에 필요한 RBAC(Role-Based Access Control) 권한을 자동으로 설정하고, 클러스터를 관리 목록에 추가합니다. 이게 정말 편리하더라고요. 제가 일일이 RBAC을 설정할 필요가 없어서 좋았어요.

    Argo CD UI에서 여러 Kubernetes 클러스터가 관리되는 모습

    Argo CD UI에 접속하면 이렇게 등록된 여러 클러스터들이 한눈에 보입니다. 각 클러스터의 상태도 직관적으로 확인할 수 있죠.

    3. ApplicationSet 활용: 멀티 클러스터 배포의 꽃!

    수동으로 각 클러스터에 Application(애플리케이션)을 생성하는 건 비효율적입니다. 여기서 ApplicationSet이 빛을 발합니다. ApplicationSet은 여러 클러스터에 동일하거나 유사한 형태의 Application을 자동으로 생성해주는 CRD(Custom Resource Definition)거든요. 제가 처음엔 Application을 복사 붙여넣기 했었는데, ApplicationSet을 알고 나서는 ‘아, 이래서 쓰는구나!’ 하고 무릎을 탁 쳤습니다.

    ApplicationSet을 사용하면 Git 리포지토리의 특정 경로에 있는 매니페스트를 여러 클러스터에 배포하거나, 클러스터 이름에 따라 다른 설정 값을 주입하는 등의 고급 배포 전략을 구현할 수 있습니다.

    예시 YAML 코드를 한번 볼까요? 저는 Git 제너레이터를 이용해서 특정 Git 경로의 폴더들을 클러스터로 배포하는 방식을 즐겨 사용합니다.

    
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: my-multi-cluster-app
    spec:
      generators:
      - git:
          repoURL: https://github.com/my-org/my-gitops-repo.git
          revision: HEAD
          directories:
          - path: clusters/dev/*
          - path: clusters/stg/*
          - path: clusters/prod/*
      template:
        metadata:
          name: '{{path.basename}}-app'
          labels:
            app.kubernetes.io/managed-by: argocd
        spec:
          project: default
          source:
            repoURL: https://github.com/my-org/my-gitops-repo.git
            targetRevision: HEAD
            path: '{{path}}'
          destination:
            server: '{{path.basename | clusterServer}}' # 클러스터 이름에 따라 서버 URL 매핑
            namespace: default
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
            syncOptions:
              - CreateNamespace=true
    

    위 예시에서는 clusters/dev, clusters/stg, clusters/prod 폴더 각각을 하나의 GitOps Application으로 인식하고, 해당 폴더 이름에 매핑되는 클러스터에 배포하도록 설정한 거예요. {{path.basename | clusterServer}}와 같은 템플릿 문법을 활용해서 클러스터의 서버 URL을 동적으로 주입할 수 있습니다. 물론 clusterServer 같은 함수는 직접 구현하거나, Argo CD의 List 제너레이터 등 다른 제너레이터를 활용하여 클러스터 정보를 직접 명시할 수도 있습니다.

    Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 체크리스트

    이제 제가 경험하면서 중요하다고 느꼈던 Argo CD 멀티 클러스터 GitOps 베스트 프랙티스들을 체크리스트 형태로 정리해 보았습니다. 이대로만 따라 하셔도 웬만한 삽질은 피할 수 있을 거예요!

    1. ✅ 단일 Git 리포지토리(Single Source of Truth) 사용: 모든 클러스터와 애플리케이션의 상태는 하나의 Git 리포지토리에서 관리하는 것이 좋습니다. 폴더 구조를 잘 정리해서 혼란을 줄이세요. (예: repo/clusters/dev, repo/clusters/prod, repo/apps/my-app)
    2. ✅ ApplicationSet 적극 활용: 멀티 클러스터 배포의 핵심입니다. 클러스터별 배포, 환경별 차이점 관리 등을 ApplicationSet으로 자동화하세요.
    3. ✅ Helm(헬름) 또는 Kustomize(커스터마이즈)로 설정 관리: 환경(dev, stg, prod)별로 다른 설정이 필요할 때, Helm Values나 Kustomize Overlays를 사용하여 매니페스트의 차이를 관리합니다. 저는 Helm을 주로 사용하는데, 템플릿 기능이 강력해서 유연하게 대응할 수 있더라고요.
    4. ✅ RBAC(Role-Based Access Control) 최소 권한 원칙 준수: Argo CD 컨트롤 플레인과 각 관리 대상 클러스터 간의 통신 시, Argo CD가 필요한 최소한의 권한만 가지도록 RBAC을 설정해야 합니다. 보안은 아무리 강조해도 지나치지 않아요!
    5. ✅ Secret(시크릿) 관리 전략 수립: 민감 정보는 Git에 직접 올리지 않고, HashiCorp Vault, Sealed Secrets, External Secrets Operator 같은 도구를 사용하여 안전하게 관리해야 합니다.
    6. ✅ 자동 동기화(Automated Sync) 및 Self-Heal(자가 복구) 활성화: Git의 변경사항이 자동으로 클러스터에 반영되고, 클러스터의 상태가 Git과 다를 경우 자동으로 복구되도록 설정합니다. 이게 GitOps의 가장 큰 매력 중 하나죠.
    7. ✅ Rollback(롤백) 전략 수립 및 테스트: 문제가 발생했을 때 신속하게 이전 버전으로 롤백할 수 있도록 전략을 세우고, 실제 롤백 테스트를 주기적으로 수행하세요.
    8. ✅ 모니터링(Monitoring) 및 로깅(Logging) 구축: Argo CD의 상태, 배포 이력, 각 클러스터의 애플리케이션 상태 등을 모니터링하고 로깅 시스템에 연동하여 가시성을 확보해야 합니다. Prometheus(프로메테우스)와 Grafana(그라파나)는 국룰이죠.
    9. ✅ PreSync/PostSync Hooks(훅) 활용: 배포 전후에 특정 작업을 수행해야 할 경우, PreSync/PostSync Hooks를 사용하여 데이터베이스 마이그레이션이나 캐시 초기화 같은 작업을 자동화할 수 있습니다.

    ⚠️ 삽질 경험 & 트러블슈팅: 제가 겪었던 문제들

    13년차 엔지니어도 삽질은 피할 수 없는 운명입니다. 제가 Argo CD 멀티 클러스터를 구축하면서 겪었던 몇 가지 삽질과 해결책을 공유해 드릴게요.

    1. 네트워크 연결 문제: 방화벽은 항상 제 발목을 잡죠

    문제: Argo CD Control Plane에서 Managed Cluster로 접근이 안 되는 경우가 있었습니다. 클러스터가 다른 VPC나 다른 데이터센터에 있을 때 주로 발생하더라고요.

    해결: 뻔하지만 가장 중요한 건 방화벽(Firewall)과 네트워크 ACL(Access Control List) 설정입니다. Argo CD Control Plane이 Managed Cluster의 Kubernetes API 서버에 접근할 수 있도록 네트워크 경로를 열어줘야 합니다. 보통 443 포트를 사용하죠. 저는 보안 그룹 설정을 빼먹어서 한참 헤맸습니다. 😭

    2. RBAC 권한 부족: ‘Forbidden’ 메시지는 언제나 당황스럽습니다

    문제: Argo CD가 특정 클러스터에 애플리케이션을 배포하려는데 Forbidden 에러가 뜨는 경우가 있었습니다. ApplicationSet이 Application을 생성하지 못하거나, Application이 특정 리소스를 생성하지 못하는 상황이었죠.

    해결: Argo CD가 Managed Cluster에 설치하는 argocd-manager ServiceAccount(서비스 어카운트)의 RBAC 권한을 확인해야 합니다. ClusterRole 또는 Role이 필요한 verbs(get, list, watch, create, update, patch, delete)와 resources(pods, deployments, services 등)를 가지고 있는지 점검해야 합니다. 저는 CustomResourceDefinition(CRD)을 배포하려는데 권한이 없어서 한참 고생했어요. CRD는 특별한 권한이 필요하거든요.

    3. Sync Status ‘Out Of Sync’: Git과 실제 상태가 다를 때

    문제: Argo CD UI에서 분명히 Sync를 했는데 계속 Out Of Sync 상태로 남아있는 경우가 있었습니다. 특히 Helm 차트를 사용할 때 자주 발생했죠.

    해결: 여러 원인이 있을 수 있습니다. 첫째, Helm Hooks(훅)가 제대로 동작하지 않거나, helm.sh/hook-delete-policy 같은 주석이 설정되어 있지 않은 경우. 둘째, CRD가 먼저 배포되지 않은 상태에서 해당 CRD를 사용하는 리소스가 배포되려 할 때. 셋째, 클러스터 내부의 컨트롤러가 Git에 없는 변경을 일으키는 경우. 넷째, resource.customizations 설정을 통해 특정 리소스의 필드를 무시하도록 설정하는 방법도 있습니다. 저는 특히 CRD 배포 순서 때문에 많이 헷갈렸는데, PreSync Hook을 이용하거나, ApplicationSet에서 CRD Application을 먼저 배포하도록 순서를 조정해서 해결했습니다.

    검증 및 결과: 이제 배포가 이렇게 편해집니다!

    이런 삽질과 노하우를 바탕으로 Argo CD 멀티 클러스터 GitOps 환경을 제대로 구축하고 나니, 정말 배포의 패러다임이 바뀌는 것을 느꼈습니다. 제가 직접 Git에 커밋하고 푸시만 하면, 개발, 스테이징, 프로덕션 클러스터에 알아서 척척 배포되는 모습을 보면 정말 뿌듯하더라고요.

    • 🎉 일관성 있는 배포: 모든 클러스터에 동일한 방식으로 애플리케이션이 배포됩니다.
    • 🎉 배포 속도 향상: 수동 작업이 사라지고, Git 변경만으로 배포가 완료됩니다.
    • 🎉 가시성 확보: Argo CD UI에서 모든 클러스터의 애플리케이션 상태를 한눈에 파악할 수 있습니다.
    • 🎉 안정성 증가: 잘못된 배포는 Git 롤백으로 쉽게 되돌릴 수 있습니다.
    Argo CD 대시보드에서 멀티 클러스터 배포 성공 현황

    위 그림처럼 Argo CD 대시보드에서 여러 클러스터에 걸쳐 배포된 애플리케이션들의 상태를 실시간으로 확인할 수 있습니다. 모든 것이 ‘Synced’ 상태일 때의 그 쾌감이란! (웃음)

    마무리: 더 나은 GitOps 여정을 위해

    오늘은 Argo CD 멀티 클러스터 GitOps에 대한 저의 경험과 베스트 프랙티스 체크리스트를 공유해드렸습니다. 처음에는 복잡하게 느껴질 수 있지만, 한번 구축하고 나면 정말 강력한 배포 시스템을 손에 넣게 될 겁니다. 저도 처음엔 헷갈렸는데, 계속 시도하고 삽질하면서 익숙해지더라고요. 독자 여러분도 이 글을 통해 성공적인 GitOps 여정을 시작하시길 바랍니다.

    혹시 GitOps 환경에서 더 깊이 있는 보안 강화나, CI(Continuous Integration) 파이프라인과의 연동에 대해 궁금하시다면, 다음 글에서 다룰 예정이니 기대해주세요!

    Argo CD 멀티 클러스터 GitOps 베스트 프랙티스 요약 체크리스트

    지금까지의 내용을 요약한 체크리스트입니다. 여러분의 GitOps 환경을 점검할 때 유용하게 활용되길 바랍니다.

  • [k8s] Argo CD 멀티 클러스터 GitOps 도입 사례: 운영 노하우와 도전 과제

    [k8s] Argo CD 멀티 클러스터 GitOps 도입 사례: 운영 노하우와 도전 과제

    [인프라] Argo CD 멀티 클러스터 GitOps 도입 사례: 운영 노하우와 도전 과제

    안녕하세요, 13년차의 서버실 운영자입니다. 오늘은 제가 직접 겪었던 경험을 바탕으로 Argo CD 멀티 클러스터 GitOps 도입 사례에 대해 이야기해볼까 합니다. 쿠버네티스(Kubernetes)를 운영하다 보면 자연스럽게 여러 클러스터를 관리해야 하는 상황에 부딪히게 되죠. 개발, 스테이징, 프로덕션 환경을 분리하거나, 재해 복구(Disaster Recovery)를 위해 지리적으로 분산된 클러스터를 운영하는 경우도 많고요. 처음엔 각각의 클러스터에 배포하는 것도 정말 힘들었는데, 이 모든 것을 효율적으로 관리하려니 골머리가 지끔했었습니다. 혹시 이런 경험 있으신가요?

    저도 처음엔 수동 배포와 스크립트의 늪에서 헤맸습니다. 그러다 GitOps(깃옵스)라는 개념을 접하고, 특히 Argo CD(아르고 CD)가 이 문제를 해결해 줄 수 있다는 걸 알게 되었죠. 특히 여러 클러스터를 한 곳에서 관리할 수 있는 Argo CD 멀티 클러스터 기능은 정말 반가웠습니다. 오늘은 이 기능을 도입하면서 제가 겪었던 삽질과 노하우, 그리고 얻었던 운영 효율성에 대해 솔직하게 풀어보려 합니다.

    Argo CD 멀티 클러스터 GitOps 아키텍처 다이어그램

    Argo CD 멀티 클러스터 GitOps의 전체 아키텍처 다이어그램입니다. 중앙의 Argo CD 컨트롤 플레인이 여러 쿠버네티스 클러스터에 애플리케이션을 배포하고 관리하는 모습을 보여줍니다.

    GitOps와 Argo CD, 그리고 멀티 클러스터 관리

    먼저, GitOps(깃옵스)가 뭔지 간단히 짚고 넘어가 보겠습니다. 쉽게 말하면, Git(깃) 저장소를 진리의 원천(Source of Truth)으로 삼아 인프라와 애플리케이션 배포를 관리하는 방식이에요. 모든 변경 사항은 Git에 기록되고, Git의 상태와 실제 클러스터의 상태를 일치시키는 것이 핵심입니다. 이렇게 하면 누가 언제 무엇을 변경했는지 추적하기 쉽고, 문제가 생겼을 때 이전 상태로 되돌리기도(Rollback) 훨씬 수월하죠.

    그리고 Argo CD(아르고 CD)는 이런 GitOps 원칙을 쿠버네티스 환경에서 구현하도록 도와주는 선언적(Declarative) GitOps 지속적 배포(Continuous Delivery) 도구입니다. Git 저장소에 정의된 상태를 주기적으로 감지해서 쿠버네티스 클러스터의 실제 상태와 비교하고, 다르면 Git의 상태에 맞춰 동기화(Sync)해줍니다. 정말 편리하더라고요.

    그렇다면 멀티 클러스터 관리(Multi-Cluster Management)는 뭘까요? Argo CD는 한 개의 인스턴스로 여러 개의 쿠버네티스 클러스터에 배포를 관리할 수 있는 기능을 제공합니다. 메인 Argo CD가 설치된 클러스터(저는 이걸 컨트롤 플레인 클러스터(Control Plane Cluster)라고 부릅니다)에서 여러 타겟 클러스터(Target Cluster)들을 등록하고, 각각의 클러스터에 어떤 애플리케이션을 어떤 버전으로 배포할지 Git을 통해 지시하는 방식이에요. 덕분에 여러 환경에 동일한 애플리케이션을 일관성 있게 배포하고 관리하는 게 훨씬 쉬워졌습니다. 💡

    실전 구현: Argo CD 멀티 클러스터 환경 구축하기

    자, 이제 실제로 어떻게 구성했는지 살펴보겠습니다. 저는 세 개의 클러스터를 준비했는데, 하나는 Argo CD가 설치될 컨트롤 플레인 클러스터, 그리고 나머지 두 개는 애플리케이션이 배포될 타겟 클러스터입니다. 모든 클러스터는 kubeconfig(쿠브콘피그)를 통해 접근할 수 있도록 설정되어 있다고 가정하겠습니다.

    1. Argo CD 설치 및 타겟 클러스터 등록

    먼저, 컨트롤 플레인 클러스터에 Argo CD를 설치합니다. 이건 공식 문서에 잘 나와 있으니 간단하게 넘어갈 테고, 핵심은 타겟 클러스터를 Argo CD에 등록하는 것입니다. Argo CD CLI를 사용하면 정말 간단하게 등록할 수 있어요.

    
    # Argo CD CLI 설치 (공식 문서 참고)
    # brew install argocd
    
    # Argo CD 로그인 (admin 계정 패스워드는 초기 설치 시 자동으로 생성됩니다)
    argocd login <ARGOCD_SERVER_IP_OR_HOSTNAME>
    
    # 타겟 클러스터 등록
    # kubeconfig에 등록된 클러스터 이름을 사용합니다.
    # 제 경우엔 dev-cluster와 prod-cluster라는 이름으로 등록되어 있었죠.
    argocd cluster add dev-cluster --name dev-cluster-region-a
    argocd cluster add prod-cluster --name prod-cluster-region-b
    
    # 등록된 클러스터 목록 확인
    argocd cluster list
    

    이렇게 하면 Argo CD가 해당 클러스터에 접근해서 애플리케이션을 배포할 수 있는 권한을 가지게 됩니다. 중요한 부분은 RBAC(Role-Based Access Control, 역할 기반 접근 제어) 설정인데, Argo CD가 타겟 클러스터에서 필요한 리소스(Deployment, Service 등)를 생성/수정/삭제할 수 있도록 적절한 권한을 부여해야 합니다. 저는 처음엔 이걸 놓쳐서 “왜 배포가 안 될까?” 한참 삽질했었습니다. ⚠️

    Argo CD UI 클러스터 목록 스크린샷

    Argo CD 웹 UI에 등록된 dev-cluster-region-a와 prod-cluster-region-b 클러스터의 목록과 상태를 보여주는 화면입니다. 모든 클러스터가 Healthy 상태로 나타나 있네요.

    2. Git 저장소 구조화: App of Apps 패턴

    멀티 클러스터 환경에서 Git 저장소를 효율적으로 관리하는 방법 중 하나는 App of Apps 패턴(App of Apps Pattern)입니다. 하나의 최상위 Argo CD Application이 여러 하위 Application들을 관리하는 방식이에요. 저는 다음과 같이 Git 저장소를 구성했습니다.

    
    .
    ├── applications/
    │   ├── base/ # 공통 애플리케이션 정의 (예: Ingress Controller, Cert Manager)
    │   └── microservices/ # 마이크로서비스별 애플리케이션 정의
    ├── clusters/
    │   ├── dev-cluster-region-a/ # 개발 클러스터별 설정
    │   │   └── root-app.yaml
    │   └── prod-cluster-region-b/ # 프로덕션 클러스터별 설정
    │       └── root-app.yaml
    └── environments/
        ├── dev/ # 개발 환경별 값 (values.yaml for Helm)
        └── prod/ # 프로덕션 환경별 값 (values.yaml for Helm)
    

    각 클러스터 폴더 안의 `root-app.yaml`은 해당 클러스터에 배포될 모든 하위 애플리케이션을 정의하는 최상위 Argo CD Application입니다. 예를 들어 `dev-cluster-region-a/root-app.yaml`은 다음과 같이 작성할 수 있죠.

    
    # clusters/dev-cluster-region-a/root-app.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: dev-cluster-root-app
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/my-gitops-repo.git # 본인의 Git 저장소 URL
        targetRevision: HEAD
        path: applications # 하위 애플리케이션 정의들이 있는 경로
      destination:
        server: https://kubernetes.default.svc # 이 Application은 Argo CD가 설치된 클러스터에 적용
        namespace: argocd # Argo CD가 Application 리소스를 관리하는 네임스페이스
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
    

    그리고 `applications/` 경로 안에는 실제 배포될 애플리케이션들의 정의가 들어갑니다. 예를 들어, `applications/microservices/my-service.yaml`은 다음과 같을 수 있어요. 여기서 ApplicationSet(애플리케이션셋)을 사용하면 여러 클러스터에 동일한 애플리케이션을 배포하는 것이 훨씬 유연해집니다.

    
    # applications/microservices/my-service.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: my-service-all-clusters
      namespace: argocd
    spec:
      generators:
      - list:
          elements:
          - cluster: dev-cluster-region-a # Argo CD에 등록된 클러스터 이름
            url: https://kubernetes.default.svc # 타겟 클러스터의 API 서버 URL
            name: dev
          - cluster: prod-cluster-region-b
            url: https://kubernetes.default.svc
            name: prod
      template:
        metadata:
          name: my-service-{{name}} # 클러스터 이름에 따라 Application 이름이 생성됩니다 (예: my-service-dev, my-service-prod)
          namespace: default
        spec:
          project: default
          source:
            repoURL: https://github.com/my-org/my-app-helm-charts.git # 애플리케이션 Helm 차트 저장소
            targetRevision: 1.0.0
            path: my-service
            helm:
              values: |
                replicaCount: 2
                image:
                  tag: "1.0.0-{{name}}" # 환경별 이미지 태그 (예: 1.0.0-dev, 1.0.0-prod)
                ingress:
                  host: my-service.{{name}}.example.com
          destination:
            server: "{{url}}" # 위에서 정의된 타겟 클러스터의 URL
            namespace: default
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
            syncOptions:
              - CreateNamespace=true
    

    위 `ApplicationSet` 예시는 `list` 제너레이터를 사용해서 `dev`와 `prod` 두 환경에 `my-service`를 배포하도록 정의하고 있습니다. 각 환경에 맞는 `values`를 `helm` 섹션에서 오버라이드(Override)할 수 있어서 정말 유용해요. 저는 Helm(헬름) 차트를 주로 사용하는데, Kustomize(커스터마이즈)나 일반 YAML도 물론 지원합니다.

    ⚠️ 삽질 경험과 운영 노하우

    Argo CD 멀티 클러스터를 운영하면서 몇 가지 정말 잊지 못할 경험들이 있습니다. 독자분들은 저와 같은 실수를 반복하지 않으시길 바라며 몇 가지 중요한 팁을 공유합니다.

    1. RBAC 권한 문제

    가장 흔하고 고통스러운 문제예요. Argo CD가 타겟 클러스터에 배포할 때, 필요한 권한이 없어서 배포가 실패하는 경우가 정말 많습니다. Argo CD를 등록할 때 부여하는 권한은 ServiceAccount(서비스 어카운트)를 통해 부여되는데, 이 서비스 어카운트가 타겟 클러스터에서 ClusterRole(클러스터 롤)과 ClusterRoleBinding(클러스터 롤 바인딩)을 통해 적절한 권한을 가지고 있는지 반드시 확인해야 합니다. 저는 처음엔 `admin` 권한을 무심코 줬다가 보안 팀에게 한소리 듣고, 나중에는 필요한 최소한의 권한만 부여하도록 변경했습니다. 💡

    
    # 예시: Argo CD ServiceAccount에 필요한 ClusterRoleBinding 부여 (타겟 클러스터에서 실행)
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: argocd-manager-role-binding
    subjects:
    - kind: ServiceAccount
      name: argocd-manager # Argo CD가 사용하는 ServiceAccount 이름
      namespace: argocd # Argo CD가 설치된 네임스페이스
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: cluster-admin # 임시로 전체 권한 부여. 실제 운영에서는 더 세분화된 Role을 사용하세요.
    

    실제 운영에서는 `cluster-admin` 대신 `edit`이나 `view` 같은 기본 Role을 조합하거나, 특정 리소스에 대한 `get`, `list`, `watch`, `create`, `update`, `patch`, `delete` 권한을 명시적으로 부여하는 커스텀 ClusterRole을 만들어서 사용하는 게 훨씬 안전합니다.

    2. 네트워크 연결 및 방화벽

    Argo CD 컨트롤 플레인 클러스터에서 타겟 클러스터의 API 서버에 접근할 수 있어야 합니다. 만약 클러스터들이 서로 다른 네트워크에 있거나, 방화벽(Firewall) 정책이 엄격하다면 연결 문제가 발생할 수 있죠. 저는 회사 보안 정책 때문에 VPC 피어링(VPC Peering)이나 VPN(Virtual Private Network) 설정을 통해 연결을 확보해야 했어요. 핑(Ping) 테스트나 `kubectl`로 직접 연결해보면서 연결성을 확인하는 것이 정말 중요합니다.

    3. 시크릿 관리(Secret Management)

    여러 클러스터에 배포되는 애플리케이션들은 데이터베이스 비밀번호나 API 키 같은 민감한 정보(Secret)들을 필요로 합니다. 이걸 Git에 그대로 올릴 수는 없겠죠. 저는 External Secrets Operator(외부 시크릿 오퍼레이터)와 Vault(볼트)를 연동해서 사용하거나, Sealed Secrets(실드 시크릿)를 활용해서 시크릿을 안전하게 관리했습니다. Git에 암호화된 형태로 저장하고, 각 클러스터에서 복호화해서 사용하는 방식이에요. 이 부분은 보안상 정말 중요하니 깊이 있게 고민해야 합니다.

    4. 설정 드리프트(Configuration Drift)

    GitOps의 핵심은 Git 저장소의 상태와 실제 클러스터의 상태가 일치해야 한다는 거예요. 그런데 운영 중에 누군가 클러스터에서 직접 리소스를 수정해서 Git과 클러스터의 상태가 달라지는 설정 드리프트(Configuration Drift)가 발생할 수 있습니다. Argo CD는 이런 드리프트를 감지하고 자동으로 동기화(Self-Heal)하거나, 수동으로 동기화할 수 있는 기능을 제공합니다. 하지만 근본적으로는 클러스터에 직접 접근해서 변경하는 것을 정책적으로 금지하고, 모든 변경은 Git을 통해서만 이루어지도록 강제하는 게 중요합니다.

    🎉 Argo CD 멀티 클러스터 도입 후기 및 성과

    숱한 삽질 끝에 Argo CD 멀티 클러스터 GitOps 환경을 성공적으로 구축하고 운영하게 되었습니다. 그 결과는 정말 놀라웠어요.

    • 배포 일관성 및 안정성 향상: 개발, 스테이징, 프로덕션 클러스터에 동일한 버전의 애플리케이션을 일관된 방식으로 배포할 수 있게 되었습니다. 수동 작업으로 인한 휴먼 에러(Human Error)가 정말 줄었죠.
    • 배포 속도 단축: Git에 코드를 푸시(Push)만 하면 Argo CD가 자동으로 감지하고 배포를 시작합니다. CI/CD 파이프라인(Pipeline)과 연동하여 개발자들이 훨씬 빠르게 변경 사항을 적용할 수 있게 되었어요.
    • 쉬운 롤백(Rollback): 문제가 발생했을 때, Git의 이전 커밋(Commit)으로 되돌리는 것만으로 애플리케이션을 이전 상태로 쉽게 롤백할 수 있습니다. 비상 상황에서 정말 큰 도움이 되더군요.
    • 운영 가시성 확보: Argo CD UI를 통해 모든 클러스터의 애플리케이션 상태를 한눈에 확인할 수 있게 되었습니다. 어떤 애플리케이션이 어느 클러스터에 어떤 버전으로 배포되어 있는지 파악하기가 정말 쉬워졌어요.
    Argo CD UI 멀티 클러스터 애플리케이션 상태 대시보드

    Argo CD 웹 UI에서 dev-cluster와 prod-cluster에 배포된 여러 애플리케이션들의 동기화 상태와 Health 상태를 한눈에 보여주는 대시보드 화면입니다. 모든 애플리케이션이 Sync’d and Healthy 상태를 나타내고 있네요.

    Argo CD 멀티 클러스터 도입 전후 비교

    구분 도입 전 (수동/스크립트 기반) 도입 후 (Argo CD 멀티 클러스터 GitOps)
    배포 방식 클러스터별 수동 `kubectl` 적용 또는 쉘 스크립트 실행 Git 커밋 기반 자동 동기화 (선언적)
    배포 일관성 환경별 설정 차이 발생 가능성 높음, 휴먼 에러 빈번 Git을 통한 일관된 설정 적용, 휴먼 에러 최소화
    롤백 용이성 이전 상태 복구 어려움, 시간 소요 Git 커밋 되돌리기로 빠르고 안정적인 롤백
    운영 가시성 각 클러스터에 개별 접근하여 상태 확인 단일 Argo CD UI에서 모든 클러스터 및 애플리케이션 상태 확인
    보안 및 감사 수동 변경 이력 추적 어려움 Git 변경 이력을 통한 완벽한 감사 추적

    위 표에서 보시는 것처럼, Argo CD 멀티 클러스터 GitOps는 인프라 운영 방식에 혁신적인 변화를 가져다주었습니다. 특히 여러 클러스터를 관리해야 하는 복잡한 환경에서는 그 진가가 더욱 빛을 발하더군요.

    마무리하며: 배운 점과 다음 도전 과제

    오늘은 제가 Argo CD 멀티 클러스터 GitOps를 도입하면서 겪었던 과정과 노하우, 그리고 얻었던 성과에 대해 이야기해봤습니다. 처음엔 낯선 개념과 복잡한 설정 때문에 막막하기도 했지만, 한번 구축하고 나니 인프라 운영의 질이 한 단계 높아지는 걸 체감할 수 있었습니다. 특히 쿠버네티스 멀티 클러스터 관리에 대한 고민이 많으셨던 분들께 저의 Argo CD GitOps 사례가 작은 도움이 되었으면 좋겠어요.

    물론 아직 도전 과제는 남아있습니다. 예를 들어, 프로그레시브 딜리버리(Progressive Delivery)를 위해 Argo Rollouts(아르고 롤아웃)과 연동하거나, 더 복잡한 클러스터 간 의존성을 관리하는 방법, 그리고 비용 최적화와 같은 부분은 앞으로도 계속 고민하고 실험해봐야 할 영역입니다. 💡

    저처럼 GitOps 도입 후기를 찾고 계셨던 분들이라면, Argo CD를 적극적으로 검토해보시길 추천합니다. 직접 해보면 생각보다 어렵지 않고, 얻는 이점은 훨씬 크다는 걸 느끼실 겁니다. 다음번에는 더 깊이 있는 주제로 찾아오겠습니다. 긴 글 읽어주셔서 감사합니다! 궁금한 점이 있다면 언제든지 댓글로 남겨주세요.

  • [k8s] Kustomize와 Helm, K8s 설정 관리 도구 실측 성능 비교

    [k8s] Kustomize와 Helm, K8s 설정 관리 도구 실측 성능 비교

    Kustomize와 Helm, K8s 설정 관리 도구 실측 성능 비교: 어떤 놈이 더 빠를까요?

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘도 여전히 복잡한 인프라의 세계 속에서 삽질하고 계실 독자분들을 위해 제 경험을 풀어볼까 합니다. 😅

    Kubernetes(쿠버네티스, 이하 K8s) 환경에서 애플리케이션을 배포하고 관리하다 보면, YAML 파일의 홍수에 빠지는 경험, 다들 있으실 겁니다. 이 수많은 설정 파일들을 어떻게 효과적으로 관리할까? 바로 여기서 Kustomize(커스터마이즈)와 Helm(헬름) 같은 K8s 설정 관리(Configuration Management) 도구들이 등장하죠.

    두 도구 모두 강력한 기능을 제공하지만, 실제 운영 환경에서는 ‘과연 어떤 도구가 더 빠르고 효율적일까?’ 하는 고민을 많이 하게 됩니다. 특히 CI/CD 파이프라인에서 빌드 시간이 길어지면 개발자의 생산성에도 영향을 미치거든요. 그래서 오늘은 제가 직접 두 도구를 사용해보면서 느꼈던 Kustomize vs Helm 성능 비교와 최적화 전략에 대해 이야기해볼게요. 멘토처럼 솔직한 경험담과 함께요! 💡

    Kubernetes 설정 관리를 위한 Kustomize와 Helm 워크플로우 비교 다이어그램

    –>

    Kubernetes 설정 관리를 위한 Kustomize와 Helm 워크플로우 비교 다이어그램

    1. Kustomize와 Helm, 넌 누구냐? 🤔

    먼저 두 도구가 정확히 어떤 역할을 하는지, 개념부터 확실히 잡고 가볼까요?

    1.1. Kustomize: K8s 네이티브 설정 템플릿

    Kustomize는 Kubernetes 네이티브 설정 관리(Kubernetes native configuration management) 도구입니다. 쉽게 말해, K8s가 YAML 파일을 처리하는 방식과 매우 유사하게 동작해요. 템플릿 엔진을 사용하지 않고, 기존 YAML 파일(Base)에 덧붙이는(Overlay) 방식으로 설정을 변경합니다.

    • 선언적(Declarative): 최종 상태를 선언하는 방식이라 직관적입니다.
    • 패치(Patch) 기반: 원본 파일을 직접 수정하지 않고, 필요한 부분만 덮어쓰는 형식이에요.
    • 경량화(Lightweight): K8s 내부에 통합되어 별도의 바이너리 설치 없이 kubectl 명령어에 포함되어 있습니다. (kubectl kustomize 또는 kustomize build)

    1.2. Helm: K8s의 패키지 매니저

    Helm은 K8s를 위한 패키지 매니저(Package Manager)입니다. 마치 Ubuntu의 apt나 CentOS의 yum처럼, K8s 애플리케이션을 차트(Chart)라는 형태로 패키징하고 배포하며 관리할 수 있게 해줘요.

    • 템플릿 엔진(Templating Engine): Go 템플릿(Go Template) 문법을 사용하여 YAML 파일을 동적으로 생성합니다. values.yaml 파일로 변수를 주입하죠.
    • 릴리즈(Release) 관리: 배포된 애플리케이션의 버전을 관리하고, 업그레이드 및 롤백을 쉽게 할 수 있습니다.
    • 의존성 관리(Dependency Management): 다른 차트를 하위 차트(subchart)로 포함시켜 복잡한 애플리케이션 스택을 한 번에 배포할 수 있어요.

    2. 실전 구현: 어떻게 비교할까? 🛠️

    두 도구의 성능을 비교하려면, 동일한 조건에서 최대한 유사한 작업을 수행하게 해야겠죠? 제가 홈랩에서 간단한 웹 애플리케이션을 배포하면서 비교했던 방법을 공유해볼게요.

    2.1. Kustomize로 YAML 빌드하기

    먼저 Kustomize는 kustomization.yaml 파일을 작성하여 여러 YAML 파일을 조합하고 패치합니다. 예를 들어, 개발 환경과 운영 환경에 따라 리소스의 replica 수나 이미지 태그를 다르게 적용하는 시나리오를 가정해봅시다.

    # base/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app
    spec:
      replicas: 1
      template:
        spec:
          containers:
          - name: my-app
            image: my-repo/my-app:1.0.0
    
    # overlays/dev/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    resources:
    - ../../base
    patches:
    - path: patch-dev.yaml
    
    # overlays/dev/patch-dev.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app
    spec:
      replicas: 2 # 개발 환경은 2개로
    
    

    이렇게 구성된 Kustomize 프로젝트는 다음 명령어로 최종 YAML을 생성합니다.

    time kustomize build overlays/dev > dev-app.yaml
    

    time 명령어를 붙여서 실행 시간을 측정하는 거죠. 간단하죠? ⏱️

    2.2. Helm으로 차트 렌더링하기

    Helm은 차트 디렉토리 구조와 values.yaml 파일을 사용해서 YAML을 렌더링합니다. 마찬가지로 개발 환경과 운영 환경에 따라 설정을 다르게 해볼게요.

    # my-app-chart/templates/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {{ .Release.Name }}-{{ .Chart.Name }}
    spec:
      replicas: {{ .Values.replicaCount }}
      template:
        spec:
          containers:
          - name: {{ .Chart.Name }}
            image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
    
    # my-app-chart/values.yaml (기본값)
    replicaCount: 1
    image:
      repository: my-repo/my-app
      tag: 1.0.0
    
    # my-app-chart/values-dev.yaml (개발 환경 오버라이드)
    replicaCount: 2
    image:
      tag: 1.0.0-dev
    

    Helm 차트는 다음 명령어로 최종 YAML을 렌더링합니다.

    time helm template my-app my-app-chart -f my-app-chart/values-dev.yaml > dev-app-helm.yaml
    

    마찬가지로 time 명령어로 렌더링 시간을 측정합니다. 💡

    Kustomize 오버레이와 Helm 차트 values.yaml 파일 구조 시각화

    –>

    Kustomize 오버레이와 Helm 차트 values.yaml 파일 구조 시각화

    3. 주의사항과 삽질 경험 ⚠️

    단순히 time 명령어를 쓰는 것만으로는 완벽한 비교가 어렵더라고요. 제가 겪었던 삽질 경험을 토대로 몇 가지 주의사항을 공유합니다.

    3.1. Helm 복잡성과 의존성 관리 오버헤드

    Helm은 Go 템플릿을 사용하고, 복잡한 의존성 관리(Dependency Management) 기능을 제공합니다. 차트 내에 수많은 하위 차트(subchart)가 있거나, 템플릿 로직이 복잡해질수록 렌더링 시간이 기하급수적으로 늘어날 수 있어요. 제가 처음엔 너무 많은 헬름 차트를 한 번에 묶었다가 렌더링에만 수십 초씩 걸려서 깜짝 놀랐거든요. 😅

    반면 Kustomize는 단순히 YAML 파일을 병합하고 패치하는 방식이라, 상대적으로 오버헤드가 적습니다. 하지만 패치 파일이 너무 많아지거나, 복잡한 jsonPatch를 사용하면 가독성이 떨어지고 디버깅이 어려워지는 단점이 있습니다.

    3.2. 캐싱(Caching)과 컨테이너 환경

    CI/CD 파이프라인에서 실행할 때는 도커 컨테이너 환경에서 실행하는 경우가 많죠. 이때 도구 바이너리 로딩 시간이나 컨테이너 이미지 크기도 성능에 영향을 줄 수 있습니다. Helm은 별도의 바이너리가 필요하고, Kustomize는 kubectl에 통합되어 있거나 단독 바이너리를 사용합니다. 아주 미세한 차이지만, 빌드 횟수가 많아지면 무시할 수 없는 요소가 됩니다.

    3.3. ‘실측’의 의미: 실제 배포까지의 시간

    단순히 YAML 파일을 생성하는 시간뿐만 아니라, K8s API 서버에 리소스를 배포하고 적용하는 시간까지 고려해야 진정한 실측 성능(Real-world Performance)이라고 할 수 있습니다. kubectl apply -f 또는 helm upgrade --install 명령어가 실제로 얼마나 걸리는지도 함께 측정해봐야 합니다.

    4. 검증 및 결과: 무엇이 더 빠를까? 🚀

    결론부터 말씀드리면, ‘케바케(Case by Case)’입니다. 😅 하지만 제가 여러 프로젝트에서 직접 사용해보고 측정해본 결과, 다음과 같은 일반적인 경향을 발견했습니다.

    4.1. Kustomize의 장점 (대규모 프로젝트 초기 빌드/변경)

    • 빠른 빌드 시간: 단순 YAML 병합 및 패치 방식이라 템플릿 엔진의 오버헤드가 없습니다. 수백 개의 리소스가 있어도 빌드 자체는 상당히 빠르게 완료되는 편입니다.
    • K8s 네이티브 통합: kubectl 명령어에 포함되어 있어 별도의 도구 설치 및 관리 부담이 적습니다.
    • 예측 가능한 결과: 템플릿 로직이 없어 결과 YAML이 직관적이고 예측하기 쉽습니다.

    4.2. Helm의 장점 (복잡한 템플릿/패키징)

    • 강력한 템플릿 기능: Go 템플릿을 활용해 복잡한 조건부 로직이나 반복문 등을 구현하기 용이합니다.
    • 패키지 관리의 용이성: 재사용 가능한 차트 형태로 애플리케이션을 배포하고 관리하는 데 최적화되어 있습니다. 외부 차트를 쉽게 가져다 쓸 수 있고요.
    • 릴리즈 히스토리 관리: 배포된 애플리케이션의 버전 관리와 롤백이 매우 편리합니다.

    4.3. 성능에 영향을 미치는 주요 요인

    어떤 도구를 쓰든 다음 요소들이 성능에 큰 영향을 줍니다.

    1. 생성되는 YAML 리소스의 수: 많을수록 처리 시간이 길어집니다.
    2. Helm 차트의 복잡성: 템플릿 내의 조건문, 반복문, 함수 호출 등이 많을수록 렌더링 시간이 길어집니다.
    3. Kustomize 패치의 수와 복잡성: 너무 많은 패치나 복잡한 jsonPatch는 처리 시간을 늘릴 수 있습니다.
    4. 의존성(Dependency) 관리: 특히 Helm에서 서브 차트의 수가 많아지면 초기 로딩 및 렌더링 오버헤드가 발생합니다.
    5. 실행 환경의 성능: CI/CD 에이전트의 CPU, 메모리, 디스크 I/O도 영향을 미칩니다.

    제가 실제 측정해보니, 리소스 수가 적을 때는 두 도구 간의 빌드 시간 차이가 미미했지만, 수백 개 이상의 리소스를 다루거나 Helm 차트의 템플릿 로직이 복잡해질수록 Helm의 렌더링 시간이 Kustomize의 빌드 시간보다 눈에 띄게 길어지는 경향을 보였습니다. ⚠️

    Kustomize와 Helm 빌드 시간 측정 및 성능 비교 결과

    –>

    Kustomize와 Helm 빌드 시간 측정 및 성능 비교 결과

    5. K8s 설정 관리 최적화 전략 ⚙️

    결국 중요한 건 ‘어떤 도구가 절대적으로 빠르냐’가 아니라, ‘어떤 도구를 어떻게 사용해서 우리 환경에 최적화하느냐’입니다.

    • Kustomize 최적화:
      • 베이스(Base) 재사용: 공통된 설정은 베이스로 만들고, 오버레이는 최소한의 변경만 하도록 구성합니다.
      • 패치 최소화: 불필요하게 작은 단위로 패치를 나누기보다, 응집도 있는 패치를 사용합니다.
      • GitOps 워크플로우: Kustomize는 GitOps와 궁합이 좋습니다. Flux CD나 Argo CD 같은 도구와 함께 사용하면 변경 사항을 자동으로 감지하고 적용할 수 있어요.
    • Helm 차트 최적화:
      • 템플릿 복잡성 줄이기: Go 템플릿 내의 복잡한 로직을 최소화하고, 필요한 경우 별도의 스크립트나 도구로 전처리하는 것을 고려합니다.
      • 서브 차트 의존성 관리: 꼭 필요한 경우에만 서브 차트를 사용하고, 불필요한 의존성은 제거합니다. 서브 차트가 많으면 관리가 어려워지더라고요.
      • --atomic, --wait 옵션 활용: 배포 시 안정성을 높이지만, 이 옵션들이 배포 시간을 늘릴 수 있다는 점을 인지하고 적절히 사용해야 합니다.
      • 캐싱 활용: CI/CD 환경에서 Helm dependency update 등의 과정을 캐싱하여 시간을 절약할 수 있습니다.

    저는 개인적으로 간단하고 직관적인 애플리케이션은 Kustomize를, 복잡한 의존성을 가진 상용 솔루션이나 여러 마이크로서비스를 묶어 배포해야 할 때는 Helm을 선호하는 편입니다. 두 도구를 혼용(Hybrid)하여 사용하는 방법도 좋은 전략이 될 수 있어요. 예를 들어, Helm 차트를 Kustomize의 베이스로 사용하고, 그 위에 환경별 패치를 적용하는 방식이죠. 🤯

    Kustomize와 Helm의 주요 기능 및 장단점 비교 인포그래픽

    –>

    Kustomize와 Helm의 주요 기능 및 장단점 비교 인포그래픽

    6. 마무리: 우리의 삽질은 계속된다! 🎉

    오늘은 Kustomize와 Helm이라는 두 가지 강력한 K8s 설정 관리 도구의 성능 비교와 최적화 전략에 대해 이야기해봤습니다. 어느 한쪽이 ‘무조건 최고다’라고 말하기는 어렵고, 각자의 장단점과 사용 환경에 따라 선택이 달라질 수 있다는 점이 핵심입니다.

    저도 처음엔 ‘어떤 게 더 좋지?’ 하면서 벤치마크 숫자놀음에 집착했었는데요, 결국 중요한 건 ‘우리 팀의 워크플로우에 얼마나 잘 맞고, 유지보수가 쉬우며, 안정적인가’ 하는 점이더라고요. 성능 측정은 그 판단을 돕는 하나의 지표일 뿐입니다. 여러분도 직접 사용해보면서 자신만의 최적의 방법을 찾아보시길 바랍니다. 혹시 더 좋은 Helm 차트 최적화나 Kustomize 장점 활용 팁이 있다면 댓글로 공유해주세요! 다음 글에서는 Kustomize와 Helm을 GitOps 환경에서 연동하는 방법에 대해 더 깊이 다뤄볼 예정입니다. 기대해주세요! 😉

  • [Cloud] Jenkins on Kubernetes 마이그레이션: 클라우드 네이티브 CI/CD 전환 사례

    [Cloud] Jenkins on Kubernetes 마이그레이션: 클라우드 네이티브 CI/CD 전환 사례

    안녕하세요, 13년차의 서버실 운영자입니다. 오늘은 많은 분들이 고민하고 계실 법한 주제, 바로 Jenkins on Kubernetes 마이그레이션 경험담을 풀어보려고 합니다. 사실 저도 기존 Jenkins 환경을 운영하면서 여러 가지 페인 포인트(Pain Point)를 겪었거든요. 빌드가 몰리면 서버 리소스가 부족하고, 특정 에이전트(Agent)에 문제가 생기면 전체 파이프라인이 멈추고… 이런 경험, 혹시 여러분도 있으신가요?

    클라우드 네이티브(Cloud-Native) 환경으로의 전환은 이제 선택이 아닌 필수가 되어가고 있습니다. CI/CD(Continuous Integration/Continuous Deployment, 지속적 통합/지속적 배포) 파이프라인의 핵심인 Jenkins 역시 예외는 아니죠. 기존 VM 기반의 Jenkins를 Kubernetes(쿠버네티스) 위로 옮기면서 얻게 된 이점과, 그 과정에서 겪었던 삽질 경험을 솔직하게 공유해 드릴게요. 이 글이 여러분의 Jenkins Kubernetes 마이그레이션 여정에 멘토 같은 역할을 해주었으면 좋겠습니다.

    Jenkins on Kubernetes 클라우드 네이티브 CI/CD 아키텍처 다이어그램

    Jenkins on Kubernetes 아키텍처는 효율적인 CI/CD 파이프라인을 위한 핵심입니다. 동적 에이전트가 필요할 때마다 생성되어 리소스를 최적화합니다.

    Jenkins on Kubernetes, 왜 필요할까요?

    기존 Jenkins는 보통 고정된 마스터(Master)와 여러 개의 에이전트(Agent) 서버로 구성됩니다. 작업이 많아지면 에이전트 서버를 증설해야 하고, 사용하지 않을 때도 리소스를 계속 점유하죠. 특정 에이전트에 문제가 생기면 해당 에이전트에서 동작하는 모든 작업이 실패할 수 있으니 정말 골치 아픈 거거든요.

    하지만 Jenkins on Kubernetes 환경은 다릅니다. 쉽게 말해, 필요한 순간에만 에이전트(Agent)를 띄워서 작업을 처리하고, 끝나면 바로 없애는 방식입니다. Jenkins 마스터는 Kubernetes 클러스터 내의 컨트롤러(Controller)로 동작하고, CI/CD 파이프라인 작업이 필요할 때마다 Kubernetes Pod(파드) 형태로 에이전트를 동적으로 생성합니다. 작업이 완료되면 해당 Pod는 자동으로 소멸됩니다. 이게 바로 클라우드 네이티브 CI/CD의 핵심 장점 중 하나예요.

    ✅ 주요 장점

    • 탄력적인 확장성 (Elastic Scalability): 빌드 요청이 폭증해도 Kubernetes가 알아서 에이전트 Pod를 늘려줍니다.
    • 리소스 효율성 (Resource Efficiency): 작업이 없을 때는 에이전트 Pod가 존재하지 않으므로 불필요한 리소스 낭비가 없습니다.
    • 고가용성 (High Availability): Kubernetes의 자체 복구(Self-healing) 기능 덕분에 에이전트 Pod에 문제가 생겨도 다른 Pod로 대체되어 안정성이 높아집니다.
    • 선언적 구성 (Declarative Configuration): Jenkinsfile(젠킨스파일)과 JCasC(Jenkins Configuration as Code)를 통해 CI/CD 파이프라인과 Jenkins 자체 구성을 코드로 관리할 수 있습니다.

    Jenkins Kubernetes 마이그레이션, 실전 구현 단계

    자, 그럼 이제 제가 실제로 어떻게 Jenkins 현대화를 진행했는지 단계별로 알려드릴게요. 처음엔 어디서부터 손대야 할지 막막했는데, 하나씩 해나가다 보니 길이 보이더라고요.

    1. Kubernetes 클러스터 준비

    가장 먼저 할 일은 Jenkins를 올릴 Kubernetes 클러스터를 준비하는 겁니다. 클라우드 환경에서는 AWS EKS(Elastic Kubernetes Service), Azure AKS(Azure Kubernetes Service), Google GKE(Google Kubernetes Engine) 같은 관리형 서비스를 이용하는 게 편합니다. 저는 홈랩에서 K3s 클러스터를 운영하고 있어서 그걸 활용했어요. 온프레미스(On-premise) 환경이라면 kubeadm이나 OpenShift 등을 고려할 수 있겠죠.

    2. Helm을 이용한 Jenkins 설치

    Kubernetes에 애플리케이션을 배포하는 가장 일반적인 방법 중 하나가 Helm(헬름) 차트입니다. Jenkins도 공식 Helm 차트를 제공하고 있어서 쉽게 설치할 수 있어요. 물론, 설치 전에 PersistentVolume(영구 볼륨)과 PersistentVolumeClaim(영구 볼륨 클레임)을 설정해서 Jenkins 설정과 데이터를 보존할 수 있도록 해야 합니다. 이 부분이 제일 중요해요. 데이터 날리면 대형사고잖아요?

    # Helm 리포지토리 추가
    helm repo add jenkins https://charts.jenkins.io
    helm repo update
    
    # values.yaml 파일 커스터마이징 (예시)
    # persistentVolume 셋팅, resource limit, ingress 설정 등
    # 자세한 내용은 공식 Helm 차트 문서를 참고하세요!
    
    # Jenkins 설치
    helm install jenkins -f my-jenkins-values.yaml jenkins/jenkins -n jenkins --create-namespace
    

    위 명령어는 기본적인 설치 예시입니다. 실제 운영 환경에서는 `my-jenkins-values.yaml` 파일에 Ingress(인그레스, 외부 트래픽 진입점), 리소스 제한(Resource Limits), 플러그인 설정 등을 상세하게 정의해야 해요. 이 파일 하나로 Jenkins의 거의 모든 설정을 제어할 수 있거든요.

    Jenkins Helm values.yaml 설정 파일 예시

    Helm `values.yaml` 파일은 Jenkins 배포의 핵심입니다. 여기서 영구 스토리지, 리소스, 에이전트 설정을 세밀하게 제어할 수 있습니다.

    3. Kubernetes Cloud Plugin 설정

    Jenkins 마스터가 Kubernetes 클러스터와 통신하며 동적 에이전트를 생성하려면 Kubernetes Cloud Plugin(쿠버네티스 클라우드 플러그인)을 설치하고 설정해야 합니다. Jenkins UI에서 ‘Jenkins 관리’ → ‘시스템 설정’ → ‘클라우드’ 섹션으로 이동해서 Kubernetes 클러스터 정보를 입력합니다. 이때 Service Account(서비스 어카운트)와 RBAC(Role-Based Access Control) 설정을 통해 Jenkins가 Kubernetes API에 접근할 수 있도록 권한을 부여하는 것이 매우 중요합니다.

    여기서 Pod Template(파드 템플릿)을 정의하게 되는데, 이 템플릿이 바로 동적 에이전트 Pod의 설계도입니다. 어떤 도커(Docker) 이미지를 사용할지, 얼마나 많은 CPU와 메모리를 할당할지, 필요한 도구들은 무엇인지 등을 정의하죠. 예를 들어, Node.js 빌드가 필요하면 Node.js 런타임이 포함된 이미지를 지정하는 식이에요.

    4. Jenkinsfile 업데이트

    기존 Jenkinsfile도 약간의 수정이 필요할 수 있어요. 특히 `agent any`나 `agent { label ‘my-fixed-agent’ }` 같은 부분을 `agent { kubernetes { yaml ”’…”’ } }` 형태로 바꿔서 동적 Pod를 사용하도록 유도해야 합니다.

    // 기존 Jenkinsfile (예시)
    // pipeline {
    //     agent any
    //     stages {
    //         stage('Build') {
    //             steps {
    //                 sh 'npm install'
    //                 sh 'npm build'
    //             }
    //         }
    //     }
    // }
    
    // Kubernetes 동적 에이전트를 사용하는 Jenkinsfile (예시)
    pipeline {
        agent {
            kubernetes {
                // Kubernetes Pod 템플릿을 직접 정의
                yaml """
    apiVersion: v1
    kind: Pod
    spec:
      containers:
      - name: jnlp
        image: jenkins/inbound-agent:4.11.2-1
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
      - name: nodejs
        image: node:16-alpine
        command: ["cat"]
        tty: true
        resources:
          limits:
            memory: "1Gi"
            cpu: "1000m"
    """
                defaultContainer 'nodejs' // 이 컨테이너에서 스크립트 실행
            }
        }
        stages {
            stage('Build Frontend') {
                steps {
                    container('nodejs') { // 'nodejs' 컨테이너에서 실행
                        sh 'npm install'
                        sh 'npm run build'
                    }
                }
            }
            stage('Package Docker Image') {
                // 다른 컨테이너 또는 스크립트 실행
            }
        }
    }
    

    위 Jenkinsfile 예시처럼, 하나의 Pod 안에 여러 컨테이너를 띄워서 각 컨테이너의 역할을 분리할 수 있어요. 예를 들어, `nodejs` 컨테이너에서 프론트엔드 빌드를 하고, `maven` 컨테이너에서 백엔드를 빌드하는 식이죠. 이 덕분에 빌드 환경을 더욱 유연하게 구성할 수 있거든요.

    5. Configuration as Code (JCasC) 적용

    Jenkins 설정을 UI에서 하나하나 하는 건 사실 귀찮고 실수를 유발하기 쉽습니다. JCasC(Jenkins Configuration as Code)는 Jenkins의 모든 설정을 YAML(야믈) 파일로 관리할 수 있게 해줘요. 이 파일을 버전 관리 시스템(VCS)에 커밋(Commit)해두면, Jenkins 인스턴스를 새로 띄우거나 설정을 변경할 때 코드로 관리할 수 있어서 매우 편리합니다. 저도 이걸 적용하고 나서 “이거 진짜 편하더라고요!”라고 감탄했었어요. Jenkins 현대화의 필수 요소라고 생각합니다.

    ⚠️ 주의사항 및 트러블슈팅

    마이그레이션 과정에서 저도 꽤 삽질 좀 했습니다. 여러분은 저 같은 시행착오를 겪지 않으시길 바라며 몇 가지 팁을 공유합니다.

    • Persistent Volume(PV) 설정: Jenkins 마스터의 홈 디렉토리(`JENKINS_HOME`)는 반드시 영구 볼륨으로 설정해야 합니다. 안 그러면 Jenkins Pod가 재시작될 때마다 모든 설정과 플러그인이 날아가는 대참사가 발생합니다. 😱
    • 리소스 제한 (Resource Limits): 에이전트 Pod 템플릿에 CPU와 메모리 리소스 제한을 명확하게 설정해야 합니다. 너무 적으면 빌드가 실패하고, 너무 많으면 클러스터 리소스가 고갈될 수 있어요.
    • 이미지 크기 및 빌드 시간: 동적 에이전트에 사용할 도커 이미지는 필요한 도구만 포함하여 최대한 가볍게 만드는 것이 좋습니다. 이미지가 너무 크면 Pod가 스케줄링(Scheduling)되고 컨테이너(Container)가 시작되는 시간이 길어져 빌드 성능에 악영향을 줄 수 있거든요.
    • 네트워크 문제: Jenkins 마스터에서 GitHub, Nexus, SonarQube 등 외부 서비스에 접근할 수 있는지, 또는 그 반대로 외부에서 Ingress를 통해 Jenkins UI에 접근할 수 있는지 네트워크 설정을 꼼꼼히 확인해야 합니다.
    • RBAC 권한: Jenkins 컨트롤러가 Kubernetes API에 접근할 수 있도록 적절한 Role(역할)과 RoleBinding(역할 바인딩)을 가진 Service Account를 생성하고 연결해야 합니다. 권한 부족으로 Pod 생성이 안 되는 경우가 많아요.

    ✅ 검증 및 마이그레이션 결과

    모든 설정을 마치고 파이프라인을 실행하면, Jenkins 대시보드에서 동적으로 생성되는 에이전트 Pod들을 확인할 수 있습니다. Kubernetes 클러스터에서도 `kubectl get pods -n jenkins` 명령어로 새로운 Pod들이 생성되었다가 작업 완료 후 사라지는 것을 볼 수 있을 거예요. 드디어 됐다! 하고 외쳤던 순간이 기억나네요. 🎉

    Jenkins 대시보드에서 Kubernetes 동적 에이전트 빌드 성공 확인

    Jenkins 대시보드에서 동적 에이전트의 효율적인 동작을 확인하는 것은 마이그레이션 성공의 중요한 지표입니다.

    Jenkins Kubernetes 마이그레이션을 통해 저희 팀은 다음과 같은 이점들을 누릴 수 있었습니다.

    • 빌드 시간 단축: 필요한 리소스를 즉시 할당받아 빌드 시간이 단축되었습니다.
    • 운영 비용 절감: 유휴 리소스가 없어 불필요한 클라우드 비용을 줄일 수 있었어요.
    • 안정성 향상: 특정 에이전트 문제로 인한 빌드 실패가 줄어들고, Kubernetes의 복원력 덕분에 전체 시스템의 안정성이 높아졌습니다.
    • CI/CD 파이프라인 관리의 용이성: JCasC와 Jenkinsfile을 통해 파이프라인과 Jenkins 자체를 코드로 관리하게 되면서, 변경 사항 추적 및 재현성이 크게 향상되었습니다.
    Jenkins on Kubernetes 마이그레이션 핵심 이점 인포그래픽

    Jenkins on Kubernetes 마이그레이션은 탄력적 확장성, 비용 효율성, 그리고 안정성을 동시에 달성하는 효과적인 방법입니다.

    마무리하며: 클라우드 네이티브 CI/CD의 미래

    오늘은 제가 직접 경험했던 Jenkins on Kubernetes 마이그레이션 사례를 공유해 드렸습니다. 처음엔 VM 기반 Jenkins 환경에 익숙해서 변화가 어렵게 느껴졌지만, 클라우드 네이티브 환경으로의 전환은 분명 더 나은 CI/CD 파이프라인을 위한 투자라고 생각해요.

    물론 Jenkins 외에도 Argo CD(아르고 CD)나 Tekton(텍톤) 같은 훌륭한 클라우드 네이티브 CI/CD 도구들이 많이 있습니다. 하지만 기존 Jenkins 환경에 익숙하고, 점진적인 Jenkins 현대화를 원한다면 Kubernetes 위로 Jenkins를 옮기는 것이 좋은 시작점이 될 수 있다고 확신합니다.

    이 글이 여러분의 인프라 여정에 작은 도움이 되었기를 바랍니다. 다음번에는 또 다른 삽질 경험과 해결책으로 찾아오겠습니다. 궁금한 점이나 공유하고 싶은 경험이 있다면 언제든지 댓글 남겨주세요! 😉