CI-CD

CI를 위한 Jenkins, GitHub, Docker Hub 연계

Operation CWAL 2021. 2. 20. 18:10

본격적인 CI 구성을 위해 Jenkins와 GitHub 그리고 Docker Hub를 연계하는 방법에 대해 설명한다. k8s 위에 Jenkins를 배포하는 방법에 대해선 이전 포스트를 참고한다.

 

실습 환경에서 CI는 다음과 같은 일련의 순서로 이루어진다고 가정하자.

 

1. 개발자가 자신이 작업한 코드를 GitHub Repository에 반영

2. Jenkins는 해당 Repository를 Checkout하여 Docker Image로 빌드

3. 빌드 완료 후 Docker Hub로 Image를 Push

 

실제 CI 과정에선 코드 리뷰나 테스트 등의 과정이 포함되어야 하나 이번 실습에선 생략하도록 한다.

 

 

우선은 Jenkihns가 GitHub에서 Code를 가져올 수 있도록 Credential을 추가하자. GitHub와 Jenkins 간의 연결은 ID/PWD 방식 대신 SSH를 통한 인증을 사용한다. 

 

Jenkins Master 서버의 Container 안으로 들어가서 SSH Key를 생성한다.

kubectl exec -it -n ci jenkins-0 -- bash

ssh-keygen 명령어를 통해 SSH Key Pair를 생성할 수 있다.

Public Key(/var/jenkins_home/.ssh/id_rsa.pub) 파일 내용은 GitHub > 'Accout settings' > 'SSH and GPG keys' 메뉴를 통해 등록할 수 있다.

GitHub SSH Key 등록

Private Key(/var/jenkins_home/.ssh/id_rsa) 파일 내용은 Jenkins의 Credential로 추가해야 한다. 'Jenkins 관리' > 'Manage Credentials' 메뉴로 이동 후 'Store scoped to Jenkins' 항목에서 아래와 같이 Jenkins Store에 새로운 Credential을 추가한다. Credential ID와 Private Key 값만 입력하고 나머지는 공백으로 놔둔다.

 

 

Docker Hub는 SSH Key 대신 'Username with password' 타입의 Credential을 추가한다. Docker Hub 로그인시 사용하는 ID와 비밀번호를 의미한다.

 

GitHub, Docker Hub 연결을 위한 Credential 생성을 완료했다면, 이제 본격적으로 GitHub에 Repository를 추가한다.

어떤 소스코드든 Dockerfile이 존재하고 이를 통해 정상적으로 빌드가 된다면 현재 환경에서 바로 적용 가능하다. 필자는 누군가가 이미 Node.js로 구현한 간단한 'Hello world'의 Docker 예제 Repository를 찾아 이를 Fork하였다.

 

이제 Jenkins에서 해당 Repository를 체크아웃 후 바로 빌드할 수 있도록 아래와 같이 Jenkins 파이프라인을 정의한 스크립트 'Jenkinsfile'을 작성하여 해당 Repository에 추가한다.

podTemplate(label: 'docker-build', 
  containers: [
    containerTemplate(
      name: 'git',
      image: 'alpine/git',
      command: 'cat',
      ttyEnabled: true
    ),
    containerTemplate(
      name: 'docker',
      image: 'docker',
      command: 'cat',
      ttyEnabled: true
    ),
  ],
  volumes: [ 
    hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'), 
  ]
) {
    node('docker-build') {
        def dockerHubCred = <your_dockerhub_cred>
        def appImage
        
        stage('Checkout'){
            container('git'){
                checkout scm
            }
        }
        
        stage('Build'){
            container('docker'){
                script {
                    appImage = docker.build("<your-dockerhub-id>/node-hello-world")
                }
            }
        }
        
        stage('Test'){
            container('docker'){
                script {
                    appImage.inside {
                        sh 'npm install'
                        sh 'npm test'
                    }
                }
            }
        }

        stage('Push'){
            container('docker'){
                script {
                    docker.withRegistry('https://registry.hub.docker.com', dockerHubCred){
                        appImage.push("${env.BUILD_NUMBER}")
                        appImage.push("latest")
                    }
                }
            }
        }
    }
    
}

가장 먼저 주목해야할 내용은 podTemplate이다. 빌드 수행할 Remote Agent를 정의하는 곳으로 빌드 시작과 동시에 Jenkins Kubernetes 플러그인을 통해 Pod이 생성된다. 우리는 소스코드를 체크아웃할 git과 이미지 빌드, Push를 위한 docker만 사용하므로 해당 Pod에는 2개의 container만 필요하다. 참고로 Host에 위치한 Docker Daemon의 Unix Domain Socket(/var/run/docker.sock)을 Container에 마운트하여 Container 안에서 docker 명령어를 수행할 수 있다.

podTemplate을 통해 다양한 형태의 Remote Agent를 정의할 수 있으므로, 더 자세한 정보가 필요하다면 아래 링크를 참고하자.

www.jenkins.io/doc/pipeline/steps/kubernetes/#podtemplate-define-a-podtemplate-to-use-in-the-kubernetes-plugin

 

해당 파이프라인은 총 4단계로 구성되어 있다.

  • Checkout: git 명령어로 Repository를 가져온다
  • Build: Repository 안에 있는 Dockerfile을 사용하여 이미지를 빌드한다. 이미지 이름은 가칭 'node-hello-world'
  • Test: 빌드한 이미지로부터 임시 컨테이너를 생성 후 내부에서 지정된 명령어를 수행한다
  • Push: 빌드한 이미지를 Docker Hub로 Push한다. 이때 현재 Build Number를 이미지 태그로 사용하며 latest 이미지도 갱신.

참고로 Pod에 존재하는 모든 Container가 동일한 workspace 볼륨을 공유하기 때문에 다른 container에서 작업한 결과물을 다음 단계의 container에서도 연속적으로 사용 가능하다.

이제 이 파일을 저장, Commit 후 GitHub에 Push했다면 Jenkins Job을 추가할 차례다. 먼저 아래와 같이 Pipeline Job을 하나 생성하자.

해당 Job 구성은 아래와 같이 설정한다.

 

  • GitHub project: 빌드할 소스코드와 Jenkinsfile이 위치한 GitHub 프로젝트 URL(.git 생략)
  • Poll SCM: 5분마다 Repository를 polling하도록 설정("H/5 * * * *")한다. 실제 환경이라면 Poll SCM 대신 GitHub Webhook을 통해 빌드 트리거를 설정해야 하지만, 현재 환경은 외부에서 Jenkins에 접근할 수 없으므로 이 방식을 사용한다.

마지막으로 Pipeline 설정이다. 체크아웃할 Repository에 이미 Pipeline Script이 존재하므로 'Pipeline script from SCM' 항목을 선택한다. Repository URL은 해당 Repository URL(.git 포함)을 입력하고, Credential은 미리 생성한 GitHub Credential을 사용한다. 빌드할 대상 브랜치는 master이다.

 

이제 우리가 예상하는대로 빌드가 되는지 확인해보자. 'Build Now'를 클릭하면 바로 빌드를 시작한다.

Checkout 부터 Push까지 모든 과정이 문제없이 성공한 것으로 나온다. 그럼 Image가 실제로 Docker Hub에 올라가 있는걸까? 직접 들어가서 확인해보았다.

Build Number가 #1이었고 latest 태그도 추가하기로 했으니 우리가 Jenkinsfile에 정의한 내용과 일치한다. 이로써 기본적인 CI 과정은 기본적인 뼈대는 구현한 셈이다.

 

다음 포스트는 이전에 설명한 적이 있는 Argo CD와 연동하여 CI/CD 파이프라인을 완성하는 과정을 설명할 예정이다.

 

참고
Node.js "Hello world"

 

 

'CI-CD' 카테고리의 다른 글

Jenkins Pipeline  (0) 2021.03.01
kustomize를 활용한 Manifest 관리  (0) 2021.02.24
CD를 위한 Jenkins, Argo CD 연계  (0) 2021.02.21
Kubernetes 위에 Jenkins 설치하기  (0) 2021.02.19
ArgoCD: Kubernetes에 GitOps 적용하기  (2) 2021.02.17