쿠버네티스 위에서 젠킨스를 돌려보기 전까지는 "그냥 파드 하나 띄우면 되겠지"라고 생각했습니다. 실제로 해보니 서비스 타입 하나 잘못 고르는 것만으로도 외부 접속이 막히고, 도커 이미지 빌드 방식을 모르면 파이프라인이 절반도 완성이 안 됩니다. 이 글은 그 시행착오를 정리한 기록입니다.

쿠버네티스 서비스 타입과 LoadBalancer의 실체
쿠버네티스 파드(Pod)는 기본적으로 클러스터 내부에서만 통신이 가능합니다. 여기서 파드란 쿠버네티스가 컨테이너를 실행하는 최소 단위로, IP가 동적으로 배정되기 때문에 재시작될 때마다 주소가 바뀝니다. 그래서 서비스(Service)라는 리소스가 파드의 이름과 IP를 자동으로 매핑해주는 역할을 담당합니다.
서비스 타입은 크게 세 가지로 나뉩니다.
- ClusterIP: 기본값으로, 클러스터 내부 파드끼리만 통신할 수 있습니다. 서비스 이름만으로 다른 파드에 접근할 수 있어 내부 마이크로서비스 연결에 씁니다.
- NodePort: ClusterIP 기능에 더해 외부에서 노드 IP와 포트 번호로 직접 접근할 수 있습니다. 짧은 데모 용도로는 충분하지만, 노드가 사라지면 그 경로도 끊기는 단점이 있습니다.
- LoadBalancer: 클러스터 외부에서 대표 IP 하나로 접근할 수 있는 타입입니다. 실제 서비스 운영에서는 이걸 씁니다.
저도 처음엔 NodePort로 젠킨스 대시보드를 열었는데, 노드 IP가 바뀌는 상황이 생기자 접속이 끊겨서 당황했습니다. 그때 LoadBalancer로 바꾸면서 안정감이 달랐습니다.
문제는 직접 구축한 쿠버네티스 환경에서는 LoadBalancer 타입이 기본 제공되지 않는다는 점입니다. 클라우드 환경(EKS, GKE 등)에서는 자동으로 외부 로드밸런서가 붙지만, 온프레미스(On-Premises)로 직접 클러스터를 구성했다면 별도의 구현체를 설치해야 합니다. 여기서 온프레미스란 클라우드가 아닌 자체 서버 환경을 의미합니다.
이때 쓰는 것이 MetalLB입니다. MetalLB란 클라우드 없이 베어메탈 서버에서도 LoadBalancer 서비스를 쓸 수 있도록 해주는 오픈소스 네트워크 로드밸런서입니다. IPAddressPool을 정의해서 사용하지 않는 IP 대역을 외부 엔드포인트로 할당하고, L2Advertisement로 ARP(주소 결정 프로토콜) 브로드캐스트를 통해 해당 IP를 외부에 알리는 방식으로 작동합니다. 제가 직접 설치해보니 kube-proxy의 ipvs 모드와 strictARP 설정을 먼저 맞춰주지 않으면 MetalLB가 정상 동작하지 않는 경우가 있어서, 순서를 지키는 게 중요합니다. MetalLB 공식 문서에 따르면 v0.13 이상부터 IPAddressPool과 L2Advertisement를 별도 리소스로 분리하여 관리하도록 구조가 바뀌었으니 버전 확인이 필수입니다(출처: MetalLB 공식 문서).
Kaniko 파이프라인과 실제 빌드 흐름
젠킨스를 쿠버네티스 위에서 실행하면 빌드 에이전트가 파드 형태로 생성됩니다. 여기서 에이전트(Agent)란 실제 빌드 작업을 수행하는 실행 환경을 뜻하는데, 빌드가 완료되면 해당 파드는 자동으로 삭제됩니다. 이 구조 덕분에 빌드가 몰릴 때 파드가 늘어나고, 없을 때는 줄어드는 탄력적 운영이 가능합니다.
문제는 도커 이미지 빌드입니다. 일반적으로 도커 이미지를 빌드하려면 Docker 데몬(Docker Daemon)에 접근해야 합니다. Docker 데몬이란 컨테이너 생성, 실행, 이미지 빌드 등을 처리하는 백그라운드 프로세스입니다. 그런데 쿠버네티스 파드 안에서 호스트의 Docker 데몬에 직접 붙는 방식, 즉 DinD(Docker in Docker)나 소켓 마운트 방식은 보안상 권장되지 않습니다. 제 경험상 이 방식은 실습 환경에서 "일단 돌아가긴 한다"는 느낌이지 프로덕션에서 쓰기엔 불안합니다.
그래서 등장한 것이 Kaniko입니다. Kaniko란 Docker 데몬 없이 컨테이너 내부에서 직접 도커 이미지를 빌드하고 레지스트리에 푸시할 수 있게 해주는 Google의 오픈소스 도구입니다. 쿠버네티스 환경에서 CI/CD 파이프라인을 구성할 때 현재 사실상 표준으로 자리 잡고 있습니다(출처: Google Cloud Kaniko GitHub).
실제 파이프라인 구성을 보면, 하나의 파드 안에 두 개의 컨테이너를 함께 띄웁니다. Gradle 컨테이너가 Spring Boot JAR 파일을 빌드하고, 그 결과물을 같은 파드 내 emptyDir 볼륨을 통해 Kaniko 컨테이너와 공유합니다. Kaniko 컨테이너는 그 JAR과 Dockerfile을 바탕으로 이미지를 빌드한 뒤 Docker Hub에 푸시합니다. 같은 파드 내 컨테이너들은 네트워크와 볼륨을 공유한다는 쿠버네티스의 특성을 그대로 활용한 구조입니다.
프론트엔드 파이프라인도 구조는 동일합니다. node 컨테이너에서 npm install과 npm run build를 실행해 정적 파일을 생성하고, Kaniko 컨테이너가 그 결과물로 이미지를 만들어 푸시합니다. 솔직히 이건 예상 밖이었습니다. 백엔드와 프론트엔드 파이프라인의 차이가 생각보다 크지 않아서, 빌드 도구만 Gradle에서 Node로 바뀌는 수준이었습니다.
도커 허브 인증 정보는 쿠버네티스 시크릿(Secret)으로 등록하고, Kaniko 컨테이너가 마운트해서 사용합니다. 시크릿이란 비밀번호나 토큰처럼 민감한 정보를 클러스터 내에서 안전하게 관리하기 위한 쿠버네티스 리소스입니다. 이미지 태그로는 젠킨스 빌드 번호(BUILD_NUMBER)를 그대로 쓰는 방식이 관리하기 편했습니다.
쿠버네티스 위에서 젠킨스와 Kaniko를 연결하는 파이프라인은 처음에는 복잡해 보이지만, 결국 서비스 타입을 이해하고 빌드 컨테이너 구조만 잡으면 나머지는 반복 패턴입니다. 직접 구축 환경이라면 MetalLB 설치부터 차근차근 진행하고, 파이프라인은 백엔드부터 먼저 완성한 뒤 프론트로 확장하는 순서가 실수를 줄이는 데 도움이 됩니다. 이 흐름을 한 번 몸에 익혀두면 이후 CD(지속적 배포) 단계로 확장하는 것도 훨씬 수월해집니다.
참고: https://dltldnr2563.tistory.com/entry/%EC%BD%94%EB%94%A9%EA%B3%B5%EB%B6%8020250919-K8SJenkins