코드 리뷰 가능한 Jenkins? - 패키징 가능한 코드 기반의 Jenkins 환경 구축
이번 주 화요일에, 사내 기술 공유 세션인 NAVER ENGINEERING DAY에서 기술 발표를 진행했다. 물론 정확히는 라이브가 아니고 영상이었지만 ㅎㅎ;;
다만 발표 준비 시점에 너무 업무 및 기타 개인 일정으로 많이 바빴고, 다소 허겁지겁 자료를 만들고 영상을 찍다보니 내용 누락도 있었고 여러모로 아쉬움이 컸던 것 같다.
원래는 외부 영상 업로드까지 신청해두긴 했지만, 업무적인 내용이 포함되어 있어 만약 실제로 올라가게 된다면 발표 자료 재제작 및 영상 재촬영이 필수인 상황이라, 전반적으로 상황을 알 수 없게 되었고 (ㅜㅜㅜ), 아쉬운대로 우선 블로그에 좀 더 꼼꼼하게 내용을 공유해보려고 한다. (혹시 이후에 해당 주제의 영상이 업로드 되었다면, 아마 이 블로그에 적힌 내용과 유사할 것이다.)
왜 Jenkins를?
현재 업무를 하고 있는 부서는 목적과 실제 지역을 기반으로, 리전을 구분하고 있다. (동일한 대한민국 지역이라고 해도, 내부 서비스와 외부 서비스에 따라 리전은 분리되는 느낌이라고 보면 좋다.) 거기에 개발, 운영 환경이 있으니 더더욱 관리해야 하는 리전은 많을 수 밖에 없었다.
각 리전은 db나 배치등이 분리될 수 밖에 없고, ACL 및 네트워크 정책등을 고려하면 자연스럽게 하나의 리전에 하나의 Jenkins가 대응되어야 할 것이다.
문제는... 상술한 것 처럼 리전이 많은 환경이다보니, 스케쥴링 트리거 목적의 Jenkins가 10개 이상 필요했다는 것. 이에 아래와 같은 문제들이 있었다.
- 유지보수 문제. Job을 추가하거나 수정해야 하는 경우, 약 10개 정도의 Jenkins에 일일히 추가하고 수정해야 했다. 그래도 추가라면 존재하지 않은 경우 금방 눈치챌 수 있으나, 수정을 제대로 했는지 확인하기 위해선 직접 들어가서 세부 설정을 보지 않은 이상 수정 여부를 확인하기 어려웠다. 그러다보니 잦은 휴먼 에러가 발생했고, 심지어 1년 넘게 수정이 안 되었던 케이스도 존재했었다.
- 신규 리전 구축 문제. 현재 부서는 수십개의 스케쥴링 잡을 관리하고 있는 만큼, 새로운 리전에 Jenkins를 구축하기 위해선 설정 및 잡 추가를 모두 수행해야 한다. 대안으로 Job Import 플러그인이 있긴 하지만, Tomcat의 설정을 일부 변경해야 하며, (해당 플러그인은 Jenkins API를 호출하는 구조로 되어 있는데, 이 과정에서 기본 설정으로는 막혀있는 문자가 포함된 쿼리를 요청한다.) 막상 가져온다고 해도 각 리전 및 dev/real 에 대응하는 prefix 등에 대한 수정이 필요하다.
특히나, 상위 조직 단위에서 올해 컨테이너 환경 전환을 추진하게 되면서 자연스럽게 Jenkins도 컨테이너로 전환이 필요했고, 이는 이전까지 구축한 VM Jenkins 들을 전부 밑바닥부터 전부 재구축해야 하는 상황이 된 것이다. 이 과정에서, 누군가 재미있는 제안을 했다.
코드 리뷰 가능한 Jenkins는 어떨까요?
만약 모든 잡을 코드로 관리한다면, 매번 GUI로 수동으로 추가할 필요 없이 코드리뷰를 통해 잡을 적용할 수 있지 않을까?
즉, 우리는
- 코드 리뷰가 가능한 Jenkins Job 구조
- 모든 것들을 패키징해 컨테이너 배포 시 바로 사용 가능한 Jenkins 구조 확립
을 만들기로 했다.
#1 - Jenkins CasC (Configuration as a Code)
가장 먼저, Jenkins CasC에 대해 알아보도록 하자. 뒤에서 설명할 온갖 도구들과 비교하여, 해당 도구는 제법 잘 알려진 도구인 만큼, 간략하게 설명하기로 하겠다.
Jenkins CasC 는 Jenkins의 모든 설정 을 코드로 관리하기 위한 도구이다. 잡이 아니라는 점에 유의하라.
jenkins:
systemMessage: "Jenkins configured automatically by Jenkins Configuration as Code plugin\n\n"
globalNodeProperties:
- envVars:
env:
- key: VARIABLE1
value: foo
- key: VARIABLE2
value: bar
securityRealm:
ldap:
configurations:
- groupMembershipStrategy:
fromUserRecord:
attributeName: "memberOf"
inhibitInferRootDN: false
rootDN: "dc=acme,dc=org"
server: "ldaps://ldap.acme.org:1636"
위와 같은 일반적인 yaml 형식의 구조를 띄고 있으며, 각각의 트리 구조는 실제 설정에서의 폴더 구조와 동일하다.
다만 일반적으로 직접 CasC 파일을 하나하나 다 작성하진 않고, 보통 기존 사용중인 Jenkins 인스턴스에서 CasC 파일을 추출해서 사용하는 방식을 많이 쓴다. (즉, 설정을 복사하여 타 Jenkins 에서 빠르게 적용할 수 있도록 하는 것이 목적이다.)
아래와 같은 사항만 유의하면, Jenkins CasC를 사용하는 데에는 큰 어려움이 없을 것이다.
- CasC 플러그인이 설치된 경우, Jenkins는
CASC_JENKINS_CONFIG라는 환경변수의 값을 읽는다.- 만약 컨테이너화 등 패키징을 수행하는 것이 목적이라면, 배포 스크립트 내
CASC_JENKINS_CONFIG를 반드시 설정해야 한다.
- 만약 컨테이너화 등 패키징을 수행하는 것이 목적이라면, 배포 스크립트 내
- CasC 파일의 값도 환경변수를 사용할 수 있다. (Jenkins 부팅 과정에서 해당 파일을 읽는 구조이므로, 환경변수를 필드의 value로 넣을 수 있다.)
- 만약 로드된 CasC 파일에서 해당 Jenkins 인스턴스에 없는 설정 값이 있다면, 에러를 띄우고 부팅되지 않는다.
- 즉, 해당 파일을 추출한 Jenkins 인스턴스에 설치된 플러그인과 관련된 설정이 있다면, 해당 설정 값을 지우고 Import 하거나 아예 처음부터 플러그인 설치를 같이 수행해야 한다.
- 만약 컨테이너화 등 패키징을 수행하는 것이 목적이라면, Jenkins에서 제공하는 Plugin Installation Manager Tool 을 쓰는 것도 좋다.
- 부서내 자체 플러그인이나 기타 사유로 Plugin Installation Manager Tool 에서 설치가 불가하다면, 아예 목록에 파일을 포함하는 것도 방법이다. (우리 부서의 경우엔 사내 메신저용 알림 도구 때문에 이런 케이스가 존재했다.)
#2 - Jenkins Job DSL (실패)
일단 설정은 해결했고, 그래서 잡을 어떻게 코드로 관리할 것인가?
플러그인을 뒤져보다보니 Job을 DSL로 관리하는 플러그인이 있었다. Jenkins Job DSL 이라는 플러그인이었다.
이름만 들어봐도 알겠지만 DSL 방식으로 잡을 추가할 수 있는 플러그인이다. 예를 들어, 아래와 같은 DSL을 작성할 수 있다.
job("sendNoti") {
description("알림 발송")
keepDependencies(false)
disabled(false)
concurrentBuild(false)
steps {
shell("/home1/irteam/scripts/launchJob.sh sendNoti")
}
displayName("(DEV) KR3_SERVICE_sendNoti")
configure {
it / 'properties' / 'jenkins.model.BuildDiscarderProperty' {
strategy {
'daysToKeep'('10')
'numToKeep'('-1')
'artifactDaysToKeep'('-1')
'artifactNumToKeep'('-1')
}
}
}
}
- 기본적인 설정을 제외한 추가 설정은 configure 블록에 포함된다.
- 플러그인과 관련한 설정이 필요하다면,
it / 'properties' / 'plugin 패키지 명'식으로 접근 가능하다.
- 플러그인과 관련한 설정이 필요하다면,
- job의 id와 displayName 이 별도로 존재한다.
- Jenkins CasC와 동일하게, 해당 DSL 구문도 기존에 등록된 Job을 DSL로 변환하는 기능이 있으니, 처음이라면 해당 방법을 사용해서 export 해도 좋다.
특징이라면, 하나의 DSL 파일에서 여러 잡을 동시에 생성하는 것도 가능하다. (코드 하나에 job(~~) 구문이 여러개라면, 여러 잡을 등록할 수 있다.)
우리의 목표가 너무 쉽게 달성되는 것 아니야? 싶어서 다소 김이 샐 뻔 했지만... 사실 김 샐 게 아니긴 하지만. 아래와 같은 문제로 우리의 상황에선 직접 사용할 수 없었다.
- 기본적으로, Job DSL 의 목적은 동적으로 Job을 생성하기 위한 것이다.
- 일반적으로 Seed Job 목적의 Jenkins Project를 생성하고, 해당 프로젝트에서 DSL을 실행시키는 방식으로 잡을 생성해야 한다.
- Jenkins의 정책 상, Groovy Script 가 Sandbox 바깥에서 실행된다면 반드시 관리자 권한을 갖는 사용자의 승인이 필요하다. (심지어 사용자가 관리자라고 하더라도, 형식적인 승인을 해야 한다.)
- 이는 Jenkins 의 보안 정책에 의한 것으로, 보안 설정을 조작한다면 무시할 순 있지만, 기본값은 승인 필수이다.
그러나 우리의 목표는 Jenkins가 뜨자마자 바로 Job이 등록되는 구조를 원했기 때문에, 아쉽게도 이 도구를 직접적으로 사용할 순 없었다.
#2.5 - 잠깐, Jenkins의 LifeCycle을 들여다보자.
Jenkins 는 부팅 과정에서, 여러 상태를 갖는다. Jenkins를 이를 InitMilestone 이라고 하며, 아래와 같은 상태가 존재한다.
| 상태 |
|---|
