[DevOps] Jenkins 분산 빌드 아키텍처와 kubernetes

2025. 11. 16. 13:45·공부/DevOps
728x90

 

https://www.jenkins.io/

Jenkins는 소프트웨어 빌드, 테스트 또는 배포와 관련된 모든 종류의 작업을 자동화하는 데 사용할 수 있는 오픈 소스 자동화 서버입니다.

 

Jenkins는 기본 시스템 패키지, Docker로 설치할 수 있고 JRE가 설치된 컴퓨터에서 독립적으로 실행할 수도 있습니다.

 

먼저 Jenkins의 아키텍처 먼저 알아봅시다.

 

Jenkins 분산 빌드 아키텍처

jenkins controller는 빌드 환경을 관리하고 자체적으로 리소스를 사용하여 빌드를 수행할 수 있습니다.

 

하지만 이렇게 jenkins controller만 사용하게 된다면 다음과 같은 단점들이 존재합니다.

  • 부하가 증가하게 될 경우 scale up을 하는 동안 jenkins의 작업이 중단됩니다.
  • jenkins 사용자는 controller의 모든 권한을 갖게 됩니다.

즉, 확장성이 떨어지고 사용자에게 너무 많은 권한을 주기 때문에 jenkins는 분산 빌드 아키텍처를 제공합니다.

 

분산 빌드 아키텍처는 controller와 agent로 나뉘는데 각자 다음과 같은 역할을 담당합니다.

이름 역할
controller HTTP 요청 처리 및 빌드 환경 관리
agent 실제 빌드 작업 수행

https://growth-coder.tistory.com/

분산 빌드 아키텍처를 적용하면 다음과 같은 장점이 있습니다.

  • Agent를 추가해 연결하면 쉽게 확장이 가능합니다.
  • 다양한 OS를 사용하는 Agent를 연결할 수 있습니다.
  • Agent에게는 최소 권한을 줄 수 있습니다.

분산 빌드 아키텍처를 적용하려면 Controller와 Agent가 서로 통신할 수 있어야 합니다.

 

Controller-Agent 통신

통신 방식에는 ssh connector 방식과 in bound connector 방식이 있습니다.

 

  • ssh connector
    • ssh를 통해 controller와 agent가 통신합니다.
    • controller의 공개 키가 agent의 인증 키 집합에 포함되어야 합니다.
  • in bound connector
    • JNLP를 통해 agent를 실행합니다.
    • 수동으로 실행한다면 agent를 중앙에서 관리할 수 없지만 방화벽 내부의 agent가 방화벽 외부의 controller에 연결할 때 유용합니다.
    • 수동으로 실행한 뒤 서비스로 등록하면 자동으로 재시작되게 만들 수 있습니다.
  • in bound HTTP connector
    • in bound connector와 유사하지만 agent가 headless로 실행되고 HTTP(s)를 통해 터널링 가능합니다.

JNLP란?

JNLP(Java Network Launch Protocol)은 웹에서 java application을 실행할 수 있는 프로토콜입니다.

 

또한, JWS(Java Web Start)는 JNLP를 사용하여 클라이언트 시스템의 웹 브라우저에서 한 번의 클릭으로 Java EE 애플리케이션 클라이언트를 원격 클라이언트 시스템에 배치할 수 있게 해주는 기술입니다

 

https://askmedawaa.wordpress.com/2021/03/24/using-java-web-start-with-oracle-e-business-suite/

 

사용자가 웹 브라우저에서 jnlp 파일을 클릭하면 JNLP 프로토콜을 통해 JAR 파일과 리소스를 웹 서버로부터 다운로드 받고 JVM 위에서 애플리케이션을 실행하게 됩니다.

 

이러한 JWS는 Java 11에서 제거되었지만 Jenkins에서는 여전히 JNLP 프로토콜을 활용한 통신은 제공하고 있습니다.

 

k8s와 kubernetes를 연동하면 미리 정의해둔 pod template을 보고 Agent를 pod로 실행합니다.

 

이 때, 정의하지 않아도 jnlp contianer도 pod에 함께 포함되어 Controller와 Agent 사이 통신을 담당하게 됩니다.

Jenkins와 kubernetes

Jenkins 분산 빌드 아키텍처의 장점은 확장성입니다.

 

자유롭게 agent를 추가하여 부하에 대응할 수 있습니다.

 

물리적인 서버를 Jenkins Agent로 사용하여 controller와 연결하는 방식이 전통적인 방식입니다.

 

그런데 k8s와 같은 container orchestrator와 함께 사용하면 확장성을 높일 수 있습니다.

 

Jenkins와 k8s를 함께 사용할 때의 장점은 다음과 같습니다.

  • 자동 복구가 가능합니다.
  • 빌드를 병렬로 수행할 수 있습니다.
  • 부하를 균등하게 분배할 수 있습니다.

빌드를 시작할 때, 동적으로 k8s 안에 pod를 띄우고 이 안에서 빌드를 수행하는 방식입니다.

 

https://www.betsol.com/blog/devops-using-jenkins-docker-and-kubernetes/

jenkins pipeline을 실행하는 과정은 다음과 같습니다.

https://blog.voidmainvoid.net/140

Jenkins와 kubernetes 연동

기본 개념에 대해 모두 이해했다면 실습을 통해 Jenkins와 kubernetes를 연동해보겠습니다.

저는 local에서 master VM과 worker VM 한 대를 띄워서 k8s cluster를 구축했습니다.
로컬에서 minikube를 통해 k8s cluster를 구축했다면 진행 과정이 약간 다를 수 있습니다.
저는 Jenkins controller 자체를 k8s의 Deployment로 배포하였습니다.
k8s cluster 외부에 Jenkins를 배포하였다면 cluster 인증 정보를 입력하는 과정이 약간 다를 수 있습니다.

 

다음은 Jenkins controller를 배포하기 위한 Deployment와 NodePort Service입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins-deployment
  namespace: jenkins
  labels:
    app: jenkins
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      serviceAccountName: jenkins
      securityContext:
        runAsUser: 0           # 전체 Pod에서 root로 실행
        runAsGroup: 0
      containers:
      - name: jenkins
        image: jenkins/jenkins:lts
        ports:
        - containerPort: 8080
        - containerPort: 50000
        volumeMounts:
        - name: jenkins-home
          mountPath: /var/jenkins_home
      volumes:
      - name: jenkins-home
        hostPath:
          path: /home/[유저 이름]/data
          type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: jenkins-service
  namespace: jenkins
spec:
  type: NodePort
  selector:
    app: jenkins
  ports:
  - name: http
    port: 8080
    targetPort: 8080
    nodePort: 30080
  - name: jnlp
    port: 50000
    targetPort: 50000
    nodePort: 30050

 

몇 가지 중요한 정보를 알아봅시다.

  • security context
    • 중간에 security context의 runAsUser와 runAsGroup의 id를 0번, root로 설정하는 모습을 확인할 수 있습니다
    • jenkins 같은 경우 container를 띄우면서 내부 디렉토리에 write 연산을 수행하게 됩니다.
    • 그런데 directory에 대한 write 권한이 허용되지 않은 경우 실행에 실패할 수 있기 때문에 root로 실행하는 설정을 해두었습니다.
  • volume mount
    • 저는 worker node를 하나만 사용하기 때문에 host mount 방식을 사용했습니다.
    • volume host path는 worker node 환경에 맞게 적절히 변경해주시면 됩니다.

이제 위 manifest를 k8s에 적용하고 <node ip 주소 : 30080>으로 접속하면 admin password를 입력하라고 합니다.

 

초기 admin 계정의 비밀번호는 /var/jenkins_home/secrets/initialAdminPassword에 존재합니다.

 

다음 명령어로 비밀번호를 구합니다.

kubectl exec [jenkins pod 이름] -n jenkins -- cat /var/jenkins_home/secrets/initialAdminPassword

 

로그인에 성공하면 다음과 같은 화면을 볼 수 있습니다.

 

먼저 Jenkins Agent를 kubernetes에서 실행하기 위한 kubernetes plugin을 설치합니다.

 

Jenkins 관리 -> Plugins에서 kubernetes를 검색하고 설치하면 됩니다.

 

설치가 완료되면 자동 재시작 버튼이 있는데 재시작을 해야 plugin 설치가 완료됩니다.

 

Jenkins 관리 -> clouds -> New Cloud에서 kubernetes type을 선택하고 생성합니다.

 

다음 정보의 입력이 필요합니다.

  • Jenkins controller가 kubernetes에 접근하기 위한 정보
  • Jenkins Agent가 Jenkins Controller와 통신하기 위한 정보

이제 다음과 같은 정보를 준비하면 됩니다.

  • kubernetes URL: kubernetes API server의 주소
  • kubernetes server certificate key: X.509 PEM 인코딩 인증서 또는 base64 인코딩 인증서
  • kubernetes namespace: agent 배치 namespace
  • Credentials: k8s 사용 권한
  • Jenkins URL: Jenkins Controller server의 주소
  • Jenkins: tunnel: Jenkins에 직접 접근을 대체할 주소

kubernetes URL은 다음 명령어로 구할 수 있습니다.

kubectl cluster-info

 

아래처럼 API server의 주소를 구할 수 있습니다. (https://192.168.35.73:6443)

Kubernetes control plane is running at https://192.168.35.73:6443
CoreDNS is running at https://192.168.35.73:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

 

참고로 현재 Jenkins Controller가 k8s clsuter 내부에 배포되었기 때문에 kubernetes.default.svc.cluster.local라는 주소를 사용해도 됩니다.

 

만약 Jenkins Controller가 k8s cluster 외부에 배포되었다면 위에서 구한 주소를 입력하면 됩니다.

 

kubernetes server certificate key는 다음 명령어로 구할 수 있습니다.

kubectl config view

 

아래에서 cluster.certificate-authority의 값이 kubernetes server certificate key입니다.

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://192.168.35.73:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
users:
- name: kubernetes-admin
  user:
    client-certificate-data: DATA+OMITTED
    client-key-data: DATA+OMITTED

 

지금은 데이터들이 DATA+OMITTED로 나와 있는데 이는 config를 볼 때 --raw 옵션을 붙여주면 확인할 수 있습니다.

kubectl config view --raw

 

만약 값이 아닌 주소가 입력되어 있다면 해당 주소에서 인증서를 확인하고 base64 인코딩이 되어 있지 않다면 인코딩을 수행하면 됩니다.

 

kubernetes namespace는 Agent를 실행할 k8s의 namespace를 의미합니다. Jenkins Controller를 jenkins namespace에 생성했으니까 Agent도 같은 namespace에 생성하겠습니다.

 

Credentials은 k8s API를 사용하기 위한 자격 증명입니다. Jenkins controller가 k8s cluster 내부에 배포되어 있다면 작성하지 않아도 되지만 외부에 존재한다면 작성해야 합니다.

 

지금은 cluster 내부에 jenkins controller가 배포되어 있기 때문에 하지 않아도 되지만 그래도 kubeconfig 파일을 사용해서 credentials를 등록해보겠습니다.

 

kubeconfig 파일은 certificate key를 구할 때 사용했던 명령어로 구할 수 있습니다.

kubectl config view --raw

 

 

만약 실제 값이 아닌 주소가 담겨 있다면 해당 값을 직접 붙여넣으셔야 성공적으로 인증할 수 있습니다.

 

credentials의 +ADD 버튼을 통해 생성합니다.

 

 

이제 다음과 같이 Jenkins가 k8s에 접근하기 위한 정보를 모두 입력했습니다.

Test Connection 버튼을 눌러 "Connected to Kubernetes" 메시지가 출력되었다면 연결에 성공한 것입니다.

 

다음은 Agent가 Controller에게 접근하기 위한 정보입니다.

 

Jenins URL에는 현재 Jenkins UI의 host + port를 입력하면 됩니다.

 

Jenkins tunnel에는 protocol을 제외하고 host+port를 입력하면 됩니다.

 

중간에 disable https certificate check 같은 경우 self signed 인증서를 사용하고 있을 때 체크해줘야 합니다.

 

그러나 저는 Jenkins controller를 k8s cluster 내부에 배포했기 때문에 k8s CA 인증서를 신뢰해서 체크해주지 않아도 됩니다.

 

다음은 Agent가 Jenkins와 통신하기 위한 정보입니다. Jenkins tunnel 같은 경우 Jenkins URL과 같은 host + port를 입력하면 되는데 port의 기본 값은 50000입니다.

 

저는 NodePort 서비스로 30050 포트를 50000 포트로 연결을 했기 때문에 30050을 입력해주겠습니다.

 

cluster 생성을 완료했습니다.

 

이제 간단하게 CI 파이프라인을 작성해보면서 Agent들이 제대로 생성되고 있는지 확인하면 됩니다.

 

CI 파이프라인 작성

Pipeline plugin을 설치합니다.

 

New Item에서 Pipeline을 선택합니다.

 

다음 파이프라인 코드를 작성합니다. 간단하게 프로젝트 클론 후 maven으로 빌드와 테스트를 진행하는 파이프라인입니다.

pipeline {
    agent {
        kubernetes {
            yaml """
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: git
    image: alpine/git:latest
    command:
    - cat
    tty: true
  - name: maven
    image: maven:3.9.9-eclipse-temurin-17
    command:
    - cat
    tty: true
"""
        }
    }
    
    stages {
        stage('Clone Repository') {
            steps {
                container('git') {
                    sh 'git clone https://github.com/spring-petclinic/spring-framework-petclinic.git .'
                }
            }
        }
        
        stage('Build') {
            steps {
                container('maven') {
                    sh 'mvn clean package -DskipTests'
                }
            }
        }
        
        stage('Test') {
            steps {
                container('maven') {
                    sh 'mvn test'
                }
            }
        }
    }
}

 

지금 실행을 눌러 성공한다면 성공적으로 Jenkins와 k8s를 연동한 것입니다.

 

 

 

Jenkins Agent를 pod로 실행하는 원리

파이프라인을 실행할 때 로그를 보면 아래와 같이 pod의 manifest를 확인할 수 있습니다.

 

파이프라인에 정의해 둔 pod template을 기반으로 pod manifest를 생성하고 적용하는 것입니다.

---
apiVersion: "v1"
kind: "Pod"
metadata:
  annotations:
    kubernetes.jenkins.io/last-refresh: "1763265042559"
    buildUrl: "http://192.168.35.52:30080/job/test/11/"
    runUrl: "job/test/11/"
  labels:
    jenkins: "slave"
    jenkins/label-digest: "bb9dc6c737d64f21044ecb4dde4f90bfd6996c1e"
    jenkins/label: "test_11-8fl9l"
    kubernetes.jenkins.io/controller: "http___192_168_35_52_30080x"
  name: "test-11-8fl9l-dm97q-8gn8d"
  namespace: "jenkins"
spec:
  containers:
  - command:
    - "cat"
    image: "alpine/git:latest"
    name: "git"
    tty: true
    volumeMounts:
    - mountPath: "/home/jenkins/agent"
      name: "workspace-volume"
      readOnly: false
  - command:
    - "cat"
    image: "maven:3.9.9-eclipse-temurin-17"
    name: "maven"
    tty: true
    volumeMounts:
    - mountPath: "/home/jenkins/agent"
      name: "workspace-volume"
      readOnly: false
  - env:
    - name: "JENKINS_SECRET"
      value: "********"
    - name: "JENKINS_TUNNEL"
      value: "192.168.35.52:30050"
    - name: "JENKINS_AGENT_NAME"
      value: "test-11-8fl9l-dm97q-8gn8d"
    - name: "REMOTING_OPTS"
      value: "-noReconnectAfter 1d"
    - name: "JENKINS_NAME"
      value: "test-11-8fl9l-dm97q-8gn8d"
    - name: "JENKINS_AGENT_WORKDIR"
      value: "/home/jenkins/agent"
    - name: "JENKINS_URL"
      value: "http://192.168.35.52:30080/"
    image: "jenkins/inbound-agent:3345.v03dee9b_f88fc-1"
    name: "jnlp"
    resources:
      requests:
        memory: "256Mi"
        cpu: "100m"
    volumeMounts:
    - mountPath: "/home/jenkins/agent"
      name: "workspace-volume"
      readOnly: false
  nodeSelector:
    kubernetes.io/os: "linux"
  restartPolicy: "Never"
  volumes:
  - emptyDir:
      medium: ""
    name: "workspace-volume"

 

다음과 같은 특징이 있다는 것을 확인할 수 있습니다.

  • jnlp 컨테이너
  • volume mount

jnlp 컨테이너는 jnlp라는 프로토콜을 통해 Agent와 Controller 사이 통신을 담당합니다.

 

pod template에 정의하지 않아도 자동으로 생성됩니다.

 

또한 container 간 데이터 공유를 위해 Empty Dir Volume을 통해 pod가 실행되는 동안만 container 간 데이터를 공유합니다.

https://growth-coder.tistory.com/

 

Jenkins pipeline을 보면 clone을 git container에서, 빌드는 maven container에서 하는 모습을 확인할 수 있습니다.

 

이 empty dir volume을 mount 했기 때문에 container가 종료되더라도 이전 작업이 사라지지 않는 것입니다.

https://growth-coder.tistory.com/

Trouble Shooting

jenkins와 k8s를 연동하면서 발생할 수 있는 문제 상황들과 해결 방법들을 정리해보았습니다.

 

pod is offline

pod는 생성되었지만 offline인 상황은 Jenkins Controller와 Jenkins Agent가 통신하지 못 하는 상황입니다.

 

저 pod 부분에는 링크가 걸려있어 들어가보면 로컬 cli로 수동으로 연결을 시도할 수 있습니다.

위 명령어느 Jenkins Agent를 생성하는 명령어입니다.

 

Jenkins Controller로부터 직접 Agent를 실행하는 jar 파일을 가져와서 Agent를 pod로 실행합니다.

 

명령어를 그대로 실행하면 Agent 즉, pod를 생성하는 모습을 확인할 수 있습니다.

 

그런데 pod가 생성되자마자 종료가 되는 경우가 있습니다.

상황 정리를 하면 다음과 같습니다.

  • Jenkins Agent가 자동으로 실행되지 않습니다.
  • pod 생성은 되지만 종료됩니다.

이 두 가지 상황을 고려하면 Jenkins Controller와 Jenkins Agent가 통신하지 못 해 발생하는 문제입니다.

 

Cloud config에서 Jenkins tunnel이 누락되어 있는지 확인합니다.

 

tunnel port의 기본 값은 50000이며 저는 NodePort로 30050을 container 내부 50000 포트와 연결을 해두어서 30050 포트를 사용했습니다.

 

해당 포트가 inbound traffic을 허용하는지도 확인합니다.

 

write 권한 에러

java.nio.file.AccessDeniedException이나 permission denied 처럼 권한 에러는 대부분 디렉토리에 쓰기 권한이 없을 때 발생하는 에러입니다.

 

앞서 Jenkins Pipeline은 Empty Dir Volume을 각 container마다 mount하여 데이터를 공유한다고 배웠습니다.

https://growth-coder.tistory.com/

그런데 여기서 container간 user id가 다를 경우 문제가 발생할 수 있습니다.

 

대부분 image를 실행할 때는 uid가 0, 즉 root가 default라서 큰 문제는 없지만 간혹 user id가 다를 수 있습니다.

 

예를 들어 container 1은 uid 0으로 실행되고 container 2는 uid가 1000으로 실행되었다고 합시다.

 

container 1이 생성한 디렉토리나 파일의 소유자는 root가 됩니다.

 

그런데 이 상황에서 directory의 소유자나 그룹이 아닌 사람에게는 write 연산을 허용하지 않는다면 어떨까요?

 

다음 container 2는 uid가 1000인 일반 유저로 실행되기 때문에 해당 디렉토리에 write 연산을 할 수 없게 됩니다.

 

이럴 때는 pod의 security context의 runAsUser를 활용하여 container 실행 유저를 특정 값으로 고정하여 해결할 수 있습니다.

 

Pipeline을 실행할 때 외에도 Jenkins를 설치할 때도 이러한 오류가 발생할 수 있으니 주의하시기 바랍니다.

마무리

이렇게 Jenkins와 k8s를 연동하는 방법에 대해 알아보았습니다.

 

설정을 하면서 여러 가지 문제가 발생했는데 이로 인해 Jenkins와 k8s가 연동되는 구조에 대해 이해할 수 있었던 것 같습니다.

 

Jenkins와 k8s를 연동하시려는 분들께 도움이 되었으면 좋겠습니다.

 

긴 글 읽어주셔서 감사합니다.

728x90

'공부 > DevOps' 카테고리의 다른 글

[ArgoCD] Helm-Charts를 통해 Harbor를 GitOps 방식으로 배포  (0) 2025.11.10
[k8s] argoCD 배포 총 정리  (0) 2025.11.09
'공부/DevOps' 카테고리의 다른 글
  • [ArgoCD] Helm-Charts를 통해 Harbor를 GitOps 방식으로 배포
  • [k8s] argoCD 배포 총 정리
웅대
웅대
알고리즘과 백엔드를 중심으로 열심히 공부 중입니다! 같이 소통하며 공부해요!
    250x250
  • 웅대
    웅대 개발 블로그
    웅대
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 백준 알고리즘
        • dp
        • 문자열
        • 정렬
        • 스택
        • 브루트 포스
        • 이진 탐색
        • 정리
        • 우선순위 큐
        • 자료구조
        • 그래프
        • 기타
        • 그리디
      • 컴퓨터 언어
        • Kotlin
        • Python
        • C#
      • 공부
        • Database
        • Android Studio
        • Algorithm
        • 컴퓨터 구조론
        • Spring
        • lombok
        • AWS
        • Network
        • OS
        • Git & GitHub
        • AI
        • Computer Vision
        • 보안
        • Nginx
        • 프론트
        • express
        • GCP
        • grokking concurrency
        • DevOps
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    influxDB CLI
    Merge
    스프링 OAuth2
    파이썬
    ci/cd
    Vector Store
    다익스트라
    nn.RNN
    AWS Lambda
    푸쉬 알람
    code tree
    parametric search
    bfs
    openvidu 배포
    binary search
    ChatPromptTemplate
    RNN
    embedding
    스택
    codetree
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
웅대
[DevOps] Jenkins 분산 빌드 아키텍처와 kubernetes
상단으로

티스토리툴바