최근에 저는 부트캠프 내에서 프로젝트 스터디를 진행하면서 스터디의 DevOps, 그리고 백엔드 파트를 담당하고 있습니다.
그리고 현재는 백엔드 프로젝트를 Kubernetes 환경에서 운영중에 있는데요, 그 과정에서 Jenkins도 Kubernetes에서 관리하기로 결정하였습니다.
이번 글에서는 제가 왜 Jenkins를 Kubernetes로 운영하려고 하는지, 그리고 Jenkins on K8S를 설정하면서 겪은 어려움들을 여러분들께 공유해드리고자합니다.
0. 배경지식
우선 Jenkins on k8s를 설명하기 이전에, Jenkins에 대해서 조금 이야기를 해볼까합니다.
젠킨스는 여러분들도 아시다시피 CI/CD 자동화 도구입니다. 그러므로 젠킨스 agent는 사내의 모든 빌드들을 처리해야하기 때문에 리소스를 상당히 많이 차지합니다. 젠킨스 공식문서를 참고하시면 최소 RAM은 4gb에 50gb 이상의 디스크 유휴공간을 추천한다고 명시되어있습니다. 그리고 젠킨스를 master/slave 구조로 운영하는 것이 아닌 standalone 구조로 운영을 한다라고 가정하면, 젠킨스에 들어오는 빌드의 양에 따라서 많게는 단일 인스턴스의 RAM이 40GB 이상도 필요할 수 있다라고 알려져있습니다.
하지만 위의 방식은 대규모 프로젝트에서 좋은 방법은 아닐것입니다. 제가 생각한 단일 인스턴스 구조의 젠킨스는 아래의 단점을 가집니다.
- 하나의 젠킨스 인스턴스가 모든 빌드 환경을 기억해야한다. 즉, 플러그인을 하나의 인스턴스에 과도하게 설치해야한다
- 젠킨스는 플러그인을 설치할 때마다 Restart가 필요하다. 그런데 하나의 인스턴스가 모든 빌드를 책임져야하기 때문에 Restart의 빈도는 증가하며, 이는 곧 장애로 이어질 확률이 높다
- 빌드가 분산되지 않고 단일 인스턴스가 책임을 지기 때문에 RAM이 많이 있다고한들 CPU 자원이 딸려서 전체 빌드 속도가 느려질 우려도 존재한다
그래서 많은 곳에서는 젠킨스를 사용할 때 Jenkins master/slave 구조를 이용해서 slave에 빌드 환경을 구성하고, master는 빌드만 관리하는 형태로 많이 관리한다고 알려져있습니다.
1. 그런데 왜 master/slave 구조로 Jenkins를 구성하지 않고 쿠버네티스에 올려요?
사실 이번 프로젝트에서 Jenkins 환경을 구성하면서 master/slave 구조로 갈지, 혹은 jenkins on k8s 구성으로 갈지에 대해서 많은 고민을 하였습니다. 결과만 놓고 보자면, 저는 후자를 채택하였지만 그에 대한 합당한 근거를 제시해보고자합니다.
- Plugin을 설치하면 젠킨스 agent를 Restart를 해야하는건 master/slave 구조로 가져가더라도 달라진게 없다.
- master/slave 구조로 가져가면 형상관리의 난이도가 많이 상승한다. 물론 소규모의 사내 프로젝트 내지는 내가 진행중인 토이프로젝트 수준에서 필요한 인프라라면 형상관리의 난이도가 높지는 않으나, slave의 개수가 많아지고 빌드 환경이 다양해질수록 형상관리 난이도는 올라간다.
- 무엇보다도 내가 jenkins on k8s를 채택한건 비용의 문제가 제일 크다. master/slave 구조로 젠킨스를 운영한다는 것은 적어도 2 * (Number of Jenkins Instance) GB 만큼의 RAM이 필요한데, 이를 계속 쓰지도 않을거면서 클라우드에 올려두는 것의 비용 낭비가 심하다
세번쨰 근거를 보충 설명하겠습니다. Jenkins on k8s는 master만 쿠버네티스에 배포해두면 빌드가 유발되었을 때 slave를 동적으로 파드의 형태로 배포해서 거기서 빌드를 수행하기 때문에 이전에 설명드렸던 master/slave의 구조가 master는 배포된 jenkins pod, slave는 빌드 유발에 의해 배포되는 동적 jenkins slave pod가 역할을 담당해주므로 성립합니다.
따라서 평시에 빌드 대기가 0인 상태에서는 jenkins master agent만 쿠버네티스 위에서 동작하므로 빌드 리소스의 양을 획기적으로 줄일수있는 효과를 불러옵니다.
또한 며칠간 Jenkins를 모니터링 해본 결과, Jenkins master는 평시 500~1000MB 사이의 메모리 점유를 유지하며, 빌드 유발시 할당되는 파드는 대략 1~2gb 사이의 크기로 할당되는것을 확인하였습니다. 물론, 빌드 환경에 따라서 동적 할당되는 파드의 크기는 달라질 수 있습니다.
2. Jenkins on k8s는 어떻게 배포하죠?
해당 부분은 깃허브 링크로 대체하겠습니다. 더욱 자세한 내용은 Install Jenkins on Kubernetes를 참고해주세요.
https://github.com/doccilabs/kubernetes-utils/tree/main/jenkins
3. Jenkins를 배포하면서 겪은 어려움들
사실 여러개가 있겠지만, 대표적인 몇개만 언급하고 지나가겠습니다.
1) Helm Chart로 젠킨스를 배포하면 오류가 발생하는 현상
사실 젠킨스 공식문서 의 설명에서 리소스를 정의해서 배포하는 방법에 이어서 Helm Chart를 이용해서 배포하는 방법또한 소개를 하고있습니다.
하지만 Helm Chart를 이용해서 Jenkins를 Kubernetes 상에 배포를 해보면 Jenkins Pod가 CrashLoopBackOff에 빠지는 현상을 확인할 수 있습니다.
Cache miss for: update-center-2.332.3. Unable to retrieve JSON from https://updates.jenkins.io/update-center.json?version=2.332.3
해당 오류는 젠킨스가 plugin 업데이트 요청을 수행하는 사이트와 커넥션을 맺지 못하기 때문에 벌어지는 현상인데, 보안그룹을 아무리 열어봐도 해결이 되지 않았던 문제였기 때문에 저는 Helm Chart로 설치하는 방법 대신에 Kubernetes 리소스를 직접 정의하는 방식으로 사용하였습니다.
2) 배포한 Jenkins master와 Kubernetes 클러스터를 연결해야하더라
말 그대로, 젠킨스 마스터와 젠킨스를 빌드한 쿠버네티스 클러스터와 연결을 해야합니다. 이 과정에서 설정해줘야할 것들이 상당히 많은데요, 과정을 하나하나 설명해드리겠습니다.
- Jenkins에 Kubernetes 플러그인을 설치합니다
- Jenkins 설정 > 노드 관리 > Configure Clouds > Add a new cloud를 클릭합니다
- Kubernetes URL에는 본인의 쿠버네티스 클라우드의 URL을 입력합니다. (저의 경우는 EKS를 사용중이므로 EKS 클러스터 주소를 입력했습니다)
- Kubernetes Namespace에는 Jenkins를 배포한 namespace를 입력합니다.
- Jenkins URL은 본인의 Jenkins master 접속 주소, 그리고 Jenkins tunnel에는 jenkins master의 50000번 포트에 대응되는 ClusterIP를 대응시킵니다.
특히나 jenkins tunnel을 설정하는게 중요한데요, 이를 설정하지 않으면 jenkins master 호스트의 50000번 포트에 대해서 빌드 요청을 날리기 때문에 connection timeout이 발생하는 현상을 구경할 수 있습니다.
3) 파드간 통신을 위해서는 Terraform 으로 EKS를 설정한 경우 추가해야하는 보안그룹 규칙이 존재합니다
우선 파드간 통신, 그리고 서비스간 통신을 수행하기 위해서는 EKS에 추가해야할 보안그룹 규칙이 2개가 존재합니다. 다름 아닌 kube-dns에서 dns를 가져오기 위해 열어야하는 컨트롤 플레인에 대한 보안그룹 규칙, 그리고 Kubernetes 서비스에서 사용하는 ip 대역에 대한 보안그룹 규칙입니다.
- 컨트롤 플레인과 워커노드와의 통신을 위해서라면 VPC에 할당한 cidr_block에 대해서 inbound/outbound 규칙을 열어야합니다
- 그리고 kube-dns에서 DNS를 가져와서 ip로 치환하게 되면 service의 clusterIP로 치환이 되기 때문에 Kubernetes가 사용중인 ip 대역에 대해서도 inbound, outbound를 열어야합니다. (일반적으로 10.0.0.0/8 대역을 쿠버네티스 서비스가 사용합니다)
4) 무엇보다도, service.yaml 에서 50000번 포트도 열려있어야합니다
이 내용은 젠킨스 공식문서의 예제에는 반영이 안되어있기 때문에 따로 설명을 드리려고합니다. 물론 저도 이거 때문에 고생을 꽤나 하기도 했구요.
이는 위에서 제공한 깃허브 링크에는 미리 반영이 되어있으나, JNLP는 50000번 포트에 대해서 열려있기 때문에 Service에서 50000번 포트를 열어두지 않으면 빌드를 시작할 때 jenkins master가 jenkins slave agent에 요청을 전달하지 못합니다.
4. 그럼 저게 장점만 있느냐?
사실 해당 방법은 단점또한 명확하게 존재합니다. 제가 Jenkins on k8s를 운영하면서 느낀 단점은 대표적으로 2가지입니다.
- 우선 Jenkins Pipeline 정의 방법이 기존의 방식과 사뭇 다릅니다. Jenkins on k8s는 slave를 상시 운영하는 대신에 slave를 동적 파드로 생성하는 방식이다보니 파드 할당시 빌드 환경을 정의해야하는데요, 이를 podTemplate의 형태로 구현하기 때문에 처음 접하는 분들은 많이 생소합니다.
- 무엇보다 파이프라인 작성에 있어서 참고할만한 소스가 많이 없습니다.
Jenkinsfile 예시도 보여드리고 싶었으나, 아직까지 jenkins on k8s에서 사용하는 Jenkinsfile도 많이 미흡한 상태이기 때문에 더욱 성숙된 파이프라인을 구성할 수 있을 때 Jenkinsfile도 추가 업로드하도록 하겠습니다.
이상으로 글을 마치겠습니다.
'devOps' 카테고리의 다른 글
서비스 장애 잘 이해하고 대비하기 (0) | 2024.07.06 |
---|---|
Redis OOM 장애 (0) | 2024.06.25 |
Docker File System (Overlay2) (0) | 2023.08.06 |
Kubernetes 워커노드의 OOM에 의한 클러스터 장애 (0) | 2023.04.11 |
내가 쿠버네티스 설정하며 겪은 삽질들 (alb-controller, jenkins, monitoring) (0) | 2023.02.27 |