[CI/CD] jenkins기반 CI 구축 적용기

2022. 9. 13. 14:36[ DevOps ]/[ CI-CD ]

 

https://www.lambdatest.com/blog/jenkins-pipeline-tutorial/

 

 

이번 포스팅에서는 Jenkins를 기반으로 CI(Continuous Integration)를 구축했던 과정을 다룬다. Jenkins에서 제공하는 플러그인들과 기능들을 활용하여 빌드 파이프라인 구성뿐만 아니라, 빌드 자동화, 테스트 자동화, 코드 품질 자동화 등의 작업을 파이프라인 상에 같이 연동시켜 사용하는 경우가 많다. 이번 포스팅에서는 Jenkins를 설치하고 간단한 빌드 파이프라인을 구축하는 과정만 다룰 것이고 아래와 같은 순서로 구성된다.

 

1) Jenkins 설치 및 기본 설정 (컨테이너 기반 Jenkins 설치, 플러그인 설치, SSH 셋업)

2) CI/CD Pipeline Job 구성 (GitHub 연동, Jenkinsfile 작성)

3) 이슈 처리

 

 

cf. Jenkins는 Java 프로그램이기 때문에 2 core, 4GB 스펙이 필요함.

 

 


 

Jenkins 설치 및 기본 설정

1. docker로 jenkins 설치

$ docker run  \
	--name jenkins-container \
	-d \
	-p 8080:8080 \
	-v ~/jenkins:/var/jenkins_home \
	-v /var/run/docker.sock:/var/run/docker.sock \
	-u root \
	--restart=always \
	jenkins/jenkins:latest

1) -u root

-u root는 root 권한으로 컨테이너를 실행하겠다는 의미이다. v 옵션으로 호스트 PC의 파일 시스템으로 마운트하는데, /var 디렉토리에는 root 권한이 필요하기 때문에 설정한 것이다.

 

2) --restart=always

restart 정책의 always는 프로세스의 exit code 와는 관계없이 컨테이너를 재시작한다.

 

3) 마운트 옵션

/var/run/docker.sock 파일은 docker 커맨드에 대한 요청을 받는 데몬 프로세스의 유닉스 소켓이다. 컨테이너 안에서 호스트의 docker 데몬으로 요청을 보내기 위해 마운트했다. 이에 대해서는 아래 '이슈'부분에서 설명하고 있으나 사용되진 않는다.

 

cf. 추가적으로 궁금했던 점.

Jenkins 컨테이너에서 SSH를 이용해서 GitHub 및 CD서버로 접속하는데 22번 포트는 포워딩을 해주지 않아도 접속이 되었다. 아마 Jenkins 컨테이너 이미지 레이어 중에 22번 포트를 열어주는 이미지 레이어가 있을 거라 생각했으나, Jenkins Dockerfile을 살펴본 결과 확인할 수 없었다. 추후 알아봐야 할 부분이다.

 

2. admin password 확인

$ docker exec -it <컨테이너> bash -c "cat /var/jenkins_home/secrets/initialAdminPassword"

- 컨테이너를 bash 쉘로 접속하여 cat 커맨드를 수행한 결과를 출력하는 커맨드

 

 

3. 플러그인 설치

- 우선은 기본으로 제안되는 플러그인만 설치한다.

 

 

4. Jenkins 계정 생성

- Jenkins 관리 -> Manage Users -> 사용자 생성

 

 

계정 설정

 

기본적으로 Jenkins 유저를 생성하면 timezone이 UTC로 우리나라 시간보다 9시간 느리게 설정되어 있다. 설정으로 들어가 Asia/Seoul로 시간을 변경한다. 그리고 tester로 로그인한다.

 

 

 

5. 플러그인 추가

설치할 플러그인 목록

더보기

DSL 관련 플러그인

- Job DSL

- Simple Build DSL for Pipeline

 

파이프라인 관련 플러그인

- Docker Pipeline 

- Pipeline: Declarative Agent API

- Pipeline Utility Steps

- Build Pipeline

- SSH Pipeline Steps

- Pipeline: AWS Steps

- Pipeline: GitHub

 

GitHub 관련 플러그인

- Git Parameter

- GitHub Integration

- GitHub Authentication

 

Docker 관련 플러그인

- Docker

- Docker Commons

- docker-build-step

- CloudBees Docker Build and Publish

- CloudBees Docker Custom Build Environment

 

AWS 관련 플러그인

- Amazon Web Services SDK :: All

- CloudBees AWS Credentials

- Amazon ECR

- AWS Global Configuration

 

SSH 플러그인

- SSH

- SSH Agent

 

-> Install without restart

 

위에서 필요한 플러그인은 대부분 설치하면 된다. (AWS ECR에서 jenkins를 설치하는 게 아니라면 AWS 관련 플러그인은 설치하지 않는다.)

 

 

6. 인증 설정

인증 설정 부분에서 진행할 내용은 아래와 같다.

 

- GitHub SSH 인증 설정

- 배포 서버 SSH 인증 설정

 

 

6-1. GitHub 인증 설정 (ssh-key)

SCM(GitHub)의 코드를 가져오기 위해 SSH 키를 발급하고 등록하는 과정이다. GItHub에서 webhook로 특정 이벤트 발생 시 CI를 트리거하는 방식으로 하더라도 SSH 인증은 등록해야 한다. 

 

cf. SSH로 접속하는 방법 중 key를 기반으로 접속하는 방법을 이용할 것이다. SSH Key는 공개키(Public key)와 비공개 키(Private key)로 이루어지는데 비공개키는 접속하는 Client에 공개키는 Server에 위치하게 된다. SSH 접속을 할 때 공개키와 비공개키를 비교하여 일치하는지를 확인하여 인증하는 방식이다.

 

1) SSH 키 발급

# 로컬에서 발급 받는 방법 (macOS)
$ ssh-keygen -b 2048 -t rsa -f ~/.ssh/github-jenkins-ssh

github-jenkins-ssh는 key 이름 부분이니 사용할 이름에 맞게 수정하면 된다.

 

 

2) GitHub에 등록 (SSH server에 등록 - public key)

GitHub에서 개인 설정에 들어와 SSH and GPG keys에 들어가 발급한 키를 등록한다. 이때 GitHub이 SSH server 입장이니, public key(.pub 확장자 파일)를 등록한다. 만약 특정 레포지토리에서만 사용하고 싶다면 레포지토리 설정에서 'Deploy Key'에 등록한다.

 

 

3) jenkins에 등록 (SSH client에 등록 - private key)

설정 -> Security -> Manage Credentials -> Jenkins -> Global credentials -> Add Credentials

 

 

인증 키들은 jenkins에서 만든 Store에 저장해야 한다. Jenkins는 기본적으로 Jenkins라는 글로벌 Store가 있고 여기에 발급한 키를 등록하면 된다.

 

cf. Global Store인 이유

Jenkins에도 사용자 계정을 만들 수 있는데 이런 '사용자 계정 구별 없이' 글로벌하게 사용하기 위함이다(unrestricted). 만약 제한적으로 키를 사용하고 싶다면 restricted Store를 만들고 여기에 등록하면 된다.

 

 

Add Credentials에서 아래와 같이 설정한다. 

ssh private key는 ssh key를 생성할 때 생성된 두 파일 중 .pub이 아닌 파일이다. private key와 SSH server 공인IP만 있어도 누구든 접속이 가능하니 유출되지 않도록 주의해야 한다.

 

 

6-2. CD 서버 접속을 위한 SSH 키 발급 및 등록

CI서버(Jenkins)에서 CD서버 접속을 위해 SSH key를 다시 한번 발급한다. 아래는 CD 서버에 접속하여 배포를 담당할 리눅스 유저를 생성하고 SSH public 키를 등록하는 과정이다.

 

우선 SSH key를 생성한다. (어디서든 진행 가능)

$ ssh-keygen -b 2048 -t rsa -f ~/.ssh/jenkins-cd-ssh
$ cat ~/.ssh/jenkins-cd-ssh

 

CD 서버에 접속하여 배포를 담당할 리눅스 유저를 생성한다.

$ sudo useradd -m -c "deploy container image from CI jenkins SSH" deployer
$ sudo usermod -s /bin/bash deployer
$ sudo passwd deployer

 

생성한 유저가 docker 커맨드를 비밀번호 없이 사용할 권한을 주기 위해 docker 그룹에 추가한다.

$ su -
$ usermod -a -G docker deployer

 

그리고 위에서 발급한 SSH public key를 생성한 유저에 등록한다. (CD서버에서 진행)

$ su - deployer
$ mkdir ~/.ssh
$ chmod 700 .ssh
$ touch authorized_keys
$ chmod 0600 ~/.ssh/authorized_keys

# 발급한 SSH public key 등록. 
$ vim ~/.ssh/authorized_keys

 

 

발급한 SSH private key는 SSH client가 될 Jenkins(CI서버)에 등록한다.

 


 

CI/CD Pipeline Job 구성

1. CI Job Pipeline 구성 - Pipeline Job 생성

Jenkins에서 새로운 Item -> Pipeline을 선택하고 이름을 입력한다.

 

GitHub 레포지토리의 https URL을 등록한다.

 

 

위와 같이 SCM(Source Code Management)을 Git으로 선택하고 연동한다. 혹시나 'No ECDSA host key ~~~' 에러가 발생하면 최초 연결이 안 되어서 서명을 못 가져온 경우이니 아래와 같이 jenkins container에 한번 접속해서 ssh로 git 커맨드를 수행한 뒤 실행하면 된다. 또는 ~/.ssh/config에서 특정 호스트에 대한 StrictHostKeyChecking을 no로 설정하는 것도 하나의 방법이다.

 

$ docker exec -it <jenkins 컨테이너ID> git clone <git repo ssh>
yes

 

Script Path는 GitHub 레포지토리를 시작 경로로 하여 Jenkinsfile이 위치한 경로를 명시한다. Branches to build에는 빌드 타깃이 되는 브랜치를 명시한다.

 

cf. GitHub에서 default 브랜치에 push 될 때 webhook을 기반으로 파이프라인이 동작되도록 하고 싶다면 레포지토리 설정에서 webhook -> Payload URL에 jenkins가 있는 'Public IP:Port/web-hook/'을 입력하고 jenkins에서는 item수정 -> Build Triggers -> GitHub hook trigger for GITScm polling을 선택해주면 된다.

 

 

2. Jenkins CI/CD Job 수행 및 Docker 빌드 및 배포

Jenkinsfile 작성

def maindir="." 
def deployHost="CD서버 IP주소"
def dockerUser="도커허브 유저"
def dockerRepo="도커허브 레포지토리"
def imageTag="태그"
def dockerPasswd="도커허브 비밀번호"

pipeline {
    agent any

    stages { 
        stage('Pull Codes') {
            steps {
                checkout scm 
            }
        }
        stage('Build Codes') { 
            steps {
                sh """
                cd ${maindir}
                ./gradlew clean build
                """
            }
        }
        stage('Build Docker Image & Push to Docker Hub') {
            steps {
                sh """
                ./gradlew jib -Djib.to.auth.username=${dockerUser} -Djib.to.auth.password=${dockerPasswd} -Djib.to.image=${dockerUser}/${dockerRepo}:${imageTag} -Djib.console='plain'
                """
            } 
        }
        stage('Deploy to CD server') {
            steps {
                sshagent(credentials : ["jenkins-cd-ssh"]) {
                    sh "ssh deployer@${deployHost} \
                    'sleep 3; \
                    docker stop `docker container ps -q -f ancestor=${dockerUser}/${dockerRepo}:${imageTag}`; \
                    docker pull ${dockerUser}/${dockerRepo}:${imageTag}; \
                    docker run -d -p 61616:8080 -t ${dockerUser}/${dockerRepo}:${imageTag};'"
                }
            }
        }
    }
}

1) agent any  

파이프라인 Job 실행을 진행할 서버를 지정하는 부분이다. 예를 들어 jenkins가 master-slave구조로 되어 있다면 Slave에서도 Job이 돌아갈 수도 있고 master에도 Job이 돌 수 있다. 현재 Jenkins가 master-slave 구조가 아니고 Jenkins서버 자체를 사용할 것이기 때문에 any로 하면 해당 Jenkins가 위치한 서버에서 Job이 수행된다.

 

2) checkout scm

jenkins에서 'pipeline설정'에서 설정한 SCM(GitHub)의 브랜치로부터 소스코드를 가져온다.

 

3) Build Codes

빌드 과정에는 빌드뿐만 아니라 단위 테스트, 정적 분석 등을 포함시킬 수 있다.

 

4) Djib.console='plain'

jib에 대한 로그를 젠킨스의 콘솔에 남기기 위한 옵션이다.

 

5) -Djib.to.image=${dockerUser}/${dockerRepo}:${imageTag}

build.gradle에서 jib 필드 부분에 직접 <도커유저>/<레포이름>:<tag>를 넣어도 되지만 빌드할 때마다 동적으로 변경하기 위해 빌드 옵션으로 따루 빼두었다.

 

6) ./gradlew jib

gradle에 jib 플러그인을 추가하고 jib 빌드를 하는 부분이다. 처음에는 docker 커맨드를 통해 빌드를 하고 도커 허브에 Push했지만, 현재 Jenkins가 컨테이너로 띄워져 있고 컨테이너 안에서는 docker 커맨드를 사용할 수 없다. 그래서 위 Jenkins 컨테이너를 run 시키는 부분에서 호스트에 있는 도커 데몬의 유닉스 소켓을 마운트해두었던 이유였다. 하지만 jib을 기반으로 하면 호스트의 도커 유닉스 소켓을 마운트하지 않고도 jib 플러그인 자체적으로 컨테이너 이미지를 빌드하고 특정 컨테이너 레지스트리에 push까지 해준다. 즉, docker 커맨드 기반이 아니라는 것이다. 

 

cf. jib에 대한 포스팅: https://jh-labs.tistory.com/509

 

7) Deploy to CD server

배포 부분은 CI서버에서 SSH로 CD서버에 접속해 컨테이너 레지스트리로부터 이미지를 Pull받고 실행하는 과정으로 구축했다. 특히 docker stop 부분에서 기존에 실행 중이던 컨테이너를 중지하는데, stop할 컨테이너가 0개로 잡히면 에러가 발생하고 이후 과정이 진행되지 않는다. 이 부분은 추후 수정할 예정이다. 또한 새로운 변경사항을 레지스트리로부터 pull받기 위해 docker pull 커맨드를 사용했다. (docker run만 했을 경우 로컬에 이미지가 존재한다면 레지스트리로부터 pull받지 않음)

 

 

 

CI/CD 파이프라인 토폴로지

 

cf. 4-1. Pull Artifact

gradle의 빌드 스크립트에서는 컴파일, 빌드 시 코드 의존성 참조를 위해 artifact를 artifact 레포지토리에서 가져온다. 이러한 artifact를 포함하여 코드 빌드가 진행된다.

 


이슈 처리

1. docker in docker

jenkins에서 jib플러그인이 Docker Hub로 push하기 위해 docker login 커맨드가 사전에 필요하다. jenkins가 컨테이너로 띄워져 있는 상황이기 때문에 컨테이너 안에서 docker 커맨드를 입력해야 하는 상황인 것이다.

 

참고링크: https://devopscube.com/run-docker-in-docker/

 

위 링크에서는 컨테이너에서 docker 커맨드를 사용하는 3가지 방법을 제시한다. 그중 하나가 /var/run/docker.sock:/var/run/docker.sock으로 마운트해주는 방법이고 이를 적용했으나 jib을 기반으로 빌드하는 것이 여러 측면에서 더 좋다는 것을 알게 되어 jib을 기반으로 빌드하고 레지스트리에 push했다.

 

 

2. CD 서버에서 SSH 접속이 거부

 

ssh-keygen을 할 때 실수로 서버에서 키를 생성해주었던 게 문제였다. ssh key는 ssh로 접속할 클라이언트에서 생성하고 private key는 클라이언트에, public key는 서버에 등록해두어야 한다. 서버에 public key를 등록할 때에는 ssh로 접속하려는 리눅스 유저의 홈 디렉토리 아래 .ssh의 authorized_keys 파일(~./ssh/authorized_keys)에 등록한다.

 

 

즉, jenkins가 SCM으로부터 코드를 가져오는 작업에서도 SSH client 입장이고(WebHook 미사용일 경우) CD서버에 접속하는 부분에서도 SSH client 입장인 것이다. 따라서 두 작업에 필요한 SSH private key는 모두 jenkins 쪽에 있어야 한다.

 

cf. known_hosts에는 내가 다른 서버에 접속할 때, 그 서버의 public key가 등록되고 authorized_keys는 다른 컴퓨터가 client로서 내 서버에 접속하려고 할 때, 그 client의 public key다. known_hosts는 해당 서버에 최초 접속 시 자동으로 등록이 되고 authorized_keys는 직접 등록을 해야 해당 클라이언트가 이 컴퓨터로 접속이 가능해진다.

 

 

3. Java version 문제

현재 jenkins 이미지는 latest 태그가 붙은걸 실행 중인데, Java 17을 사용하는 애플리케이션을 gradle로 빌드하는데서 문제가 발생했다. 빌드 과정 중 compileJava 과정에서 FAILED가 발생한 것이다. 원인을 파악해보니 현재 Jenkins 컨테이너는 Java 11을 사용 중이었다. Jenkins를 아예 Java 17로 실행하는 방법은 아래 이미지를 시작하면 된다.

docker pull jenkins/jenkins:latest-jdk17

 

또는 실행 중인 Jenkins 컨테이너에서 Java 17로 올리는 방법은 아래와 같다.

 

컨테이너에 접속해 jdk를 새로 설치하고 JAVA_HOME 환경변수를 설정한다. 모든 리눅스 유저에 적용하고 싶다면 /etc/profile에, 특정 유저에만 적용하고 싶다면 ~/.bashrc에 아래 내용을 append한다.

 

Jenkins 설정에서 Global Tool Configuration에 들어가 설치한 JDK를 설정한다.

 

 

Jenkinsfile에서 해당 파이프라인에서 사용할 jdk를 명시할 수 있다.

pipeline {
    agent any
    
    tools {
        jdk 'jdk17-arena'
    }
    
    stages { 
    }
    
}

 

 

 

 

 

 

Reference

- https://www.lambdatest.com/blog/jenkins-pipeline-tutorial/

- Jenkins에서 Java 버전 변경, https://royleej9.tistory.com/entry/Jenkins-jdk-%EC%84%A4%EC%A0%95