일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- sonarqube
- java
- jsonwebtoken
- 구름
- bitmask
- softeer
- objectstorage
- DFS
- 소프티어
- CODETREE
- 완전탐색
- 동전 퍼즐
- DP
- 알고리즘
- 백엔드 개발
- On-Premise
- 인가인증
- MESSAGEBROKER
- 자바
- BFS
- s3
- 카카오엔터프라이즈
- 카카오클라우드
- 함수 종속성
- dockercompose
- es_java_home
- bfs
- 정렬
- db
- 코드트리
- Today
- Total
wooing
On-premise환경에서 Jenkins CI/CD 세팅하기 본문
프로젝트의 인프라를 학교에서 제공받은 온프레미스에서 구축하고있다. 학교 외부에서 이 작업을 하려면 FortiClient VPN으로 학교 내부망에 접속해야한다. 학교에서 제공받은 계정으로 VPN에 접속하여 작업을 하였다.

시작하기에 앞서
Jenkins를 Git과 연동하여 CI/CD를 구축하는 방법으로는 webhook으로 특정 액션이 발동되면 url에 전달한다. 하지만 우리는 On-Premise환경이기때문에 외부에서 먼저 요청을 보내올 수 없다. 이런 이유로 Webhook방식으로는 할 수 없다. 그래서 우리는 Poll SCM방식으로 Jenkins가 레포지토리에 일정 시간마다 빌드를 시도하는 방법으로 구현하려고 한다.
만약 이 방법을 사용하지 않으려면 GitLab을 사용하여 서버 내부에 SCM을 구축해놓는 방법으로도 해결 가능하다.
0. CI/CD 서버 ssh접속
부여받은 IP중 CI/CD서버 아이피에 접속한다.
ssh ubuntu@{서버 IP}

1. 젠킨스 설치
아래의 공식 홈페이지의 Jenkins Linux설치 가이드를 따라 진행함
https://www.jenkins.io/doc/book/installing/linux/
2. 젠킨스 시작 및 접속
터미널에서 아래 명령어로 젠킨스를 시작한다.
sudo systemctl start jenkins
그 후 http://{서버 IP}:8080/ 에 접속한다.
아이디: admin
비밀번호: 1234
3. Ubuntu docker 설치 및 Docker hub 연결
1. Docker 설치
- https://haengsin.tistory.com/128 여기 따라서 함
2. Jenkins에 Docker, Docker pipeline 플러그인 설치
- Jenkins 관리 → Plugins → Available plugins → “Docker” 검색 → Docker, Docker pipeline 설치
3. Credentials생성
- Jenkins 관리 → Credentials → System → Global credentials → Add Credentials
- Password는 Docker Hub에서 Account setting → Security에서 Key 생성함

4. Pipeline 설정에서 repository에 방금 생성한 Credentials 추가
4. Poll SCM 생성
1. New item에서 pipeline을 선택한다.
2. Poll SCM 선택 및 스케줄 지정
3. Repository 등록

3-1. 특정 branch설정
4. 지금 빌드로 테스트

5. 프로젝트별 Dockerfile작성
Jenkins가 SCM에서 커밋 감지 후 Docker hub에 올리기 위한 Docker image를 빌드하는 Dockerfile을 작성해야한다. Dockerfile안에는 각 프로젝트 빌드파일 생성 및 컨테이너 실행 Entrypoint지정을 해주어야 한다.
[Spring boot]
# Use the official gradle image to create a build artifact.
FROM gradle:8.6.0-jdk17 AS build
# 작업 디렉토리 설정
WORKDIR /app
# 소스 코드와 build.gradle 파일을 이미지로 복사
COPY src ./src
COPY build.gradle settings.gradle ./
# 애플리케이션 빌드
RUN gradle clean build -x test
# 빌드 단계 완료 후, 실행 단계를 위한 새로운 베이스 이미지 사용
FROM openjdk:17-jdk
# 작업 디렉토리 설정
WORKDIR /app
# 빌드 단계에서 생성된 JAR 파일을 현재 디렉토리로 복사
COPY --from=build /app/build/libs/*.jar app.jar
# 컨테이너 실행 시 JAR 파일 실행
ENTRYPOINT ["java","-jar","app.jar"]
[Fast API] - 수정 예정
# 베이스 이미지로 공식 Python 이미지 사용
FROM python:3.9
# 작업 디렉토리 설정
WORKDIR /app
# 의존성 파일 복사
COPY requirements.txt ./
# 의존성 설치
RUN pip install --no-cache-dir -r requirements.txt
# 애플리케이션 코드를 작업 디렉토리로 복사
COPY . .
# 애플리케이션 실행 명령어
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
[React] - 수정 필요
# 가져올 이미지를 정의
FROM node:18
# 경로 설정하기
WORKDIR /app
# package.json 워킹 디렉토리에 복사 (.은 설정한 워킹 디렉토리를 뜻함)
COPY ./material-kit-react/package.json .
# 명령어 실행 (의존성 설치)
RUN npm install
# 현재 디렉토리의 모든 파일을 도커 컨테이너의 워킹 디렉토리에 복사한다.
COPY . .
# 3000번 포트 노출
EXPOSE 3000
# npm start 스크립트 실행
CMD ["npm", "run", "start"]
6. Jenkins script 작성
각 레포지토리에 Jenkinsfile을 만들어서 pipeline script로 Dockerfile을 자동 빌드하고 Docker hub에 push한다. Docker를 빌드 후에 바로 Docker hub로 push하기 때문에 빌드가 끝난 도커 이미지를 로컬(VM)에서 삭제하여 스토리지 낭비를 방지한다. 도커 이미지는 도커 빌드때, 도커 허브에 push할때 각 1개씩 생성되니 프로젝트 하나당 2개의 이미지를 삭제해야한다.
빌드 및 Docker hub push가 끝나면 쿠버네티스 마스터노드에 rollout명령을 보내 최신 버전으로 배포하도록 요청한다.
Spring boot, React(npm), Fast API 그리고 Docker를 빌드해야하기 때문에 관련한 패키지들을 CI/CD 서버에 설치해야 한다.
[Article service & User service]
게시글 서비스와 유저 서비스는 한 레포지토리 안에 존재하기 때문에 각 서비스가 변경됐는지 확인하고 변경된 서비스만 빌드해야한다. 서비스가 변경됐는지 확인하는 방법은 최근 빌드의 마지막 커밋 지점부터 가장 최근 커밋까지의 변경된 파일들 중 경로에 서비스 root 주소가 있는지 확인하는 방법으로 해결했다. 그리고 테스트를 위해 직접 빌드한 경우 코드 변경에 상관없이 코드가 모두 빌드 되어야 하기 때문에 빌드 종류를 확인하는 과정도 필요하다. 자세한 코드는 'Check Git Changes' stage를 보면 알 수 있다.
빌드의 마지막 커밋 지점은 VM에 마지막 커밋 지점을 .last_build_commit'파일에 저장했다.
pipeline {
agent any
environment {
DOCKER_CREDENTIAL_ID = 'docker_credentials'
DOCKER_HUB_USERNAME = 'kovengers'
IMAGE_NAME_ARTICLE_SERVICE = 'article-service'
IMAGE_NAME_USER_SERVICE = 'user-service'
VERSION = "${env.BUILD_NUMBER}" // Jenkins 빌드 번호를 버전으로 사용합니다.
}
stages {
stage('Check Git Changes') {
steps {
script {
// 빌드 원인 확인
def causes = currentBuild.getBuildCauses()
if (causes.any { it._class == 'hudson.model.Cause$UserIdCause' }) {
echo "This build was started manually."
env.ARTICLE_SERVICE_CHANGED = 'true'
env.USER_SERVICE_CHANGED = 'true'
} else {
echo "This build was not started manually."
def lastBuildCommit = 'HEAD^'
if (fileExists('.last_build_commit')) {
lastBuildCommit = readFile('.last_build_commit').trim()
echo "Last Build Commit: ${lastBuildCommit}"
} else {
echo 'No last build commit file found. Assuming first build.'
}
def changes = sh(script: "git diff --name-only ${lastBuildCommit} HEAD", returnStdout: true).trim()
echo "Changes: ${changes}"
env.ARTICLE_SERVICE_CHANGED = changes.contains('article-service') ? 'true' : 'false'
env.USER_SERVICE_CHANGED = changes.contains('user-service') ? 'true' : 'false'
}
}
}
}
stage('Build Docker images') {
steps {
script {
if (env.ARTICLE_SERVICE_CHANGED == 'true') {
dir('article-service') {
docker.build("${DOCKER_HUB_USERNAME}/${IMAGE_NAME_ARTICLE_SERVICE}:${VERSION}")
echo "Build ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_ARTICLE_SERVICE}:${VERSION}"
}
}
if (env.USER_SERVICE_CHANGED == 'true') {
dir('user-service') {
docker.build("${DOCKER_HUB_USERNAME}/${IMAGE_NAME_USER_SERVICE}:${VERSION}")
echo "Build ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_USER_SERVICE}:${VERSION}"
}
}
}
}
}
stage('Push Docker images') {
steps {
script {
docker.withRegistry('https://registry.hub.docker.com', "${DOCKER_CREDENTIAL_ID}") {
if (env.ARTICLE_SERVICE_CHANGED == 'true') {
docker.image("${DOCKER_HUB_USERNAME}/${IMAGE_NAME_ARTICLE_SERVICE}:${VERSION}").push()
echo "Push ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_ARTICLE_SERVICE}:${VERSION}"
}
if (env.USER_SERVICE_CHANGED == 'true') {
docker.image("${DOCKER_HUB_USERNAME}/${IMAGE_NAME_USER_SERVICE}:${VERSION}").push()
echo "Push ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_USER_SERVICE}:${VERSION}"
}
}
}
}
}
stage('Docker image cleanup') {
steps {
script {
if (env.ARTICLE_SERVICE_CHANGED == 'true') {
sh 'docker rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_ARTICLE_SERVICE}:${VERSION}'
sh 'docker rmi registry.hub.docker.com/${DOCKER_HUB_USERNAME}/${IMAGE_NAME_ARTICLE_SERVICE}:${VERSION}'
echo "rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_ARTICLE_SERVICE}:${VERSION}"
}
if (env.USER_SERVICE_CHANGED == 'true') {
sh 'docker rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_USER_SERVICE}:${VERSION}'
sh 'docker rmi registry.hub.docker.com/${DOCKER_HUB_USERNAME}/${IMAGE_NAME_USER_SERVICE}:${VERSION}'
echo "rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME_USER_SERVICE}:${VERSION}"
}
}
}
}
stage('Save Last Commit Hash') {
steps {
script {
// 현재 커밋 해시를 파일에 저장
sh 'git rev-parse HEAD > .last_build_commit'
}
}
}
stage('Kubernetes deploy') {
steps{
script {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
sh 'kubectl --kubeconfig=$KUBECONFIG rollout restart deployment/frontend-deployment'
}
}
}
}
}
}
[AI Service]
pipeline {
agent any
environment {
DOCKER_CREDENTIAL_ID = 'docker_credentials'
DOCKER_HUB_USERNAME = 'kovengers' // 여기에 Docker Hub 사용자 이름을 입력하세요.
IMAGE_NAME = 'fast-api' // 여기에 사용할 이미지 이름을 입력하세요.
VERSION = "${env.BUILD_NUMBER}" // Jenkins 빌드 번호를 버전으로 사용합니다.
}
stages {
stage('Build Docker images') {
steps {
script {
docker.build("${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}") // 이미지 빌드 및 버전 태그
}
}
}
stage('Push Docker images') {
steps {
script {
docker.withRegistry('https://registry.hub.docker.com', "${DOCKER_CREDENTIAL_ID}") {
docker.image("${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}").push() // Docker Hub에 푸시
}
}
}
}
stage('Docker image cleanup') {
steps {
script {
sh 'docker rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}'
sh 'docker rmi registry.hub.docker.com/${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}'
}
}
}
stage('Kubernetes deploy') {
steps{
script {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
sh 'kubectl --kubeconfig=$KUBECONFIG rollout restart deployment/frontend-deployment'
}
}
}
}
}
}
[Front-end]
pipeline {
agent any
environment {
DOCKER_CREDENTIAL_ID = 'docker_credentials'
DOCKER_HUB_USERNAME = 'kovengers'
IMAGE_NAME = 'frontend'
VERSION = "${env.BUILD_NUMBER}" // Jenkins 빌드 번호를 버전으로 사용합니다.
}
stages {
stage('Build Docker images') {
steps {
script {
docker.build("${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}") // 이미지 빌드 및 버전 태그
}
}
}
stage('Push Docker images') {
steps {
script {
docker.withRegistry('https://registry.hub.docker.com', "${DOCKER_CREDENTIAL_ID}") {
docker.image("${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}").push() // Docker Hub에 푸시
}
}
}
}
stage('Docker image cleanup') {
steps {
script {
sh 'docker rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}'
sh 'docker rmi registry.hub.docker.com/${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${VERSION}'
}
}
}
stage('Kubernetes deploy') {
steps{
script {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
sh 'kubectl --kubeconfig=$KUBECONFIG rollout restart deployment/frontend-deployment'
}
}
}
}
}
}
7. CICD 확인

Webhook 방식으로는 공인 IP로 접속할 수 없기 때문에 온프레미스 환경에서 CI/CD를 구축할 수 없다. 아래는 Webhook방식으로 Jenkins연결을 시도한 방법이지만, 클라우드나 공인 IP로 접속 가능한 환경에서는 실행 가능할테니 남겨놓는다.
3. Git Apps 설정하기
1. Organization 설정 → Developer settings → GitHub Apps

2. GitHub App생성

2-1. Repository permission 설정
- Adminstration : Readonly
- Checks : Read and Write
- Metadata : Readonly
- Pull request : Readonly
- Commit statuses: Read and Write
- Webhooks : Read and Write
2-2. Subcrite to events
- Check run
- Check Suite
- Pull Reqeust
- Push
- Repository
3. Private key 생성

다운 받은 private key 변환
openssl pkcs8 -topk8 -inform PEM -outform PEM -in [downdload-key.pem] -out new-key.pem -nocrypt
cat new-key.pem
개인 계정의 personal access token생성


이후 발급되는 키는 저장해놓고 남에게 공유하면 안된다.
4. Install app


4. Jenkins에 Credentials 추가
Credentials설정

- global 선택
- Add credentials
- Kind : GitHub App
- ID : 원하는 ID 입력
- Description : 설명 입력
- App ID : Github organization → settings → developer setting → GitHub Apps → Configure → Edit

5. Jenkins Item 생성
- Organization Folder 생성

2. Project 값 입력
- Api endpoint : 비워두면 알아서 채워짐
- credentials : 방금 생성한 것 선택
- Owner : organization 이름

- 특정 branch만 가져오도록 하는법
- GitHub Organization → Add → Within Repository → filter by name (with wildcards)
레포지토리 스캔 완료 (Jenkinsfile 없는 상태로)

6. Jenkinsfile 생성 후 빌드 테스트
- 최상위 branch에 jenkinsfile생성

- 브랜치 생성 후 코드 변경사항 커밋
git commit -a -m "test:Test jenkins"
git push
- 다시 Scan
- Backend 레포지토리의 develop브랜치에 Jenkinsfile 확인
Organization에 repository가 추가된걸 볼 수 있다.

Repository로 들어가면 develop브랜치가 빌드 시도한걸 볼 수 있다.
References
https://www.jenkins.io/doc/book/installing/linux/
https://yeonyeon.tistory.com/58
https://velog.io/@padomay1352/jenkins-github-organization
https://blog.kubwa.co.kr/jenkins-로-도커-이미지-build-push-자동화하기-dc7126299b48
'프로젝트 > 가천대학교 카카오엔터프라이즈 SW아카데미' 카테고리의 다른 글
카카오클라우드 튜토리얼 수정 기여 회고 (0) | 2024.06.09 |
---|---|
서브모듈로 민감정보 관리하기 (0) | 2024.05.20 |