Jenkins pipeline, ArgoCD로 K8S배포 자동화 하기 - 1

들어가며

 

k8s는 구축했지만 실제 애플리케이션이 k8s에 자동 배포되도록 해야했다.

해당 포스팅에서 사용된 환경은 다음과 같다.

 

  • Spring boot
  • Maven
  • Jenkins-Pipeline
  • ArcoCD
  • GitHub
  • DockerHub
  • K8S

 

 

시나리오

 

구축하고자하는 배포 플로우의 시작점과 끝은 단순하다.

사용자가 my-app이라는 애플리케이션의 수정사항을 원격 브랜치로 push를 하면 k8s에 배포가 자동화되면 된다.

 

실제 구축하고자하는 논리적인 배포 플로우의 형태는 아래의 그림과 같다.

 

 

 

구축 단계

 

  1. Jenkins Pipeline & Github webhook 설정
  2. Jenkinsfile 작성
  3. ArgoCD 구축

 

 

1. Jenkins Pipeline & Github webhook 작성

 

아래와 같이 Pipeline 타입으로 아이템을 생성한 뒤 Pipeline 내용을 설정하면된다.

 

 

Pipeline이란?

 

Job들을 순차적 혹은 병렬적으로 실행시키거나 작성한 스크립트로 이벤트들을 연속적으로 실행시키는 등의 일을 지원하는 기능이다.

더 간단하게 말하면 Job 하나 작성할 때 대부분 GUI로 제공받아 마우스로 체크해서 설정했다면 Pipeline은 이러한 내용들을 스크립트를 통해 더 딥하고 유연하게 작성할 수 있다. 즉 작성하려는 배포 플로우가 엄청 간단한건 아니라서 pipeline 스크립트로 작성해야한다.

 

 

Build Triggers - GitHub hook Trigger for GITScm polling : github repository의 이벤트를 감지하는 설정이며 아래의 내용으로 Github 설정을 진행한다.

 

Repository -> Settings -> Webhooks -> Add webhook 순으로 진행한다.

 

Jenkins가 push 이벤트 감지하도록  Pushes에 체크한다.

 

 

Repository URL : Pipeline을 실행하는 스크립트가 담긴 Github Repository 명시.

Credentials : jenkins에서 github private repository 에 접근하기위한 Credential 이며 설정법은 아래의 포스팅 내용중에 존재하니 참고한다.

https://hwannny.tistory.com/89

 

[Jenkins] Jenkins로 자동 빌드 배포 환경 구축하기

들어가며 회사에서는 이미 빌드 배포환경이 구축되어있기 때문에 구축해볼 기회가 없어 사이드프로젝트에 직접 구축해보면 좋을것같아 구축한 내용을 포스팅하고자한다. 해당 포스팅은 다음

hwannny.tistory.com

 

Branch Specifier : Pipeline을 실행하는 스크립트가 담긴 브랜치명 명시.

Script Path : Github repository에서 Pipeline을 실행하는 스크립트가 담긴 파일의 경로를 명시 (이미지에서는 Root에 위치했으므로 파일명만 명시)

위와 같이 설정이 완료가 됐다면 개발자가 my-app repository의 원격 develop 브랜치에 변경사항을 push할 경우 jenkins가 push 이벤트를 감지하여 repository root 경로 내 Jenkinsfile의 내용을 Pipeline으로 실행할 것이다.

 

 

2. Jenkinsfile 작성

 

Pipeline이 수행하는 핵심 배포 플로우 스크립트를 Jenkinfile에 작성해보자.

 

Pipeline 문법의 종류는 두가지(Scripted, Declarative)가 존재하지만 해당 포스팅에서는 Declarative 문법을 사용한다.

 

Stage - Checkout Application Git Branch

 

Jenkins 에서 배포하고자하는 my-app 소스코드를 가져오기위해 Check out 해서 clone을 하는 stage를 작성한다.

 

pipeline {
  agent any
  tools {
    maven 'M3'
  }

  stages {

    stage('Checkout Application Git Branch') {
        steps {
            git credentialsId: '{Credential ID}',
                url: 'https://github.com/best-branch/my-app.git',
                branch: 'develop'
        }
        post {
                failure {
                  echo 'Repository clone failure !'
                }
                success {
                  echo 'Repository clone success !'
                }
        }
    }
  }
  
}

 

 

private repository에 접근하기 위해서는 credentialsId 를 명시해야하는데 내용은 아래와 같다.

 

참고로 Git Hub에서 ID/PW기반의 Basic authentication 인증을 금지하고, ID/Personal access token 방식의 Token authentication 인증을 요구하고 있다. 그러므로 Personal access token을 발급해보자.

 

Github Profile -> Settings -> Developer Settings -> Personal access token -> Generate new token 버튼을 클릭한 후

 

 

토큰 유효기간을 나타내는 Expiration과 토큰의 권한을 부여하는 scopes를 설정한다.

단순히 repository만 관리할거라면 repo에만 체크해도 충분하다.

 

발급 완료 후 access token 값을 복사하여 jenkins credential로 등록하여 사용하면된다.

 

 

Stage - Maven Jar Build

 

가져온 my-app 소스코드를 Maven으로 Jar를 생성하는 stage를 작성한다.

 

pipeline {
  agent any
  tools {
    maven 'M3'
  }

  stages {

    stage('Checkout Application Git Branch') {
		...
    }

   stage('Maven Jar Build') {
        steps {
            sh 'mvn -DskipTests=true package'
        }
        post {
                failure {
                  echo 'Maven jar build failure !'
                }
                success {
                  echo 'Maven jar build success !'
                }
        }
    }
  }
}

 

위 내용대로 Maven으로 Jar로 패키징하면 centos 7, jenkin 2.2777.4 기준으로 아래의 경로에 생성된다. (다른 버전도 같은지는 모르겠다.)

 

/var/lib/jenkins/workspace/{jenkins-item명}/target/ROOT.jar

 

 

Stage - Docker Image Build

 

k8s 배포에 사용할 Docker image를 빌드 할 때 앞서 생성한 Jar을 담는 stage를 작성한다.

 

pipeline {
  agent any
  tools {
    maven 'M3'
  }

  stages {

    stage('Checkout Application Git Branch') {
		...
    }

    stage('Maven Jar Build') {
        ...
    }
    
    stage('Docker Image Build') {
        steps {
            sh "cp target/ROOT.jar ./"
            sh "cp deploy/Dockerfile ./"
            sh "docker build . -t ${dockerHubRegistry}:${currentBuild.number}"
            sh "docker build . -t ${dockerHubRegistry}:latest"
        }
        post {
                failure {
                  echo 'Docker image build failure !'
                }
                success {
                  echo 'Docker image build success !'
                }
        }
    }
    
  }
}

 

Dockerfile은 my-app 프로젝트 내에 작성 해두었다. 이를 참조하여 docker image를 빌드한다.

Dockerfile 내용은 아래와 같다.

 

FROM adoptopenjdk/openjdk8

ARG HOST_JAR_FILE_PATH=./ROOT.jar # Jar 경로 환경변수 설정.
COPY ${HOST_JAR_FILE_PATH} ./ # Maven을 통해 패키징된 Jar을 Docker image에 포함시킨다.

# 해당 Docker image로 Container를 생성/실행하는 시점에 아래의 커맨드가 수행되도록한다.
CMD ["java", "-Dspring.profiles.active={e.g. beta, release}", "-jar", "./ROOT.jar"]

 

 

Stage - Docker Image Push

 

빌드된 Docker image 를 DockerHub에 push하는 stage를 작성한다.

 

pipeline {
  agent any
  tools {
    maven 'M3'
  }
  
  environment {
    dockerHubRegistry = '{DockerHub계정명}/{DockerHub repository명}'
    dockerHubRegistryCredential = '{Credential ID}'
  }

  stages {

    stage('Checkout Application Git Branch') {
		...
    }

    stage('Maven Jar Build') {
        ...
    }
    
    stage('Docker Image Build') {
        ...
    }
    
    stage('Docker Image Push') {
        steps {
            withDockerRegistry([ credentialsId: dockerHubRegistryCredential, url: "" ]) {
                                sh "docker push ${dockerHubRegistry}:${currentBuild.number}"
                                sh "docker push ${dockerHubRegistry}:latest"

                                sleep 10 /* Wait uploading */ 
                            }
        }
        post {
                failure {
                  echo 'Docker Image Push failure !'
                  sh "docker rmi ${dockerHubRegistry}:${currentBuild.number}"
                  sh "docker rmi ${dockerHubRegistry}:latest"
                }
                success {
                  echo 'Docker image push success !'
                  sh "docker rmi ${dockerHubRegistry}:${currentBuild.number}"
                  sh "docker rmi ${dockerHubRegistry}:latest"
                }
        }
    }
   
  }
}

 

environment가 추가됐는데 이는 Jenkins에서 DockerHub내 Private Repository에 접근하기 위해서다.

 

Jenkins에서 Credential을 Username with password 타입으로 선택하고 DockerHub ID/PW을 기입 후 생성된 Credential ID를 사용하면 된다.

 

Stage - K8S Manifest Update

 

DockerHub에 push된 docker image를 어떻게 k8s 클러스터 내에다 배포할지 고민하다가 ArgoCD라는 툴이 있어 이를 이용하고자 했다.

 

 

ArgoCD란?

 

k8s에서 사용하는 manifest.yaml 파일들을 github repository로 관리하고 변경이 감지되면 k8s에 변경된 manifest 내용으로 업데이트하는 역할을 수행한다. 즉, github menifest repository와 k8s 싱크를 맞춰준다.

 

 

위 그림에서 Manifest update가 Code push하는 부분을 해당 포스팅에서는 Pipeline 마지막 stage에서 수행된다고 볼 수 있다.

 

pipeline {
  agent any
  tools {
    maven 'M3'
  }
  
  environment {
    ...
  }

  stages {

    stage('Checkout Application Git Branch') {
		...
    }

    stage('Maven Jar Build') {
        ...
    }
    
    stage('Docker Image Build') {
        ...
    }
    
    stage('Docker Image Push') {
        ...
    }
    
    stage('K8S Manifest Update') {
        steps {
            git credentialsId: '{Credential ID}',
                url: 'https://github.com/best-branch/k8s-manifest.git',
                branch: 'master'

            sh "sed -i 's/my-app:.*\$/my-app:${currentBuild.number}/g' deployment.yaml"
            sh "git add deployment.yaml"
            sh "git commit -m '[UPDATE] my-app ${currentBuild.number} image versioning'"
            sshagent(credentials: ['{k8s-manifest repository credential ID}']) {
                sh "git remote set-url origin git@github.com:best-branch/k8s-manifest.git"
                sh "git push -u origin master"
             }
        }
        post {
                failure {
                  echo 'K8S Manifest Update failure !'
                }
                success {
                  echo 'K8S Manifest Update success !'
                }
        }
    }
   
  }
}

 

Credential ID는 Checkout Application Git Branch Stage에서 사용된것과 동일하다.

k8s-manifest repository credential ID 키 설정법은 아래의 포스팅 내용중에 존재하니 참고한다.

https://hwannny.tistory.com/89

 

[Jenkins] Jenkins로 자동 빌드 배포 환경 구축하기

들어가며 회사에서는 이미 빌드 배포환경이 구축되어있기 때문에 구축해볼 기회가 없어 사이드프로젝트에 직접 구축해보면 좋을것같아 구축한 내용을 포스팅하고자한다. 해당 포스팅은 다음

hwannny.tistory.com

 

ArgoCD가 k8s에 배포를 하게끔 하려면 앞서 언급했던 내용대로 Github manifest repository에 manifest파일을 수정 후 push해야한다.

 

 

ArgoCD와 바인딩할 manifest repository를 미리 작성해둬야한다.

미리 작성해둘 service.yaml, deployment.yaml 내용은 아래와 같다.

 

# service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: ClusterIP
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

 

# deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  selector:
    matchLabels:
      app: my-app
  replicas: 2
  template:
    metadata:
      labels:
        app: my-app
    spec:
      imagePullSecrets:
        - name: {k8s 내에서 생성한 secret 명, dockerhub 내 private repository에 접근하기 위함.}
      containers:
        - name: my-app
          image: {Dockerhub 계정명}/my-app:{Jenkins build number}
          ports:
            - containerPort: 8080

 

k8s 클러스터는 pod 배포를 관리하기 위해 deployment를 이용한다.

 

K8S Manifest Update stage에서는 이 deployment 설정 파일을 아래와 같이

 

sh "sed -i 's/my-app:.*\$/my-app:${currentBuild.number}/g' deployment.yaml"

 

수정한 후 push하면 ArgoCD가 감지하여 앞선 stage에서 아래처럼 DockerHub에 push된 docker image를

 

sh "docker push ${dockerHubRegistry}:${currentBuild.number}"

 

pull하여 k8s에 배포하도록 한다.

 

즉, jenkins의 build number가 Docker image의 새 태그 버전이 된다고 보면된다.

 

(ArgoCD 구축은 다음 포스팅에 진행하고자 한다.)

 

Jenkinsfile 전체 내용은 아래와 같다.

 

pipeline {
  agent any
  tools {
    maven 'M3'
  }
  environment {
    dockerHubRegistry = '{DockerHub계정명}/{DockerHub repository명}'
    dockerHubRegistryCredential = '{Credential ID}'
  }
  stages {

    stage('Checkout Application Git Branch') {
        steps {
            git credentialsId: '{Credential ID}',
                url: 'https://github.com/best-branch/my-app.git',
                branch: 'develop'
        }
        post {
                failure {
                  echo 'Repository clone failure !'
                }
                success {
                  echo 'Repository clone success !'
                }
        }
    }

   stage('Maven Jar Build') {
        steps {
            sh 'mvn -DskipTests=true package'
        }
        post {
                failure {
                  echo 'Maven jar build failure !'
                }
                success {
                  echo 'Maven jar build success !'
                }
        }
    }

    stage('Docker Image Build') {
        steps {
            sh "cp target/ROOT.jar ./"
            sh "cp deploy/Dockerfile ./"
            sh "docker build . -t ${dockerHubRegistry}:${currentBuild.number}"
            sh "docker build . -t ${dockerHubRegistry}:latest"
        }
        post {
                failure {
                  echo 'Docker image build failure !'
                }
                success {
                  echo 'Docker image build success !'
                }
        }
    }

    stage('Docker Image Push') {
        steps {
            withDockerRegistry([ credentialsId: dockerHubRegistryCredential, url: "" ]) {
                                sh "docker push ${dockerHubRegistry}:${currentBuild.number}"
                                sh "docker push ${dockerHubRegistry}:latest"

                                sleep 10 /* Wait uploading */ 
                            }
        }
        post {
                failure {
                  echo 'Docker Image Push failure !'
                  sh "docker rmi ${dockerHubRegistry}:${currentBuild.number}"
                  sh "docker rmi ${dockerHubRegistry}:latest"
                }
                success {
                  echo 'Docker image push success !'
                  sh "docker rmi ${dockerHubRegistry}:${currentBuild.number}"
                  sh "docker rmi ${dockerHubRegistry}:latest"
                }
        }
    }

    stage('K8S Manifest Update') {
        steps {
            git credentialsId: '{Credential ID}',
                url: 'https://github.com/best-branch/k8s-manifest.git',
                branch: 'master'

            sh "sed -i 's/my-app:.*\$/my-app:${currentBuild.number}/g' deployment.yaml"
            sh "git add deployment.yaml"
            sh "git commit -m '[UPDATE] my-app ${currentBuild.number} image versioning'"
            sshagent(credentials: ['{k8s-manifest repository credential ID}']) {
                sh "git remote set-url origin git@github.com:best-branch/k8s-manifest.git"
                sh "git push -u origin master"
             }
        }
        post {
                failure {
                  echo 'K8S Manifest Update failure !'
                }
                success {
                  echo 'K8S Manifest Update success !'
                }
        }
    }
  }
}

 

 

 

마치며

 

해당 포스팅은 정리할 내용이 너무 많다 보니 생략한 내용 또한 무척 많은것 같다. 시간날 때 틈틈이 다시 읽어서 부족한 부분을 보충해야겠다.

 

 

다음 포스팅

 

https://hwannny.tistory.com/114

 

Jenkins pipeline, ArgoCD로 K8S배포 자동화 하기 - 2

들어가며 이전 포스팅에 이어 ArcoCD 구축하는 내용을 포스팅하고자 한다. https://hwannny.tistory.com/113 Jenkins pipeline, ArgoCD로 K8S배포 자동화 하기 - 1 들어가며 개념만 익혔던 k8s를 실제 프로젝트에..

hwannny.tistory.com