최종 프로젝트 인프라 구축④-배포환경 구성
젠킨스와 로컬 쿠버 연동
개요
minikube 클라스터 설치가 완료되었으니, 이제 젠킨스로 이곳에 배포하는 환경을 만들어 보겠다.
젠킨스 컨테이너 설치
우선은 혹시 모를 SELinux 오류 방지를 위해
1
2
3
4
5
getenforce
# Enforcing
setenforce 0
getenforce
# Permissive
잠시 꺼두겠다.
podman파일 만들기
젠킨스 서버도 Rocky Linux 8이니까 podman으로 젠킨스 컨테이너를 설치해 주겠다.
그냥 공개되어 있는 젠킨스 이미지를 받아도 되겠지만… 아래 젠킨스파일 단계를 위해 이미지를 커스텀해줘야 한다.
1
2
3
4
5
6
7
8
9
10
11
FROM jenkins/jenkins:lts-jdk17
USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends \
openssh-client \
rsync && \
rm -rf /var/lib/apt/lists/*
USER jenkins
컨테이너 파일을 만들어 봤다.
요지는 공개되어 있는 jenkins:lts-jdk17를 가져다, 거기에 openssh-client와 rsync 패키를 추가해준다는 것이다.
아무튼 이 파일을 Containerfile
로 저장한 다음
1
podman build -t my-jenkins .
my-jenkins 이라는 이름으로 빌드해 주자.
이미지 작동
우선 컨테이너가 재시작돼도 초기화되지 않게 볼륨을 만들어 주고
1
sudo podman volume create jenkins_data
podman run은 위 사진처럼 하지 말고
1
systemctl enable podman.socket
을 먼저 해준 다음에(호스트의 podman을 컨테이너와 연결하는 소켓 활성화)
1
2
3
4
5
6
7
8
sudo podman run -d \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_data:/var/jenkins_home \
-v /run/podman/podman.sock:/var/run/docker.sock \
--name jenkins-server \
--restart=always \
my-jenkins
이렇게 해주자. 위 사진과의 차이는
-v /run/podman/podman.sock:/var/run/docker.sock \
이랑
my-jenkins
이 추가된 것인데,
-v /run/…은 쉽게 말해 컨테이너에서 docker 명령어 수행할 때 podman을 docker로 속이겠다라는 말이다.
(소켓을 통해, 지난 시간에 말하지 못했는데 docker와 podman은 호환된다)
젠킨스 구성
젠킨스 초기 설정은 예전 포스트처럼 하면 되는데,
그때랑 다른 점이 몇 개 있다. 우선 플러그인 설치 부분인데
사진에 나와있는 플러그인 말고도
- ssh agent
이것을 추가로 설치해줘야 한다. 이유는 뒤의 젠킨스 파일에서…
그리고, 젠킨스 호스트PC(즉 192.168.56.101)에서
1
sudo ln -sf $(which podman) /usr/local/bin/docker
이렇게 podman 위치(which podman, 위치여서 which네!)를 /usr/local/bin/docker로 연결시키는 심볼릭 링크(바로가기) 하나를 만들어 준다.
이거는 아까 말했듯이, 소켓 연결로 인해 컨테이너에서 docker 명령어를 수행할 때 호스트의 podman 명령어를 대신 사용하는데,
여기 Tools 설정의 Docker 항목에서 실행 경로를 지정해줘야 하기 때문이다. 따라서 docker의 installation root에 /usr/local/bin/docker로 설정해 주면 된다.
젠킨스와 minikube 연동
이제, 젠킨스와 minikube를 연결해줘야 하는데, 젠킨스 호스트(192.168.56.101)에서
1
2
3
4
podman exec -it -u root jenkins-server /bin/bash
su - jenkins
ssh-keygen -t rsa
ssh-copy-id (클러스터서버 아이디)@192.168.56.100
jenkins 컨테이너에 접속해 jenkins 아이디로 minikube 서버에 ssh 키를 만들어 복사해 준다.
이러면 젠킨스 컨테이너는 이제 minikube 서버에 아이디 없이 접속할 수 있다!
그 다음에는 반대로 minikube를 젠킨스랑 연동시키면 되는데,
젠킨스 설정→Credentials→global→Add Credential 한 다음
위 사진처럼 Kind에 SSH Username with private key,
Username에 minikube 사용자 아이디,
Private Key에 Enter directly를 체크하고
아까 컨테이너에서 생성한 rsa_key 내용(cat /var/jenkins_home/.ssh/id_rsa
)을 그대로 넣어주면 된다.
(주의할 점이 양 끝에 공백이나 개행이 있으면 안돼요!)
이 크리덴션의 이름을 잘 기억해두자…젠킨스파일 만들 때 필요하다!
젠킨스파일 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.my_project.backend.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test")
public String Test() {
return "Hello Asset!";
}
}
이렇게 간단한 api를 구현한 스프링부트 서비스를 만들었다.
application.yaml
은 아래와 같다.
1
2
3
4
5
spring:
application:
name: backend_test
server:
port: 8080
```java
tasks.register('getAppName') {
doLast {
println "${rootProject.name}" }
}
tasks.register('getAppVersion') {
doLast {
println "${project.version}" }
}
이 두 줄을 추가해준 다음, 서비스 루트에 k8s
폴더를 만들어
*deployment.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-test-deployment
spec:
replicas: 1
selector:
matchLabels:
app: backend-test
template:
metadata:
labels:
app: backend-test
spec:
containers:
- name: backend-test
image: localhost/backend_test:0.0.1 # build.gradle에 있는 getAppName:getAppVersion 과 동일하게
# Minikube 내부에 이미지를 직접 빌드하므로 외부 레포에서 이미지를 가져오지 않도록
imagePullPolicy: Never
ports:
- containerPort: 8080
(여기서 spec-containers-image 이름을
localhost/(application.yaml의 spring:application:name 명):(build.gradle의 version 명)
이렇게 지정해줘야 한다. 그 이유는 나중에)
service.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: backend-test-service
spec:
selector:
app: backend-test
ports:
- protocol: TCP
port: 8080
targetPort: 8080
nodePort: 30008
type: NodePort
(spec: selector: app 명을 위 deployment의 app 명과 동일하게,
port 및 targetPort는 application.yaml의 port와 동일하게,
nodePort는 30000-32767 범위 내에서 아무거나, 없어도 자동으로 설정)
이 두 파일을 작성해준 다음, Containerfile
을 프로젝트 루트에 생성해 준다.
1
2
3
4
5
FROM amazoncorretto:17
VOLUME /app
EXPOSE 8080
COPY build/libs/*.jar /app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
컨테이너파일 설명
FROM amazoncorretto:17
amazoncorretto:17(jdk17의 변종) 이미지를 기반으로VOLUME /app
/app 디렉토리를 컨테이너 볼륨으로 설정하고EXPOSE 8080
8080 포트를 사용해COPY build/libs/*.jar /app.jar
빌드된 jar 파일을 컨테이너의 /app.jar로 복사하고ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
컨테이너를 실행할 때 이 명령어들을 실행한다
다시 젠킨스파일 만들기
드디어 Jenkinsfile
을 마찬가지로 프로젝트 루트에 생성해 준다.
#!/usr/bin/env groovy
def APP_NAME
def APP_VERSION
def DOCKER_IMAGE_NAME
pipeline {
agent any
environment {
KUBE_IP = '192.168.56.100'
KUBE_USER = 'admin'
KUBE_SSH_KEY_ID = 'local-cluster-key' // 젠킨스 global credential key
}
tools {
gradle 'Gradle 8.14.2' // 젠킨스 Tools의 Gradle 이름
jdk 'OpenJDK 17' // 젠킨스 Tools의 JDK 이름
dockerTool 'Docker' // 젠킨스 Tools의 Docker 이름
}
stages {
stage('Set Version') {
steps {
script {
APP_NAME = sh (
script: "gradle -q getAppName",
returnStdout: true
).trim()
APP_VERSION = sh (
script: "gradle -q getAppVersion",
returnStdout: true
).trim()
DOCKER_IMAGE_NAME = "${APP_NAME}:${APP_VERSION}"
sh "echo IMAGE_NAME is ${APP_NAME}"
sh "echo IMAGE_VERSION is ${APP_VERSION}"
sh "echo DOCKER_IMAGE_NAME is ${DOCKER_IMAGE_NAME}"
}
}
}
stage('Checkout Dev Branch') {
steps {
// Git에서 dev 브랜치의 코드를 가져옵니다.
checkout scm
}
}
stage('Build Spring Boot App') {
steps {
// gradlew 권한부여
sh 'chmod +x gradlew'
// 디버그
sh 'ls -al src/main/resources/'
sh 'cat src/main/resources/application.yaml'
// Gradlew로 빌드
sh './gradlew clean build'
}
}
stage('Copy Artifacts to Minikube') {
steps {
sshagent(credentials: [KUBE_SSH_KEY_ID]) {
sh """
ssh -o StrictHostKeyChecking=no ${KUBE_USER}@${KUBE_IP} 'mkdir -p ~/app'
rsync -avz --delete --exclude '.git/' ./ ${KUBE_USER}@${KUBE_IP}:~/app/
"""
}
}
}
stage('Remote Podman Build & K8s Deploy') {
steps {
sshagent(credentials: [KUBE_SSH_KEY_ID]) {
withKubeConfig([credentialsId: KUBE_CONFIG_ID]) {
script {
def remoteBuildScript = """
cd ~/app
podman build -t ${DOCKER_IMAGE_NAME} .
kubectl apply -f k8s/
kubectl rollout restart deployment/backend-test-deployment
"""
sh """
ssh -o StrictHostKeyChecking=no ${KUBE_USER}@${KUBE_IP} '${remoteBuildScript}'
"""
}
}
}
}
}
}
}
젠킨스파일 설명
본 배포 프로세스는 이미지 레지스트리를 거치지 않고 바로 이미지를 만들어 쿠버에 집어넣는 방식이다.
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
32
33
34
35
36
37
38
39
40
def APP_NAME
def APP_VERSION
def DOCKER_IMAGE_NAME
pipeline {
agent any
environment {
KUBE_IP = '192.168.56.100'
KUBE_USER = 'admin'
KUBE_SSH_KEY_ID = 'local-cluster-key' // 젠킨스 global credential key 이름
}
tools {
gradle 'Gradle 8.14.2' // 젠킨스 Tools의 Gradle 이름
jdk 'OpenJDK 17' // 젠킨스 Tools의 JDK 이름
dockerTool 'Docker' // 젠킨스 Tools의 Docker 이름
}
stages {
stage('Set Version') {
steps {
script {
APP_NAME = sh (
script: "gradle -q getAppName",
returnStdout: true
).trim()
APP_VERSION = sh (
script: "gradle -q getAppVersion",
returnStdout: true
).trim()
DOCKER_IMAGE_NAME = "${APP_NAME}:${APP_VERSION}"
sh "echo IMAGE_NAME is ${APP_NAME}"
sh "echo IMAGE_VERSION is ${APP_VERSION}"
sh "echo DOCKER_IMAGE_NAME is ${DOCKER_IMAGE_NAME}"
}
}
}
이 부분은 빌드 사전설정을 진행한다. 쿠버서버 접속을 위한 KUBE_* 변수들도 등록하는 것을 볼 수 있다.
KUBE_SSH_KEY_ID는 아까 젠킨스 크리덴션으로 등록한 키 이름으로~
APP_NAME과 APP_VERSION을 gradle -q 로 가져오는 것을 볼 수 있는데,
이것 때문에 아까 build.gradle에 tasks.register 두 개를 추가하라 한 것이다.
아무튼 이 두 개로 이미지 이름(DOCKER_IMAGE_NAME)도 만든다.
이 이미지 이름을 아까 deployment.yaml에 localhost/를 붙여 동일하게 입력해야 한다.
(앞에 localhost/를 붙이는 이유는 레포에 올리지 않기 때문)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
stage('Checkout Dev Branch') {
steps {
// Git에서 dev 브랜치의 코드를 가져옵니다.
checkout scm
}
}
stage('Build Spring Boot App') {
steps {
// gradlew 권한부여
sh 'chmod +x gradlew'
// Gradlew로 빌드
sh './gradlew clean build'
}
}
여기는 젠킨스 파이프라인과 연결된 깃허브 레포에서 소스 코드를 가져와 자바 빌드하는 프로세스이다.
1
2
3
4
5
6
7
8
9
10
stage('Copy Artifacts to Minikube') {
steps {
sshagent(credentials: [KUBE_SSH_KEY_ID]) {
sh """
ssh -o StrictHostKeyChecking=no ${KUBE_USER}@${KUBE_IP} 'mkdir -p ~/app'
rsync -avz --delete --exclude '.git/' ./ ${KUBE_USER}@${KUBE_IP}:~/app/
"""
}
}
}
이 곳이 중요한데, 젠킨스 서버에서 쿠버 서버에 ssh 접속해서 ~/app 디렉토리를 만든 다음,
다시 돌아와 rsync를 통해 빌드된 자바파일을 쿠버 서버의 ~/app/으로 복사하는 과정이다.
컨테이너 레포지토리를 만들지 않을 것이기에 이미지 빌드도 쿠버서버에서 진행할 것이기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
stage('Remote Podman Build & K8s Deploy') {
steps {
sshagent(credentials: [KUBE_SSH_KEY_ID]) {
withKubeConfig([credentialsId: KUBE_CONFIG_ID]) {
script {
def remoteBuildScript = """
cd ~/app
podman build -t ${DOCKER_IMAGE_NAME} .
kubectl apply -f k8s/
kubectl rollout restart deployment/backend-test-deployment
"""
sh """
ssh -o StrictHostKeyChecking=no ${KUBE_USER}@${KUBE_IP} '${remoteBuildScript}'
"""
}
}
}
}
}
마지막으로 쿠버서버에 원격 접속해서 이미지를 빌드하고,
쿠버 등록(프로젝트 k8s 폴더에 있는 deployment, service)까지 하는 곳이다.
sshagent(credentials: [KUBE_SSH_KEY_ID])
ssh 키 기반으로 원격 접속하기에 젠킨스가 비밀번호 없이도 원격 접속 할 수 있다!
만약 젠킨스에서 ssh agent 플러그인을 설치하지 않았으면, sshagent 명령어 실행이 안 된다.
위에서 ssh agent를 추가 설치하라는 것이 이 때문이다.
파이프라인 생성, 깃허브 웹훅 구축
이렇게 만든 프로젝트를 깃허브 레포지토리에 푸쉬 하고..
젠킨스 파이프라인을 위 사진처럼 생성해 준다. 본인은 dev 브랜취를 대상으로 할 것이다.
그 다음, 깃허브 레포지토리에서 웹훅을 등록할 것이다.
젠킨스는 웹훅을 지원하기 때문에 Payload URL에 (젠킨스 주소)/github-webhook/ 이렇게 설정하면
dev 브랜취에 푸쉬가 생길 때 마다 바로 젠킨스에 빌드를 수행하는 진기명기를 볼 수 있다.
문제는 로컬 IP로는 웹훅 등록이 불가능하기 때문에 ngrok
등을 사용해 젠킨스 ip&포트만 퍼블릭 도메인으로 바꿔줘야 하는 수고가 있지만 말이다…
참고로 ngrok은 로컬에 특정 포트를 넣어 작동시키면 해당 PC의 그 포트만 도메인으로 공개시키는, 편리한 프로그램이다.
사용법은 여기에 친절하게 나와있습니다~
아무튼 이렇게 해서 파이프라인을 만들고 dev 브랜취에 푸쉬 하면…
와우! 된다…………
일 리가 없지.
요점은 현재 minikube 드라이버의 런타임이 docker 기반이라 발생한 오류라고 한다.
1
2
3
minikube stop
minikube delete
minikube start --driver=podman --container-runtime=crio
이렇게 minikube 런타임을 crio로 하여 다시 실행해 주자.
그 다음, 다시 푸쉬 하면…
이렇게 디플로이먼트&포드와 서비스가 올라오는 것을 확인할 수 있다!
(실제 프로젝트로 넣었기 때문에 이름이 달라요)
요청 테스트
1
minikube service backend-test-service(서비스명)
이렇게 입력하면 쿠버노드 IP가 뜬다.
이 IP로 API 요청을 보내보면…
이렇게 나온다!
문제는 서비스로 노드포트를 열어 놓아도 호스트 PC에서 접근을 할 수 없었는데,
여기를 보니까 minikube 자체가 로컬 테스트를 목적으로 한 것이라 vm 밖에서 기본적으로 접근할 수 없다고 한다…
vm에 nginx를 깔아 리버스 프록싱을 구현하던가, 아님 정통 kubeadm으로 뜯어 고칠 방안을 생각해 봐야겠다.