[CI/CD] Jenkins(2)

황서희·2023년 3월 8일
0
post-thumbnail

Jenkins

파이프라인 개념

젠킨스의 파이프라인(Pipeline as a Code)는 코드로서 파이프라인을 구현하는 것이다. CI/CD 를 하기 위해 버전 제어에서 최종 목표까지 소프트웨어를 가져오는 자동화된 프로세스를 지원하는 기능/플러그인이다. 파이프라인은 DSL을 통해 코드로 작성된다. Jenkinsfile은 Pipeline as a Code 를 구현하기 위한 dsl로 작성된 텍스트 파일이며 Git과 같은 소스 제어 저장소에 커밋할 수 있다.

물리적인 환경에서는 물리적으로 시스템을 세팅하는 등 해야했기 때문에 논리적인 구성이 어려웠다. 하지만 점점 기구축된 피지컬 시스템 위에 가상화된 인프라를 동적으로 만들기 쉬워지면서, 코드로서 원하는 형태를 정의하고 관리하는 것이 더 효율적으로 되었다. 코드로 관리하게 되면 추적이 가능하고, 버전 컨트롤 시스템을 이용하며 원하는 형태로 어떻게 변하는지 버전 관리를 할 수 있고, 코드의 재사용도 추구할 수 있다. 따라서 현재의 인프라는 코드로 배포하는 것이 기본이다.

선언적 방식의 파이프라인

일반적인 Jenkins의 웹 폼과 비슷하다. 비교적 가독성이 더 좋고, 블루오션 인터페이스를 사용 가능하다. 스크립트 방식의 파이프 라인에 비해서 문법 확인과 에러 확인이 비교적 쉽고 반복되는 로직에 대한 지원이 없다. 왜냐하면 프로그래밍 언어가 아니기 때문이다.

파이프라인 문법

서술적 파이프라인은 지시문과 섹션을 포함하는 블록으로 구성된다. 각 섹션에는 다른 섹션과 지시문, 스테이지, 스텝이 있을 수 있다.

pipeline {
  agent {label 'worker_node1'}  #작업을 실행할 장소
  stages {
    stage('Checkout') {    #단계 이름
      steps {
        git 'git@repository address'
      }
    }
    stage('Build') {
      steps {
        sh 'mvn clean package'
      }
    }
  }
}

Jenkins에는 다양한 파이프라인을 쉽게 생성할 수 있도록 문서 기능이 웹에 내장되어 있고, 플러그인을 설치하면 내장 문서도 관련된 내용이 자동으로 업데이트된다. 참고

서술적 파이프라인 구조: 블록

시작과 끝이 있는 코드의 묶음이다. 파이프라인의 모든 구성은 블록이지만, 일반적으로 pipeline 블록을 지칭한다. 서술적 파이프라인은 반드시 pipeline 블록으로 시작하기 때문이다.

pipeline {
	# code..
}

서술적 파이프라인 구조: 세션

파이프라인의 흐름 안에서 하나 이상의 지시문 또는 스텝을 묶은 것이다.

  • agent
    전체 파이프라인 또는 특정 단계가 실행되는 노드 지정. 가장 많이 볼 섹션 중 하나이다.
    • agent any
      모든 노드에서 실행된다.
    • agent none
      최상위 레벨에서 지정되면, 개별 스테이지에서 노드를 지정해야 한다.
    • agent { label "< LABEL >" }
      특정 레이블을 가진 에이전트에서 실행된다.
    • agent { docker { image '< IMAGE >'} }
      도커에서 특정 컨테이너 이미지를 실행하는 컨테이너에 실행된다.
    • 쿠버네티스의 특정 이미지를 실행하는 파드에서 실행되기도 한다.
agent {
  	kubernetes {
yaml '''
 apiVersions: v1
 kind: Pod
 spec: XXX 
 	'''
 	}
 }
  • stage
    하나 이상의 순서가 있는 stage 묶음. 가장 많이 볼 섹션 중 하나이다.
pipeline {
 agent any
 	stages {
 		stage('Example') {
 			steps { 
 				echo 'Hello World'
 			}
 		}
 	}
}
  • steps
    stage 지시문에서 실행할 하나 이상의 작업. git, sh, echo와 같은 DSL 문장을 넣을 수 있다.
    파이프라인 스텝 참조/검색

  • post
    stages 또는 stage의 마지막에 실행할 추가 작업. 전통적인 프리스타일 잡의 빌드 후 처리 동작과 비슷하다. stages 또는 steps를 실행한 후 상태에 따라 실행 여부를 결정할 수 있다.

    • always
      항상 실행한다.
    • changed
      현재 빌드 작업이 이전 빌드의 상태와 다른 경우 실행한다.
    • success
      현재 작업이 성공한 경우 실행한다.
    • failure
      현재 작업이 실패한 경우 실행한다 (실패 - 웹 인터페이스에서 빨간 색)
    • unstable
      현재 빌드 상태가 불안정(테스트 실패, 코드 이상)할 경우 실행한다. (불안정 - 웹 인터페이스가 노란색)
pipeline {
 	agent none 
 	stages {
 		stage('Example Build') {
 			agent { 
 				docker 'maven:3.8.1-adoptopenjdk-11'
 			} 
 			steps {
 				echo 'Hello, Maven'
 				sh 'mvn --version'
 			}
 		}
 		stage('Example Test') {
 			agent { 
 				docker 'openjdk:8-jre'
 			} 
 			steps {
 				echo 'Hello, JDK'
 				sh 'java -version'
 			}
 		}
 	}
 	post { 
 		always { 
 			echo 'I will always say Hello again!'
 		}
 	}
}

서술적 파이프라인 구조: 지시문

  • environment
    환경 변수를 정의하고, 자격증명을 참조한다. shell에서 참조할 수 있는 변수이다. stage 안에서 정의한 environment는 stage 안에서만 사용이 가능하고, pipleline에서 정의한 environment는 파이프라인 전체에서 사용이 가능하다. credentials() 메소드를 사용하여 미리 정의된 자격 증명에 접근할 수 있다.

  • option
    파이프라인에 적용할 값을 미리 정의한다.

  • parameter
    parameter는 젠킨스 내에서만 참조할 수 있는 변수이다. 파이프라인에서 프로젝트의 매개 변수를 정의 및 참조할 수 있다.

  • trigger
    파이프라인을 자동으로 실행할 트리거이다.

    • cron
      파이프라인이 트리거 되어야 하는 주기. git의 커밋과 상관없이 빌드한다.
      triggers { cron('H */4 * * 1-5') }
    • pollSCM
      새로운 소스 변경 사항이 있는지 확인하는 주기, git에 변화가 있으면 빌드한다. H(ash)는 균일한 부하를 생성하기 위함이다. 0 0 * * * 주기를 갖고 있는 파이프라인이 여러 개인 경우 특정 시각에 부하가 몰리게 되는데, 이것을 분산시켜준다.
      triggers { pollSCM('H */4 * * 1-5') }
    • upstream
      특정 파이프라인 잡이 실행되면, 후속으로 파이프라인을 실행할 수 있다. 하나의 프로젝트가 끝나면 당므 프로젝트가 바로 실행되도록 지정한다.
      triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) }
    • githubPush
      GitHub 저장소의 웹훅에 의해 트리거된다. Github Integration 플러그인을 설치해야 한다.
      triggers { githubPush() }
  • stage
    파이프라인에서 stages 섹션에 실행할 작업을 stage 지시문으로 순서 정의한다. stage 지시문에서는 실행할 작업을 steps에 정의한다.

pipeline {
 	stages {
 		stage('stage 1') {
 			steps { echo "step 1" }
 		}
 		stage('stage 2') {
 			steps { echo "step 2" }
 		}
 	}
 }
  • tools
    Jenkins의 "글로벌 도구" 에서 정의된 도구를 파이프라인에서 사용할 도구로 지정한다. agent none일 경우 무시된다. maven, jdk, gradle이 있다.
pipeline {
	agent any
    tools { maven 'apache-maven-3.0.1' {
  • input
    stage에서 사용자 입력 값의 요청을 정의한다.
  • when
    stage에서 step을 실행하기 위한 조건 정의이다. 조건문은 아니지만 그 비슷한 것이라고 생각하면 된다.
    • branch
      특정 브랜치인 경우
      when { branch 'master' }
    • environment
      환경 변수의 값과 일치하는 경우
      when { environment name: 'DEPLOY_TO', value: 'production' }
    • expression
      Groovy 표현식이 참인 경우
      when { expression { return params.DEBUG_BUILD } }
    • allOf
      조건이 모두 참인 경우
      when { allOf { branch 'master'; environment name:
      'DEPLOY_TO', value: 'production' } }
    • anyOf
      조건 중 하나가 참인 경우
      when { anyOf { branch 'master'; branch 'staging' } }
    • not
      조건이 거짓인 경우
      when { not { branch 'master' } }

파이프라인 작성

Jenkins 아이템 추가에서 유형을 Pipeline으로 하고 생성한다. 프로젝트의 이름과 SCP의 WorkSpace 주소를 같게 해야 한다. 다음과 같은 서술형 파이프라인을 작성하고 Apply 후 저장한다.

pipeline {
  agent { label 'jenkins-node' }

  stages {
    stage('Checkout') {
      steps {
        git branch: 'main', url: 'github url'
      }
    }

    stage('Maven Build') {
      steps {
        sh 'mvn clean package'
      }
    }

    stage('Deploy to Tomcat') {
      steps {
        sh 'scp /var/lib/jenkins/workspace/maven_pipeline/target/hello-world.war ubuntu@private ip:/var/lib/tomcat9/webapps'
      }
    }
  }
}

Jenkinsfile 작성

Pipeline Script from SCM을 이용해 github 저장소와 연동한다. Jenkinsfile에 trigger와 parameter로 작성된 tomcat 주소, 작업 디렉터리를 환경변수로 사용한다.

vi Jenkinsfile

pipeline {
  agent { label 'jenkins-node' }
   
  triggers {
    pollSCM '* * * * *'
  }
  
  
  parameters {
    string defaultValue: 'ubuntu', name: 'TOMCAT_USER_ID'
    string defaultValue: 'private ip', name: 'TOMCAT_IP'  
    string defaultValue: '/var/lib/tomcat9/webapps', name: 'TOMCAT_WEBAPP_DIR'
  }

  stages {
    stage('Checkout') {
      steps {
        git branch: 'main', url: 'github url'
      }
    }

    stage('Maven Build') {
      tools { maven 'Maven-3' }  # maven 툴 지정
      steps {
        sh 'mvn clean package'
      }
    }

    stage('Deploy to Tomcat') {
      steps {
        sh "scp $(env.WORKPASCE)/target/hello-world.war ${params.TOMCAT_LOGIN_USER}@${params.TOMCAT_IP}:${params.TOMCAT_WEBAPP_DIR}"
      } #절대경로
    }
  }
}

trigger의 존재를 인식시키기 위해 한 번은 수동으로 빌드를 실행시켜줘야 한다. 여기서, 홑따옴표는 글자 그대로 명령어를 인식해 실행시키고, 쌍따옴표는 특수문자를 명령으로 적용해 특수문자와 함께 실행시킨다.

Docker 에서의 Jenkins 통합

참고

EC2 인스턴스에 설치한 Docker를 통해 Jenkins를 컨테이너로 설치할 것이다. Build할 때 필요한 Node는 동적으로 생성 및 삭제할 것이고, 최종적으로 이미지를 빌드해 docker-hub에 push 후 다시 가져와 배포를 진행할 것이다.

새로 생성한 Jenkins-Docker 인스턴스(t3.medium, 40g 스토리지)에 설정한다.

sudo hostnamectl set-hostname jenkins-docker
exec bash
sudo usermod -aG docker $USER #user에 docker그룹 부여
exit
docker network create jenkins #도커 네트워크 생성
docker container run --name docker-dind --detach \
 --privileged --network jenkins --network-alias docker \
 --env DOCKER_TLS_CERTDIR=/certs \
 --volume jenkins-docker-certs:/certs/client \
 --volume jenkins-data:/var/jenkins_home \
 --volume docker:/var/lib/docker \
 --publish 2376:2376 \
 --restart=always \
 docker:dind --storage-driver overlay2 #docker in docker 컨테이너 실행

jenkins/jenkins 이미지에 docker 명령을 설치하고 필수 플러그인을 설치한다

mkdir jenkins
cd jenkins
vi Dockerfile

FROM jenkins/jenkins:lts-jdk11 #java11 이 설치되어 잇는 jenkins 이미지
USER root #root권한
RUN apt-get update && apt-get install -y lsb-release
RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \
 https://download.docker.com/linux/debian/gpg
RUN echo "deb [arch=$(dpkg --print-architecture) \
 signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \
 https://download.docker.com/linux/debian \
 $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
RUN apt-get update && apt-get install -y docker-ce-cli
USER jenkins #플러그인 설치
RUN jenkins-plugin-cli --plugins "docker-plugin docker-workflow

docker image build -t jenkins-docker:lts-jdk11 .

안에 Docker 명령어가 존재하는 Jenkins 컨테이너를 실행한다. env는 docker 클라이언트가 참조하는 환경변수이고, jenkins-docker:lts-jdk11은 docker in docker 컨테이너를 의미한다. jenkins에서 docker 명령을 실행하면 docker의 가짜 서버라고 할 수 있는 DinD로 통신이 가고, DinD는 가장 큰 도커 데몬으로 통신을 연결한다. Jenkins 역시 호스트에 있는 도커 데몬을 이용해 컨테이너를 만들 수 있다.

docker run --name jenkins-docker \
 --restart=always \
 --detach \
 --network jenkins \
 --env DOCKER_HOST=tcp://docker:2376 \
 --env DOCKER_CERT_PATH=/certs/client \
 --env DOCKER_TLS_VERIFY=1 \
 --publish 8080:8080 \
 --publish 50000:50000 \
 --volume jenkins-data:/var/jenkins_home \
 --volume jenkins-docker-certs:/certs/client:ro \
 jenkins-docker:lts-jdk11

jenkins-docker 안에서도 docker 명령을 사용가능하지만, 분리된 공간으로 취급되어 밖과는 다른 내용이 출력된다.

docker container exec -it jenkins-docker bash
$ docker container ls

http://DOCKER_HOST_IP:8080으로 Jenkins 대시보드에 접속한다.

docker exec jenkins-docker cat /var/jenkins_home/secrets/initialAdminPassword

명령어를 실행해 Unlock을 진행하고, admin을 만들어 접속한다. 플러그인에서 docker 플러그인이 설치되어 있는 것을 확인할 수 있다. 노드 관리의 Configure Cloud에서 Docker를 선택하고 tcp://docker:2376으로 통신이 되나 확인해 보면 400 error가 뜬 채로 연결되지 않는다. 이것은 인증서가 등록되어 있지 않기 때문으로, certs 디렉터리에 만들어져 있는 인증서를 Server Credentials에 등록 해 주면 오류가 나오지 않고 연결되는 것을 확인할 수 있다.

docker container exec jenkins-docker ls /certs/client/key.pem #Client Key
docker container exec jenkins-docker ls /certs/client/cert.pem #Client Certificate
docker container exec jenkins-docker ls /certs/client/ca.pem #Server CA Certificate

ID는 docker-client-certs로 지정한다.

Jenkins-Docker 파이프라인 생성

참고

Pipeline 유형의 프로젝트에서 Pipeline Script from SCM을 선택한 후 참고 github의 레포지토리를 등록한다. branch를 main으로 변경하고 저장한다.

docker container exec jenkins-docker docker ps 

작업을 진행하는 동안엔 컨테이너가 보이지만, 종료되면 컨테이너가 보이지 않는다(제거된다). Jenkins에는 npm 명령이 없지만, docker에서 생성한 컨테이너 내부에서 명령을 실행한다. 즉 stage일 때만 docker 컨테이너를 띄우고 작업을 종료하면 컨테이너를 제거한다.

profile
다 아는 건 아니어도 바라는 대로

0개의 댓글