Groovy를 이용해서 Jenkins 설정 자동화하기(사용자 생성, 고정 API Token 입력)

kindof

·

2023. 1. 18. 18:47

0. Docker 환경에서 Jenkins 서버 띄우기

이전 글[Jenkins, Docker를 통한 CI 환경 구축과 Slack 알람 받기]에서 도커 환경에서 Jenkins 서버를 띄우는 방법에 대해 설명했습니다.

그런데 이 글에서는 DockerHub에서 제공하는 Jenkins lts 버전 이미지를 Pull 받아 사용했었는데요.

사실 위 글에서 사용한 이미지에는 어떤 커스텀화 된 설정들이 없기 때문에 최초 서버 접속 시 아래와 같이 사용자의 비밀번호를 입력하고 플러그인 설치를 묻는 과정, 그 후에 계정을 설정하는 등의 과정을 수작업으로 진행해야 했습니다.

 

Jenkins 최초 접속 시 화면

 

 

1. Groovy 파일을 이용해서 사용자 자동 생성

Jenkins Docker의 공식 깃허브를 보면, 기본 Jenkins 이미지에 사용자가 원하는 기능을 추가하기 위해 /usr/share/jenkins/ref 위치에 custom groovy 파일을 둘 수 있다고 되어 있습니다.

 

jenkinsci > docker > readme

 

그래서 이 내용을 따라 새로운 Dockerfile과 groovy 파일을 작성하고 이를 바탕으로 자동으로 사용자 계정을 만드는 실습을 먼저 진행해보겠습니다.



[1] 먼저 Docker는 설치되어 있어야 합니다.

$ brew install docker docker-credential-helper --formula
 
# 설치 확인
$ docker --version


[2] 아래와 같이 Dockerfile, groovy 파일을 생성합니다.

$ tree -l

.
├── Dockerfile
└── ref
    └── init.groovy.d
        └── 01_user_setting.groovy

3 directories, 2 files


[3] Dockerfile을 작성합니다.

FROM jenkins/jenkins:lts

USER root

COPY ref /usr/share/jenkins/ref

USER jenkins
ENV JENKINS_USER sunghyeon
ENV JENKINS_PASSWORD 1234

#
# Skip initial setup
#
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false


새롭게 작성한 Dockerfile은 기존의 LTS 이미지에 로컬의 ref 경로를 /usr/share/jenkins/ref에 복사, 사용자의 정보를 넣는 ENV 추가, 초기 셋업을 스킵하는 옵션만 추가했습니다.

[4] groovy 파일을 작성합니다.

import jenkins.model.*
import hudson.security.*

def env = System.getenv()

def jenkins = Jenkins.getInstance()
jenkins.setSecurityRealm(new HudsonPrivateSecurityRealm(false))
jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy())

def user = jenkins.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASSWORD)
user.save()

jenkins.save()

사실 구글링을 해보면 groovy 파일 작성에 관한 정보가 많이 없는 것 같은데요. 뒤에서 또 다른 groovy 파일 설정을 해볼텐데, 이렇게 Jenkins가 제공하는 기능들은 공식 깃허브에서 찾아볼 수밖에 없는 것 같습니다.

어쨌든, 위 파일은 사용자 계정을 Dockerfile의 ENV에서 읽어와서 생성하고, 로그인을 하면 모든 권한을 부여한다는 내용입니다.

 

[5] 도커파일 빌드, 실행

docker build --tag jenkins/test . && docker run -p 8085:8080 jenkins/test


Dockerfile이 위치한 디렉토리에서 위 명령어를 실행하면 아래와 같은 로그를 남기며 서버가 띄워집니다.

Sending build context to Docker daemon  4.096kB
Step 1/7 : FROM jenkins/jenkins:lts
 ---> 1055c8a99f66
Step 2/7 : USER root
 ---> Using cache
 ---> ab021d54eea0
Step 3/7 : COPY ref /usr/share/jenkins/ref
 ---> Using cache
 ---> cd012d711f09
Step 4/7 : USER jenkins
 ---> Using cache
 ---> 41eaf0afda6c
Step 5/7 : ENV JENKINS_USER sunghyeon
 ---> Using cache
 ---> 19c62f0bf460
Step 6/7 : ENV JENKINS_PASSWORD 1234
 ---> Using cache
 ---> a86ed8be2972
Step 7/7 : ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false
 ---> Using cache
 ---> baf8acf7b64b
Successfully built baf8acf7b64b
Successfully tagged jenkins/test:latest
Running from: /usr/share/jenkins/jenkins.war
webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")
2023-01-18 06:38:37.792+0000 [id=1]	INFO	winstone.Logger#logInternal: Beginning extraction from war file
2023-01-18 06:38:38.286+0000 [id=1]	WARNING	o.e.j.s.handler.ContextHandler#setContextPath: Empty contextPath
2023-01-18 06:38:38.316+0000 [id=1]	INFO	org.eclipse.jetty.server.Server#doStart: jetty-10.0.12; built: 2022-09-14T01:54:40.076Z; git: 408d0139887e27a57b54ed52e2d92a36731a7e88; jvm 11.0.17+8
2023-01-18 06:38:38.477+0000 [id=1]	INFO	o.e.j.w.StandardDescriptorProcessor#visitServlet: NO JSP Support for /, did not find org.eclipse.jetty.jsp.JettyJspServlet
2023-01-18 06:38:38.505+0000 [id=1]	INFO	o.e.j.s.s.DefaultSessionIdManager#doStart: Session workerName=node0
2023-01-18 06:38:38.717+0000 [id=1]	INFO	hudson.WebAppMain#contextInitialized: Jenkins home directory: /var/jenkins_home found at: EnvVars.masterEnvVars.get("JENKINS_HOME")
2023-01-18 06:38:38.832+0000 [id=1]	INFO	o.e.j.s.handler.ContextHandler#doStart: Started w.@6cd56321{Jenkins v2.375.2,/,file:///var/jenkins_home/war/,AVAILABLE}{/var/jenkins_home/war}
2023-01-18 06:38:38.847+0000 [id=1]	INFO	o.e.j.server.AbstractConnector#doStart: Started ServerConnector@485e36bc{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
2023-01-18 06:38:38.864+0000 [id=1]	INFO	org.eclipse.jetty.server.Server#doStart: Started Server@625abb97{STARTING}[10.0.12,sto=0] @1384ms
2023-01-18 06:38:38.869+0000 [id=23]	INFO	winstone.Logger#logInternal: Winstone Servlet Engine running: controlPort=disabled
2023-01-18 06:38:39.015+0000 [id=30]	INFO	jenkins.InitReactorRunner$1#onAttained: Started initialization
2023-01-18 06:38:39.033+0000 [id=28]	INFO	jenkins.InitReactorRunner$1#onAttained: Listed all plugins
2023-01-18 06:38:39.448+0000 [id=30]	INFO	jenkins.InitReactorRunner$1#onAttained: Prepared all plugins
2023-01-18 06:38:39.450+0000 [id=29]	INFO	jenkins.InitReactorRunner$1#onAttained: Started all plugins
2023-01-18 06:38:39.458+0000 [id=31]	INFO	jenkins.InitReactorRunner$1#onAttained: Augmented all extensions
2023-01-18 06:38:39.604+0000 [id=28]	INFO	jenkins.InitReactorRunner$1#onAttained: System config loaded
2023-01-18 06:38:39.604+0000 [id=28]	INFO	jenkins.InitReactorRunner$1#onAttained: System config adapted
2023-01-18 06:38:39.604+0000 [id=28]	INFO	jenkins.InitReactorRunner$1#onAttained: Loaded all jobs
2023-01-18 06:38:39.607+0000 [id=28]	INFO	jenkins.InitReactorRunner$1#onAttained: Configuration for all jobs updated
2023-01-18 06:38:39.633+0000 [id=44]	INFO	hudson.util.Retrier#start: Attempt #1 to do the action check updates server
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.codehaus.groovy.vmplugin.v7.Java7$1 (file:/var/jenkins_home/war/WEB-INF/lib/groovy-all-2.4.21.jar) to constructor java.lang.invoke.MethodHandles$Lookup(java.lang.Class,int)
WARNING: Please consider reporting this to the maintainers of org.codehaus.groovy.vmplugin.v7.Java7$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
2023-01-18 06:38:39.781+0000 [id=28]	INFO	j.util.groovy.GroovyHookScript#execute: Executing /var/jenkins_home/init.groovy.d/01_user_setting.groovy
2023-01-18 06:38:40.159+0000 [id=28]	INFO	jenkins.InitReactorRunner$1#onAttained: Completed initialization
2023-01-18 06:38:40.195+0000 [id=22]	INFO	hudson.lifecycle.Lifecycle#onReady: Jenkins is fully up and running
2023-01-18 06:38:49.839+0000 [id=44]	INFO	h.m.DownloadService$Downloadable#load: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller
2023-01-18 06:38:49.840+0000 [id=44]	INFO	hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1


[6] 결과 확인
이제 localhost:8085 로 접속해보면 젠킨스 설정과 관련된 스텝들을 건너뛰고 바로 Dashboard가 보여집니다.

 

Jenkins Dashboard


그리고 우리가 설정했던 사용자의 아이디와 비밀번호를 입력하면 아래와 같이 로그인이 되는 것을 확인할 수 있습니다.

 

설정했던 내용으로 계정이 생성되어 있다.

 

2. Jenkins Remote Job 실행

아래와 같이 실험을 위해 테스트용 Job을 하나 만들겠습니다.

 

test


Job을 수동으로 돌리면 아래와 같이 정상 동작합니다.

Console output


그런데 만약 이러한 Job이 Jenkins 외부에서 호출되어야 한다면 어떨까요?

실제로 규모가 큰 서비스에서는 많은 Job들이 서로 Pipeline으로 연결되어있어 외부의 Job이 종료된 뒤, 우리 내부의 Job을 호출할 수 있습니다.

그리고 이런 경우를 상정하여 Jenkins에서는 API Token이라는 것을 제공하는데요.

아래와 같이 <Jenkins URL>/user/<username>/configure 에 들어가보면 API Token을 생성할 수 있고, 이 토큰을 바탕으로 외부에서 내부의 Job을 트리거할 수 있습니다.

실제로 테스트를 위해 curl 요청을 보내보면, 201 응답과 함께 Dashboard에서 Job이 트리거 된 것을 확인할 수 있습니다.

외부에서 curl 요청을 보낸다.

 

3. Groovy 파일을 이용해서 고정된 API Token 설정

그런데 위와 같이 사용자의 API Token을 바탕으로 한 외부의 트리거링은 한 가지 문제가 있습니다. 바로 '젠킨스 도커가 다시 띄워지면 초기화된다'는 것인데요.

따라서 위와 같은 루트를 통해 Job을 트리거링한다면, 젠킨스 도커가 시작될 때마다 API Token을 이전에 사용하던 값 그대로 넣어주는 작업이 필요하게 됩니다. Token을 새로 발급받으면 이를 다시 공유해야 하기 때문이죠.

그래서 처음에 작성한 Groovy 파일에 내용을 추가합니다.

import jenkins.model.*
import hudson.security.*
import jenkins.security.*

def env = System.getenv()

def jenkins = Jenkins.getInstance()
jenkins.setSecurityRealm(new HudsonPrivateSecurityRealm(false))
jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy())

def user = jenkins.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASSWORD)

###################################################
def apiTokenProperty = user.getProperty(ApiTokenProperty.class)
def result = apiTokenProperty.tokenStore.addFixedNewToken("test_token", "11e869c106fd9b99e0461864b07473629b")
###################################################

user.save()
jenkins.save()

이제 다시 Dockerfile을 빌드하고 실행해보면, 아래와 같이 "test_token"이 바로 저장되어 있고, Jenkins 컨테이너 안에 config 파일을 보면 위에서 추가한 토큰이 해시화되어 들어가 있는 것을 확인할 수 있습니다.

 

Token이 저장되어 있다.

$ docker exec -it <Container ID> bash
$ cat /var/jenkins_home/users/<user>/config.xml

저장한 토큰이 해시화되어 들어가있다.

 


감사합니다.