Post

(CI/CD) 젠킨스 Jenkins

젠킨스 설치

  • 공식 홈페이지에서 권장하는 방법은 .war 파일 하나 받아서 java 커맨드로 띄우는 방식. 이게 제일 간단
  • yum 등을 이용해 아예 패키지로 설치해도 되긴 함
1
2
#!/bin/sh
nohup java -DJENKINS\_HOME=/home1/user/jenkins/jenkins\_home -jar /home1/user/apps/jenkins/jenkins.war --httpPort=8080  > /home1/user/logs/kenins/nohub.out &
  • JENKINS_HOME은 옵션으로 줄 수도 있고, 환경변수로 줄 수도 있음. 기본 경로는 ~/.jenkins
  • jenkins-x.x.x 폴더에 symlink 걸어서 jenkins로 참조하도록 하는게 관리 편함
  • 8080은 기본 포트
자바가 없다면?

Jenkins 설정

jenkins가 webhook을 수신했을 때, 어떤 작업을 수행하도록 명시하는 방법 중 자주쓰는 방식은Freestyle projectPipeline 방식 (젠킨스에서 New Item 눌러보면 알겠지만 다른 방식도 있다.)

Pipeline 방식 = Jenkinsfile 사용하는 방식

Freestyle project가 빌드와 관련된 설정을 젠킨스 상에서 이것 저것 눌러서 세팅하는 방식인 반면, Pipeline 방식은 Jenkinsfile이라는 소스 안에 빌드 설정이 들어가는 방식

Jenkinsfile 예제

Pipeline에서 Jenkinsfile을 빈 파일로 두면, 빌드가 트리거 될 때 마다 빌드옵션이 없어서 빈 빌드를 수행하게 되어 항상 success가 뜬다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
node {
    stage ('clone') {
        checkout scm
    }
    stage ('gradle wrap') {
        sh '/opt/gradle/gradle-5.2.1/bin/gradle wrap'
    }
    stage('gradlew build') {
        if (isUnix()) {
            sh './gradlew build'
        } else {
            bat 'gradlew.bat build'
        }
    }
}

checkout scm이 credential을 적절히 설정하고, branch도 알아서 설정한 다음, git에서 해당 브랜치를 fetch해오는 작업을 해주는 명령어다.

PR webhook 수신 시 빌드 트리거 하는 플러그인

  • Github Pull Request Builder Plugin
    • 기존에 많이 사용하던건데, 보안 이슈 때문에 deprecated 되었음. 그래도 여전히 사용 가능.
  • Github Branch Source Plugin
    • ghprb가 보안 때문에 말이 많아서 이걸 쓰는 것을 권장하고 있음
    • 젠킨스 설치 시 인기 플러그인 자동설치를 누르면 알아서 설치됨
  • docs + howto
  • github
  1. Jenkinsfile을 프로젝트 루트 디렉토리에 추가한다. ( 이걸 안하면 Repo가 인식이 안됨. )
  2. New Item - Github Organization 선택 *** Github Org 아이템은 모든 레포 중 조건에 맞는 것들을 선택할 때 사용하는 Item. 이걸 이용해도 되긴 된다.
  3. Owner에 github repository가 위치한 계정 또는 조직 이름 적고 (접근 권한 가진 계정은 Credentials에 명시)
  4. 해당 계정 또는 조직에서 어떤 레포지토리에 접근할 것인지는 Behaviors - Add - Filter by name 선택. 하고 레포지토리 명시.
  5. Discover branches - Exclude …(기본 옵션으로 놔둔다)
    • 이건 해당 레포지토리에서 branch를 가져와 목록으로 띄워줄거냐,를 결정한다.
    • Exclude ...옵션은 일반 브랜치는 Branch 쪽에 띄워주고 pull request를 요청한 브랜치는 Pull Requests쪽에 띄워준다.
      • PR을 날렸을 때는 PR한 브랜치에 대해서 빌드하고, Branch 목록에 있는 브랜치에서 push가 발생하면 push 할 때 마다 빌드함.
    • Only ... 옵션은 현재 pull request를 요청한 브랜치만 Branch 목록 및 Pull Requests 목록에 중복해서 띄워준다.
      • 즉 이 옵션을 선택하면 push PR 대기중인 브랜치와 PR 대상 브랜치만 트래킹한다.
    • All branches 옵션은 그냥 모든 브랜치 보여준다.
  6. Add - within repository - Filter by name (with wildcards)에 푸시마다 빌드를 하지 않을 브랜치(feature/*)를 적어준다.
  7. Scan Organization Triggers를 원하는 시간으로 설정한다.
    • PR을 날렸을 때 바로 빌드하는게 아니라, 주기적으로 레포지토리를 Scan해서 새로운 PR이 있으면 빌드하는 방식이다.
    • 한 10분 정도로 해놓으면 괜찮은 듯. 기본은 1day로 되어 있어서 너무 텀이 길다.

이렇게 설정하면, master, develop같은 브랜치는 푸시할 때 마다 빌드하게 되고, Filter로 Exclude된 feature/* 브랜치에 대해서는 푸시해도 따로 빌드를 하지 않는다(애초에 브랜치 목록에 안보인다.)

새로운 PR이 있는지 주기적으로 체크해서 별도로 빌드, Merge 시 develop, master에 대해 빌드. 하게 된다.

젠킨스에서 빌드 테스트가 돌아가는 시점을 언제로 할 것인가?
  • master, dev 브랜치 Push(Merge)가 발생할 때만 빌드 테스트.
    • 별로임. 코드리뷰 다 끝나고 Merge 눌렀을 때 테스트가 돌아가는데, 이 때 Failure이 떨어지면 난감하다.
    • 물론 PR 올리기 전에 dev -> feature로 땡겨와서 동기화해주면 문제가 발생할 여지가 줄어들기는 하는데, 완벽한 해결책은 아님
  • PR 시점
    • Code review 전에 테스트가 돌아갈 수 있다는 장점이 있음
    • https://ohgyun.com/551- PR 시점의 빌드 테스트의 장점과 플러그인
  • feature 브랜치에 Push가 발생할 때도 빌드 테스트.
    • 제일 안전한 방식
    • 근데 스타일에 따라 Push를 자주 하는 사람도 있을거고, 일단 임시로 Fail 뜨는 Push를 하는 경우도 있을거라.. 좀 지저분하긴 함 보편 적으로는 master, dev push 시, PR 시 둘 다 쓴다.

기타

모든 설정을 잘 한 것 같은데 안된다? 로그를 확인해볼 것. 권한 없어서 안될 가능성도 있음.
1
2
3
4
5
6
7
8
9
10
11
5월 19, 2019 2:06:02 오후 jenkins.branch.OrganizationFolder$OrganizationScan run
정보: hackday #20190519.140600 organization scan action completed: SUCCESS in 2.4 sec
5월 19, 2019 2:07:02 오후 org.jenkinsci.plugins.github.webhook.WebhookManager$1 run
정보: GitHub webhooks activated for job hackday/secure\_keyboard with [GitHubRepositoryName[host=github.com,username=NAVER-CAMPUS-HACKDAY,repository=secure\_keyboard]] (events: [PULL\_REQUEST, PUSH])
5월 19, 2019 2:07:02 오후 jenkins.branch.OrganizationFolder$OrganizationScan run
정보: hackday #20190519.140700 organization scan action completed: SUCCESS in 2 sec
5월 19, 2019 2:07:02 오후 com.squareup.okhttp.internal.Platform$JdkWithJettyBootPlatform getSelectedProtocol
정보: ALPN callback dropped: SPDY and HTTP/2 are disabled. Is alpn-boot on the boot class path?
5월 19, 2019 2:07:03 오후 org.jenkinsci.plugins.github.webhook.WebhookManager$2 applyNullSafe
경고: Failed to add GitHub webhook for GitHubRepositoryName[host=github.com,username=NAVER-CAMPUS-HACKDAY,repository=secure\_keyboard]
java.lang.NullPointerException: There are no credentials with admin access to manage hooks on GitHubRepositoryName[host=github.com,username=NAVER-CAMPUS-HACKDAY,repository=secure\_keyboard]
유용한 Jenkins 플러그인 목록
  • CheckStyle
  • SLOCCount : 언어 별, 파일 별 loc 계산. UI 깔끔
  • Cobertura , JaCoCo : TC 커버리지
    • Cobertura는 관리도 잘 안되고 있는 것 같고, javaNCSS를 사용하고 있는데 이게 java8 대응이 안돼서 lambda 파싱에러가 무지남. JaCoCo 쓰는게 나아보인다.
  • SonarQube : 정적 분석 도구 (플러그인으로 위 모듈 모두 달 수 있어서, SonarQube를 달거라면 이렇게 하는게 낫지 싶다.)
  • plugins.jenkins.io/pipeline-stage-view/

Jenkins start, stop shell script

start-stop-daemon 이용한 방법
직접 작성
  • shell script에서 할당 시에는 = 앞뒤 공백 있으면 안됨
  • [ $PID ]말고 bash ["X$PID" = "X"] 로 체크하는게 무슨 장점이 있었던 것 같은데 까먹었다.
  • VAR=cmd 형식의 문법은 이 라인이 실행되는 순간 VAR가 결정되는 것이고, 매번 evaluation 되는 것이 아니다. 매번 evaluation 되도록 하려면 참고
  • 함수 evaluation은 $(fun)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/bin/sh
 
USER=`/usr/bin/whoami`
get_jenkins_pid() {
  ps -Af | grep "jenkins.war" | grep -v grep | awk '{print$2}'
}
 
start() {
  echo "Starting jenkins..."
  if [ $(get_jenkins_pid) ]; then
    echo "Jenkins($(get_jenkins_pid)) is already running."
  exit 1
  fi
 
  nohup java -DJENKINS_HOME={jenkins_home_path} -jar {jenkins.war path} --httpPort=18080 > {log_path}/nohup.out 2>&1 &
 
  for i in {1..10}; do
    if [ $(get_jenkins_pid) ]; then
      break
    fi
    echo "."
    sleep 1
  done
 
  if [ $(get_jenkins_pid) ]; then
    echo "Started jenkins($(get_jenkins_pid))"
  else
    echo "Failed to start jenkins"
  fi
}
 
stop() {
  echo "Stopping jenkins..."
 
  cnt_jenkins_pid=$(get_jenkins_pid)
 
  if [ ! $cnt_jenkins_pid ]; then
    echo "Jenkins was not running."
    exit 1
  fi
 
  kill -9 $cnt_jenkins_pid
 
  for i in {1..10}; do
    if [ ! $(get_jenkins_pid) ]; then
      break
    fi
    echo "."
    sleep 1
  done
 
  if [ ! $(get_jenkins_pid) ]; then
    echo "Stoped jenkins($cnt_jenkins_pid)"
  else
    echo "Failed to stop jenkins($(get_jenkins_pid))"
  fi
}
 
if [ $USER != "irteam" ]; then
  echo "irteam 계정으로 실행해 주세요";
  exit;
fi
 
case $1 in
  "stop")
  stop
  ;;
 
  "start")
  start
  ;;
 
  *)
  echo "Usage: $0 { start | stop }"
  exit 1
  ;;
esac
This post is licensed under CC BY 4.0 by the author.