Post

젠킨스 LTS 세 단계를 한 번에 — 블루/그린으로 2분 만에 넘어간 보안 패치 업그레이드

쿠버네티스로 옮긴 뒤 젠킨스 버전 업그레이드는 늘 “새로 구축”이었다. 할 때마다 번거롭고, 할 때마다 절차가 달랐다. 2026-10-28 보안 공지로 또 올려야 할 일이 생겼고, 이번에는 끝을 내고 싶었다. VM에서 운영하던 시절처럼 간단하게, 정해진 매뉴얼대로, 팀원 누구나 할 수 있게. 결과적으로 2.541.2에서 2.580.2로 올리면서 빌드가 멈춘 시간은 약 2분이었고, 그 과정을 다음에도 그대로 반복할 수 있는 절차로 정리했다. 이 글은 그 과정과 각 결정의 이유를 정리한 것이다.

1. 배경: 업그레이드가 곧 재구축이었다

1.1 VM 시절에는 간단했다

VM에서 젠킨스를 운영할 때 버전 업그레이드는 단순한 일이었다. 젠킨스 패키지(war)를 바꾸고 재시작하면 끝났다. 무엇이 바뀌는지, 어디를 보면 되는지가 한눈에 보였다.

1.2 쿠버네티스로 옮긴 뒤에는 매번 새로 구축했다

젠킨스를 쿠버네티스 위에 다시 세운 뒤로 사정이 달라졌다. 젠킨스 하나가 여러 층으로 나뉘어 있었다.

  • 공식 헬름 차트와 values
  • 공식 이미지 위에 플러그인을 얹은 컨트롤러 커스텀 이미지
  • 이전 커스텀 이미지 위에 도구를 쌓아온 에이전트 커스텀 이미지
  • JENKINS_HOME PVC와 에이전트 공유 PVC 버전 하나를 올리려 해도 이 층들이 서로 어떻게 엮여 있는지부터 다시 따라가야 했다. 그래서 지금까지는 업그레이드가 있을 때마다 사실상 새로 구축하는 방식으로 진행했다. 매번 시간이 많이 들었고, 그때그때 상황에 맞춰 하다 보니 절차가 매번 달랐다. 그 절차는 작업한 사람의 머릿속에만 남았다.

1.3 이번에 하고 싶었던 것

젠킨스 보안 공지는 앞으로도 계속 나온다. 그때마다 이렇게 할 수는 없었다. 그래서 이번 업그레이드의 목표를 버전을 올리는 것 자체보다 업그레이드하는 방법을 정리하는 것에 두었다.

목표이유
매번 같은 절차로 한다그때그때 다른 방식으로 하면 실수가 반복되고, 소요 시간도 예측할 수 없다
팀원 누구나 매뉴얼을 보고 따라 할 수 있다특정 사람만 할 수 있는 작업은 그 사람이 없으면 멈춘다
운영 중인 젠킨스는 최대한 건드리지 않는다젠킨스는 팀 전체의 배포 경로라서, 멈추는 시간이 곧 팀의 대기 시간이다
언제든 되돌릴 수 있다되돌릴 수 있어야 업그레이드를 미루지 않고 할 수 있다
구성은 코드로 남긴다UI에서 손으로 바꾼 설정은 다음 업그레이드 때 다시 찾아내야 한다

새 도구를 들이기보다, 지금 있는 헬름 차트·이미지·PVC 구조는 그대로 두고 절차와 경계만 정리하기로 했다. 이미 돌아가는 구조를 갈아엎으면 팀원들이 익혀야 할 것만 늘어나기 때문이다.

2. 계기: LTS 세 라인을 건너뛰어야 했다

Jenkins Security Advisory 2026-10-28은 LTS 2.580.1 이하를 대상으로 High 등급 취약점을 여럿 공개했다. 수정 버전은 LTS 2.580.2다.

운영 젠킨스는 2.541.2였다. 패치 버전까지 가려면 LTS 라인 세 개를 한 번에 건너야 했다.

1
2.541 → 2.555 → 2.568 → 2.580

중간에 굵직한 변화가 두 개 있었다.

  • 2.555.1: Java 21이 필수가 됐다.
  • 2.580.1: 역직렬화 보안이 강화됐고, 일부 플러그인이 번들에서 빠졌다. 코어 버전만 바꾸면 끝나는 업그레이드가 아니었다. 플러그인과 에이전트까지 영향받을 수 있는 업그레이드였다.

환경

항목내용
플랫폼네이버 클라우드 쿠버네티스(NKS)
배포공식 헬름 차트 jenkins/jenkins
컨트롤러 이미지공식 이미지 + 로고·플러그인을 얹은 커스텀 이미지 (사설 레지스트리)
에이전트 이미지이전 커스텀 이미지 위에 kubectl, helm, vault, node 같은 도구를 쌓아온 커스텀 이미지
스토리지NAS CSI StorageClass. JENKINS_HOME과 에이전트 공유 볼륨 모두 RWX
접속Service type: LoadBalancer에 인증서를 annotation으로 연결하고, DNS로 도메인을 붙임

이 글에 등장하는 도메인, 레지스트리, 클러스터 등 자원 명은 실제 자원 명이 아니라 임의로 재구성한 예시다. 젠킨스 리소스 이름은 Jenkins on Kubernetes 재구축 글과 맞췄다.

3. 처음에는 제자리 업그레이드로 시작했다

이번에는 새로 구축하는 대신, 가장 간단해 보이는 방법부터 시도했다. values의 이미지 태그를 바꾸고 helm upgrade를 하는 방식이다. 테스트 젠킨스(2.568.1)로 먼저 해봤다. 그런데 한 단계 넘어갈 때마다 다른 문제가 나왔다.

증상원인
docker.io/jenkins:2.580.2 pull 실패차트는 tag를 지정하면 tagLabel(jdk21)을 무시한다. 접미사까지 tag에 써야 한다
docker.io/library/jenkins-agent pull 실패values의 registry가 주석 처리돼 있어서 기본값 docker.io가 붙었다
빌드에서 kubectl: not found에이전트 이미지를 공식 inbound-agent로 바꿨더니 그동안 쌓아온 도구가 전부 사라졌다

앞의 두 개는 값만 고치면 됐다. 문제는 세 번째였다.

에이전트 이미지는 공식 이미지에서 시작한 게 아니었다. 이전 커스텀 이미지 위에 도구를 계속 쌓아온 구조였다. FROM만 바꿔서 될 일이 아니고 처음부터 다시 쌓아야 했다. 컨트롤러도 비슷했다. 커스텀 이미지 위에 차트가 플러그인을 덮어쓰는 구조여서, 버전을 올렸을 때 무엇이 어디서 오는지 단번에 말할 수 없었다.

여기서 제자리 업그레이드의 진짜 위험이 보였다.

  • 실패하면 JENKINS_HOME의 플러그인과 설정은 이미 새 버전 기준으로 바뀌어 있다. 이미지 태그를 되돌리는 것만으로는 원래대로 돌아가지 않고, 백업 복원이 필요하다.
  • 컨트롤러와 에이전트를 같이 바꾸면, 문제가 생겼을 때 어느 쪽이 원인인지 가리기 어렵다.
  • 무엇보다 검증하는 동안 운영 젠킨스가 멈춰 있어야 한다.

    4. 블루/그린으로 바꾼 이유

방식을 바꿨다. 기존 젠킨스는 그대로 두고, 옆에 새 젠킨스를 띄운다. 데이터를 부어서 검증한 뒤 DNS만 바꾼다.

이 방식을 고른 이유는 네 가지다.

  1. 이미 검증된 방식이다. 깃랩을 이관하고 업그레이드할 때 같은 방식(원본은 보존하고 별도 인스턴스에 복원 → 검증 → 전환)을 써서 문제없이 넘어간 경험이 있다. 새 방법을 찾기보다 검증된 절차를 젠킨스에 옮기는 쪽을 택했다.
  2. 롤백이 “트래픽 되돌리기”로 끝난다. 기존 젠킨스를 한 번도 건드리지 않았으니 그대로 다시 켜면 된다.
  3. 운영 중에 검증할 수 있다. 새 젠킨스에서 플러그인, 권한, 크리덴셜, 빌드를 충분히 확인하는 동안 기존 젠킨스는 평소처럼 돈다. 빌드가 멈추는 시간은 마지막 데이터 복사와 DNS 전환뿐이다.
  4. 매번 같은 절차로 반복할 수 있다. “새 젠킨스를 띄운다 → 붓는다 → 확인한다 → 주소를 바꾼다”는 흐름은 버전이 무엇이든 같다. 바뀌는 건 이미지 태그와 이름 몇 개뿐이라 매뉴얼로 만들기 좋았다. 1장의 목표에 가장 잘 맞는 방식이었다. 에이전트는 이번 범위에서 뺐다. 이번 취약점은 컨트롤러(코어) 문제라서 컨트롤러만 바꾸면 패치가 끝나기 때문이다. 기존 에이전트는 이미 Java 21 이상이라 2.580.2의 최소 조건도 만족했다. 에이전트 이미지 재구성은 별도 과제로 분리했다.

5. 아키텍처

젠킨스 블루/그린 전환 구조 블루(기존)와 그린(신규)이 같은 네임스페이스에 release 이름만 달리해서 뜬다

구조를 정하면서 내린 결정과 그 이유는 다음과 같다.

같은 네임스페이스에 띄웠다. PVC는 네임스페이스를 넘어서 마운트할 수 없다. 에이전트 공유 볼륨(jenkins-n-agent-pvc)을 두 젠킨스가 같이 쓰려면 같은 네임스페이스여야 했다. 헬름은 리소스 이름에 release 이름을 붙이므로, release 이름만 다르면 StatefulSet, Service, Secret이 겹치지 않는다.

release 이름은 버전이 아니라 슬롯(blue/green)으로 지었다. 헬름 release 이름은 나중에 바꿀 수 없다. jenkins-n-2-580 같은 이름은 다음 업그레이드 때 의미가 틀어진다. 대신 슬롯 이름을 번갈아 쓴다. 이번에 그린으로 왔으니 다음에는 블루로 간다.

JENKINS_HOME용 PVC는 새로 만들었다. 같은 StorageClass를 쓰더라도 NAS CSI는 PVC마다 별도 NAS 볼륨을 만든다. 그래서 기존 디스크와 섞일 걱정이 없다. 한 가지 함정이 있었다. StorageClass가 WaitForFirstConsumer라서, PVC를 만들어도 그걸 쓰는 pod가 뜨기 전까지 Pending으로 남는다. 고장이 아니다.

접속 주소는 DNS 한 곳에서만 바꾼다. LB는 release마다 따로 생긴다. 사용자와 GitLab 웹훅은 도메인만 알고 있다. 그래서 도메인 레코드 값만 바꾸면 전환이 끝나고, 웹훅 설정은 하나도 손대지 않아도 된다.

6. 붓기: JENKINS_HOME을 어떻게 옮길까

새 젠킨스를 빈 상태로 띄우는 건 쉽다. 어려운 건 기존 데이터를 “붓는” 일이었다. 빌드 이력, 잡 설정, 크리덴셜, 사용자, 권한이 모두 JENKINS_HOME에 있다.

6.1 tar 스트림

새 PVC를 마운트한 임시 pod를 띄운다. 그다음 기존 컨트롤러 pod의 JENKINS_HOME을 tar 스트림으로 흘려 넣는다.

1
2
3
4
5
6
kubectl exec -n jenkins-n jenkins-n-0 -c jenkins -- sh -c '
  cd /var/jenkins_home &&
  find . -mindepth 1 -maxdepth 1 \
    ! -name workspace ! -name caches ! -name .cache ! -name .snapshot -print0 \
  | tar czf - --null -T -' \
| kubectl exec -i -n jenkins-n jenkins-restore -- tar xzf - -C /restore

이 방식을 고른 이유는 다음과 같다.

  • 기존 젠킨스는 읽기만 한다. 운영 중에 돌려도 영향이 없고, 실패하면 그냥 다시 돌리면 된다.
  • 임시 pod를 젠킨스 uid(runAsUser: 1000)로 띄워서, 파일 소유권을 따로 맞출 필요가 없다.
  • 임시 pod 이미지로는 새 컨트롤러 이미지를 그대로 썼다. tar가 들어 있고 사설 레지스트리에도 이미 있으니 이미지를 하나 더 관리할 필요가 없다. workspace, caches, .cache는 뺐다. 빌드하면 다시 만들어지는 작업물이라서다. 이렇게 하니 JENKINS_HOME 822MB가 97초 만에 옮겨졌다.

6.2 NAS가 만든 함정 두 개

처음 돌렸을 때 tar가 에러를 냈다.

1
2
tar: ./.snapshot: Read-only file system
tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted

NAS 볼륨 루트에는 읽기 전용 .snapshot 디렉토리가 있다. 그리고 볼륨 루트 디렉토리 자체는 권한을 바꿀 수 없다. tar czf - .로 묶으면 루트(.)까지 아카이브에 들어간다. 그러면 풀 때 루트 권한을 바꾸려다 실패한다.

그래서 find -mindepth 1 -maxdepth 1로 루트 안의 항목만 골라 묶고, .snapshot을 제외했다. --no-overwrite-dir 옵션도 시도해 봤지만 루트 문제는 해결되지 않았다.

6.3 엉뚱한 클러스터를 물 뻔했다

테스트 클러스터와 운영 클러스터의 네임스페이스 이름이 같았다. 작업 중에 컨텍스트가 운영으로 잡힌 채 테스트용 스크립트를 실행할 뻔했다. 그 뒤로 모든 스크립트 첫 줄에 이 검사를 넣었다.

1
2
EXPECTED_CONTEXT=prod-cluster
[ "$(kubectl config current-context)" = "$EXPECTED_CONTEXT" ] || { echo "컨텍스트가 $EXPECTED_CONTEXT 가 아님. 중단"; exit 1; }

스크립트 디렉토리도 restore-test/와 restore-prod/로 나눴다. 두 환경은 컨텍스트, 이미지 경로, pull secret 여부가 다르다. 파일 하나를 고쳐 쓰다 보면 언젠가 운영에 테스트 값이 들어간다.

6.4 첫 붓기와 최종 붓기

붓기는 두 번 한다.

 첫 붓기최종 붓기
시점그린을 처음 만들 때 (운영 중)전환 직전 (빌드 중지 후)
대상JENKINS_HOME 전체그 사이 쌓인 빌드 이력·설정
제외workspace, caches, .snapshot위 + plugins/
소요97초37초

최종 붓기에서 plugins/를 뺀 이유는 다음 장의 내용 때문이다.

7. 가장 많이 막힌 곳: 플러그인은 어디서 오는가

그린을 처음 띄웠을 때 기동에 실패했다.

1
2
Failed Loading plugin ...
No hudson.slaves.Cloud implementation found for kubernetes

이미지에는 새 버전 플러그인을 넣어뒀는데, 왜 옛 플러그인이 로드됐을까. 원인을 쫓다 보니 지금까지 잘못 알고 있던 사실이 드러났다.

플러그인 적용 경로 비교 왼쪽은 이전 운영의 실제 상태, 오른쪽은 지금의 구조

7.1 이미지에 구운 플러그인은 한 번도 적용된 적이 없었다

젠킨스 컨트롤러 플러그인이 들어오는 경로는 세 가지다.

  1. 이미지의 /usr/share/jenkins/ref/plugins: 기동할 때 젠킨스 스크립트가 JENKINS_HOME으로 복사한다. 단, JENKINS_HOME에 없는 플러그인만 복사한다.
  2. 차트의 installPlugins: init 컨테이너가 설치한다. 이때 이미지의 ref 플러그인 폴더 위에 빈 볼륨(emptyDir)을 마운트한다.
  3. UI(Plugin Manager): JENKINS_HOME에 바로 설치된다. 이전 운영은 installPlugins가 켜져 있었다. 그래서 이미지에 정성껏 구워둔 plugins.txt 121개는 빈 볼륨에 가려져서 한 번도 적용되지 않았다. 실제로 돌고 있던 건 PVC에 쌓인 102개였다. 차트가 설치한 것과 UI로 설치한 것이 섞여 있었다.

이 JENKINS_HOME을 그대로 그린에 부으니 이런 일이 생겼다. 이미지의 새 플러그인은 “이미 있는 플러그인은 덮어쓰지 않는다”는 규칙 때문에 들어가지 못했다. 옛 kubernetes 플러그인이 새 코어 위에서 로드에 실패했고, 그 플러그인에 의존하는 플러그인들이 연쇄로 실패했다.

7.2 정리한 방법

한 번만 플러그인 폴더를 비웠다. 차트의 overwritePlugins: true는 기동할 때마다 JENKINS_HOME의 plugins/를 전부 지운다. 이걸 켜고 한 번 재기동하자 이미지 기준으로 플러그인이 깨끗하게 깔렸다. 그 즉시 false로 되돌렸다. 계속 켜두면 UI로 설치한 플러그인이 재시작할 때마다 사라진다. 팀원들이 UI로 플러그인을 설치하는 일이 실제로 있어서 이건 받아들일 수 없었다.

values는 이렇게 고정했다.

1
2
3
4
5
6
controller:
  installPlugins: false        # 켜면 이미지 플러그인이 다시 가려진다
  overwritePlugins: false      # UI 설치 플러그인을 지우지 않는다
  containerEnv:
    - name: PLUGINS_FORCE_UPGRADE
      value: "true"            # 이미지 쪽이 더 높은 버전이면 JENKINS_HOME 플러그인을 올려준다

PLUGINS_FORCE_UPGRADE가 핵심이다. 이게 없으면 다음 업그레이드에서도 “이미 있으니 안 덮어씀” 문제가 똑같이 생긴다.

최종 붓기에서 plugins/를 뺐다. 그린에는 이미 새 버전 플러그인이 정리돼 있다. 여기에 블루의 옛 플러그인을 다시 부으면 처음 문제로 돌아간다.

7.3 플러그인 목록은 두 파일로 나눴다

1
2
3
4
5
6
7
FROM jenkins/jenkins:2.580.2-jdk25
 
COPY logos /usr/share/jenkins/ref/userContent/logos
COPY plugins.txt plugins-custom.txt /usr/share/jenkins/ref/
RUN grep -hv '^#' /usr/share/jenkins/ref/plugins.txt /usr/share/jenkins/ref/plugins-custom.txt \
      > /usr/share/jenkins/ref/plugins-all.txt && \
    jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins-all.txt --verbose
  • plugins.txt: jenkins-infra/docker-jenkins-lts의 목록을 손대지 않고 그대로 복사한다. 젠킨스 인프라팀이 자기들 운영 젠킨스용으로, LTS가 나올 때마다 호환 버전을 맞춰 커밋하는 목록이다. 버전 호환성 검증을 그들에게 맡기는 셈이다.
  • plugins-custom.txt: 인프라팀 목록에는 없지만 우리가 쓰는 플러그인 25개다. gitlab-plugin, role-strategy, uno-choice 같은 것들이다. 버전 없이 이름만 적었다. 그러면 빌드 시점에 해당 코어와 호환되는 최신 버전이 설치된다. 두 파일로 나눈 이유는 다음 업그레이드를 쉽게 하기 위해서다. plugins.txt는 cp 한 번으로 끝나고, 우리 목록은 섞이지 않고 남는다.

감수한 점도 있다. 인프라팀 목록에는 우리가 쓰지 않는 플러그인도 40여 개 들어 있어서 함께 설치된다. 모두 같은 시점의 호환 버전이라 동작에 문제는 없다. 목록을 직접 깎아서 관리하는 비용보다 이쪽이 싸다고 판단했다.

이미지 빌드 단계에서 jenkins-plugin-cli가 버전 충돌을 잡아준다. 운영에 띄우기 전에 호환성 문제를 발견하는 첫 관문이 생긴 셈이다. 최종 이미지에는 의존성을 포함해 151개 플러그인이 들어갔다.

UI로 설치하는 건 계속 허용한다. 대신 설치했으면 plugins-custom.txt에도 한 줄 추가하는 규칙을 팀에 공유했다. 그래야 다음 업그레이드 때 그 플러그인이 옛 버전으로 남지 않는다.

8. 143일 동안 숨어 있던 설정: markupFormatter

그린이 뜨고 나서 화면을 보니, 뷰 설명에 넣어둔 HTML(빨간 글씨, 표)이 태그째 글자로 보였다.

원인은 JCasC였다. 차트는 JCasC로 시스템 설정 일부를 관리하는데, markupFormatter의 기본값이 plainText다. 누군가 UI에서 HTML 포매터로 바꿔뒀지만, 재기동할 때마다 JCasC가 다시 plainText로 덮는다. 이전 운영은 143일 동안 재시작한 적이 없어서 UI에서 바꾼 값이 유지되는 것처럼 보였을 뿐이다.

values에 고정했다.

1
2
3
4
controller:
  markupFormatter:
    custom:
      disableSyntaxHighlighting: false

custom이라는 JCasC 심볼 이름은 문서에서 찾기 어려웠다. 그래서 /manage/configuration-as-code/viewExport(POST)로 현재 설정을 내보내서 실제 이름을 확인했다.

교훈은 이것이다. JCasC가 관리하는 항목은 UI에서 바꾼 값이 재시작 전까지만 살아 있다. 오래 재시작하지 않은 젠킨스일수록 이런 시한폭탄이 숨어 있을 수 있다. 블루/그린은 새 젠킨스를 “처음 기동”시키는 방식이라서, 이런 문제가 운영 전환 전에 드러난다는 장점도 있었다.

9. 전환 당일

전환 플로우 A 구간은 운영 중에 끝냈고, 빌드가 멈춘 건 B 구간뿐이다

9.1 순서와 이유

순서작업이유
5블루에서 Prepare for Shutdown젠킨스를 끄는 게 아니라 새 빌드만 막는다. 복사 중에 빌드 기록이 반쯤 쓰인 채로 넘어가는 걸 막는다
6그린 replicas 0실행 중인 젠킨스의 JENKINS_HOME에 덮어쓰면 파일이 충돌한다
7최종 붓기 (plugins/ 제외)그 사이 쌓인 빌드 이력만 옮긴다
8그린 replicas 1로그에서 fully up, 화면에서 블루의 마지막 빌드 기록을 확인한다
9DNS 전환그린 확인이 끝난 뒤에 바꾼다. 먼저 바꾸면 사용자가 준비 안 된 그린으로 들어온다
10블루 replicas 0삭제하지 않는다. release, PVC, LB는 롤백용으로 남긴다

9.2 블루를 지우지 않고 replicas 0으로 둔 이유

처음에는 “블루/그린이면 블루는 켜둔 채 대기해야 하는 거 아닌가”라고 생각했다. 하지만 두 젠킨스가 동시에 켜져 있으면 위험하다. DNS 캐시가 남은 사용자나 웹훅이 블루에서 빌드를 돌릴 수 있고, 그러면 빌드 이력이 두 갈래로 갈라진다. 게다가 둘은 에이전트 공유 볼륨을 같이 쓰기 때문에 같은 잡이 동시에 돌면 워크스페이스가 겹친다.

그래서 블루는 끄되 지우지 않았다. 롤백할 때는 replicas 1로 올리고 DNS만 되돌리면 된다. 이것도 대기 상태의 블루다.

9.3 당일에 겪은 것들

Prepare for Shutdown을 엉뚱한 쪽에 누를 뻔했다. DNS를 바꾸는 중에는 같은 도메인이 블루로 갈지 그린으로 갈지 알 수 없다. 블루에서 빌드를 막으려면 블루 LB 주소로 직접 접속해야 한다. 어느 쪽인지는 화면 오른쪽 아래의 젠킨스 버전으로 확인했다.

최종 붓기 중 스트림이 끊겼다. connection reset by peer가 났지만 다시 돌리니 성공했다. tar는 파일을 덮어쓰기 때문에 재실행해도 안전하다. 반복해서 끊긴다면 원본과 대상 PVC를 한 pod에 같이 붙여서 클러스터 안에서 복사하는 방법을 대안으로 준비해 뒀다. 원본은 readOnly로 마운트한다. JENKINS_HOME PVC가 RWX라서 가능하다.

DNS를 바꿨는데 반영이 안 됐다. 네이버 클라우드 Global DNS는 레코드를 수정한 뒤 “적용” 버튼까지 눌러야 반영된다. 그다음 로컬 DNS 캐시(ipconfig /flushdns, 브라우저 chrome://net-internals/#dns)도 비웠다.

9.4 롤백 경로

1
2
3
4
5
# 1) 블루를 먼저 올린다 (DNS보다 먼저 — 올라오기 전에 DNS를 돌리면 접속 대상이 없다)
kubectl scale statefulset jenkins-n -n jenkins-n --replicas=1
# 2) DNS 레코드를 기록해 둔 블루 LB 주소로 되돌리고 "적용"
# 3) 그린을 내린다
kubectl scale statefulset jenkins-n-green -n jenkins-n --replicas=0

롤백하면 전환 이후 그린에서 돈 빌드 이력은 블루에 없다. 그래서 전환 직후 몇 시간 안에 롤백 여부를 판단하기로 했다. 다행히 쓸 일은 없었다.

10. 표준화한 절차: 다음 업그레이드에서 할 일

이번 작업에서 가장 공을 들인 부분은 버전을 올린 것보다 다음에도 같은 방식으로 올릴 수 있게 정리한 것이다. 1장에서 세운 목표대로, 작업 내용을 저장소에 런북과 스크립트로 남겼다.

10.1 디렉토리를 “무엇을 띄우는가”와 “어떻게 옮기는가”로 나눴다

1
2
3
4
5
6
7
8
jenkins/
├── JENKINS-UPGRADE-RUNBOOK.md     # 처음 보는 사람이 따라 하는 매뉴얼
├── image/
│   ├── controller/                # 무엇을 띄우는가: Dockerfile, plugins.txt, plugins-custom.txt
│   └── agent/
└── addon/
    ├── restore-test/              # 어떻게 옮기는가 (테스트 리허설)
    └── restore-prod/              # 어떻게 옮기는가 (운영): PVC, 임시 pod, 붓기 스크립트, values

두 디렉토리는 바뀌는 주기가 다르다. 이미지는 버전이 오를 때마다 바뀌고, 옮기는 방법은 거의 바뀌지 않는다. 섞어두면 매번 어디를 고쳐야 하는지 헷갈린다.

10.2 업그레이드마다 바꾸는 값은 이것뿐이다

바꾸는 곳값
image/controller/plugins.txt인프라팀 저장소에서 cp
image/controller/DockerfileFROM jenkins/jenkins:<새 버전>-jdk25 한 줄
valuescontroller.image.tag, persistence.existingClaim
붓기 스크립트출발 pod 이름(SRC_POD), 새 PVC 파일 이름
PVC yaml새 슬롯 이름

나머지는 그대로 쓴다. 차트 구조, 플러그인 정책(installPlugins: false, overwritePlugins: false, PLUGINS_FORCE_UPGRADE), 컨텍스트 검사, 붓기 제외 목록, 전환 순서는 이번에 정해서 고정했다.

10.3 슬롯을 번갈아 쓴다

회차출발 (현재 운영)도착 (새 젠킨스)
이번 (2026-10)jenkins-njenkins-n-green
다음jenkins-n-greenjenkins-n-blue
그 다음jenkins-n-bluejenkins-n-green (재사용)

release 이름에 버전을 넣지 않았기 때문에 이름 규칙을 매번 새로 정할 필요가 없다. 매뉴얼에는 이 표만 있으면 된다.

10.4 매뉴얼에 넣은 것

  • 현재 현황: 지금 어떤 release, PVC, 이미지, DNS가 쓰이고 있는지
  • 단계별 명령과 확인 기준: 예를 들어 “로그에 fully up이 나오면 성공”, “복사 후 secrets/에 master.key가 있어야 한다”
  • 롤백 절차와 정리 절차
  • 이번에 실제로 겪은 문제와 해결 방법
  • 모든 단계에 왜 그렇게 하는지: 상황이 달라졌을 때 그대로 따라야 할지, 바꿔도 될지 판단할 수 있게 VM 시절처럼 “패키지를 바꾸고 재시작”만큼 간단해지지는 않았다. 그래도 정해진 값 몇 개를 바꾸고, 정해진 순서대로 스크립트를 실행하는 일로는 줄였다. 처음 보는 팀원도 매뉴얼을 따라 할 수 있는 수준이다.

11. 결과

항목결과
버전2.541.2 → 2.580.2 (컨트롤러 Java 25)
빌드 중지 시간약 2분 (최종 붓기 37초 + 재기동)
블루(기존)replicas 0, release·PVC·LB 보존 (1~2일 관찰 후 정리)
플러그인151개. 이미지 기준으로 관리하고 UI 설치도 유지
사용자 영향없음. 도메인, 웹훅, 계정, 권한 모두 그대로

컨트롤러를 Java 21이 아니라 25로 올린 데도 이유가 있다. 젠킨스는 Java 21 지원을 2027-09-30 이후 종료할 예정이다. 어차피 이번에 이미지를 새로 만드니, 다음에 한 번 더 바꿀 일을 미리 없앴다.

12. 남은 것

  • 에이전트 이미지 재구성: 공식 inbound-agent jdk25 베이스에서 다시 쌓는다. 그동안 버전을 고정하지 않았던 도구(kubectl stable, yq latest 등)도 이번에 고정한다. Java 21 지원 종료 전까지 해야 한다.
  • 카니코 교체: 원본 GoogleContainerTools/kaniko는 2025년 6월에 아카이브돼서 더 이상 보안 패치가 나오지 않는다. 유지보수 포크로 바꾸는 걸 검토 중이다. 이번 업그레이드와는 무관하다.
  • 블루 정리: NAS StorageClass가 Retain이라 PVC를 지워도 NAS 볼륨과 비용이 남는다. 콘솔에서까지 지워야 끝난다. 다음 업그레이드는 런북을 따라가면 된다. 이번에 그린으로 왔으니 다음에는 블루로 간다.

13. 회고

잘한 점

  • 버전이 아니라 방법을 목표로 잡았다. 버전만 올렸다면 다음 공지 때 또 새로 구축했을 것이다. 절차를 표준화하는 걸 목표로 잡았기 때문에, 이번에 겪은 문제들이 전부 매뉴얼의 확인 기준과 트러블슈팅 항목으로 남았다.
  • 제자리 업그레이드를 고집하지 않았다. 테스트에서 문제가 연달아 나왔을 때 하나씩 땜질하기보다 방식을 바꿨다. 덕분에 운영 젠킨스는 마지막 2분을 빼고는 영향을 받지 않았다.
  • 이미 검증된 방식을 가져왔다. 깃랩 업그레이드에서 써본 블루/그린 절차를 그대로 옮겼다. 새로운 도구를 도입하지 않고도 충분했다.
  • 이번 기회에 “실제로 돌던 것”을 코드로 끌어왔다. 플러그인 목록과 markupFormatter처럼 코드와 실제가 어긋나 있던 것들을 values와 Dockerfile 기준으로 맞췄다. 아쉬운 점

  • 플러그인 구조를 처음부터 확인하지 않았다. installPlugins가 이미지 플러그인을 가린다는 사실을 미리 알았다면 첫 기동 실패는 없었을 것이다. 업그레이드 전에 “이미지 목록”과 “실제 PVC에 깔린 목록”부터 비교하는 단계를 런북 맨 앞에 넣었다.
  • 테스트와 운영의 차이를 과소평가했다. 테스트 젠킨스는 플러그인 구성과 커스텀 이미지 여부가 운영과 달랐다. 그래서 테스트 리허설이 운영 성공을 보장하지 못했다. 블루/그린이었기 때문에 운영에서도 그린을 띄워 한 번 더 검증할 수 있었고, 그게 결국 안전장치가 됐다. 이번 작업으로 얻은 건 새 버전보다 반복할 수 있는 업그레이드 절차다. 업그레이드가 “누군가 날을 잡고 새로 구축하는 일”에서 “매뉴얼을 펴고 값 몇 개를 바꾸는 일”이 됐다. 다음 보안 공지가 나오면 팀원 누구든 이 매뉴얼로 올릴 수 있으면 좋겠다.

참고

This post is licensed under CC BY 4.0 by the author.

© noah. Some rights reserved.