[Infra] Kubernetes Jenkins Pipeline(Feat. Kaniko, Jnlp)
쿠버네티스에서 젠킨스 파드를 배포했고, 커스텀 pvc/pv 생성하여 볼륨 마운트를 해줬다.
jenkins-values.yaml
apiVersion: v1
kind: Namespace
metadata:
name: jenkins
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins
namespace: jenkins
spec:
replicas: 1
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
imagePullSecrets:
- name: docker-hub
nodeSelector:
app: jenkins # worker-node-02에 지정한 레이블과 일치
containers:
- name: jenkins
image: jenkins/jenkins:jdk21
securityContext:
runAsUser: 0
ports:
- containerPort: 8080
name: web
- containerPort: 50000
name: agent
volumeMounts:
- name: jenkins-home
mountPath: /var/jenkins_home
- name: docker-sock
mountPath: /var/run/docker.sock
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
volumes:
- name: jenkins-home
persistentVolumeClaim:
claimName: jenkins-pvc
- name: docker-sock
hostPath:
path: /var/run/docker.sock
---
apiVersion: v1
kind: Service
metadata:
name: jenkins
namespace: jenkins
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
name: web
selector:
app: jenkins
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jenkins-ingress
namespace: jenkins
annotations:
nginx.ingress.kubernetes.io/backend-protocol: HTTP
cert-manager.io/cluster-issuer: "letsencrypt"
spec:
ingressClassName: nginx
tls:
- hosts:
- <도메인>
secretName: jenkins-tls-secret
rules:
- host: <도메인>
http:
paths:
- path: / # /jenkins와 그 하위 경로 모두 매칭
pathType: Prefix
backend:
service:
name: jenkins
port:
number: 8080
jenkins-custom-pvc-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkins-pv
spec:
capacity:
storage: 8Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /data/jenkins # EC2 인스턴스 내 경로
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-pvc
namespace: jenkins
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
volumeName: jenkins-pv
jenkins 배포 후 파이프라인에서 Docker image를 빌드하고 푸시하는 과정에서 다음과 같은 에러가 발생했다.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
해당 에러는 쿠버네티스에서 Jenkins 파드가 Docker 데몬에 접근할 수 없서 생기는 문제다.
Jenkins가 쿠버네티스에서 실행 중인데, 파이프라인 내에서 docker build
명령을 실행할 때, Docker 데몬에 접근할 수 없어서 발생한 에러.
쿠버네티스는 컨테이너화된 환경이고, Jenkins도 그 안에서 Pod로 돌아가고 있음. 그런데 docker build
는 호스트의 Docker 데몬에 접근해야 작동함.
하지만 Jenkins Pod 안에는 기본적으로 /var/run/docker.sock
이 없고, Docker 데몬도 없음. 즉, docker CLI는 있는데 도커 엔진이 없어서 build/push도 못하는 상태.
Jenkins 파드 안에서 docker 설치 여부를 확인하고 설치하려고 했으나 또 에러가 발생.
which docker || sudo apt update && sudo apt install -y docker.io
/usr/bin/docker
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:
The following packages have unmet dependencies:
containerd.io : Conflicts: containerd
E: Error, pkgProblemResolver::Resolve generated breaks, this may be caused by held packages.
에러 메세지를 보면 The following packages have unmet dependencies: containerd.io : Conflicts: containerd
이렇게 나와있다.
마스터 노드 구축할 때, 컨테이너 런타임을 containerd
로 설치했고, 해당 containerd
패키지와 containerd.io(Docker 의존성)
이 충돌하는 것이었다.
즉, Docker를 설치하려고 하니까 이미 설치된 containerd가 충돌을 일으켜 패키지 충족 조간이 깨진 것임.
이를 해결하기 위한 방법은 여러가지가 있다. 충돌하는 containerd
를 제거하고 Docker를 설치하거나 Docker가 아닌 다른 방법을 통해 이미지 build/push를 하는 것.
충돌하는 containerd
를 삭제하게되면 클러스터 내에서 문제가 발생하기 때문에 절대해서는 안된다. 따라서 Docker가 아닌 다른 방법으로 image build/push를 하는 방법을 찾았고, 두 가지 방법이 있었다.
1. Docker-in-Docker (DinD) 방식
Jenkins Pod 안에 Docker 데몬을 같이 실행시켜서 내부에서 docker build
가 가능하게 만드는 방식
단, privileged: true
가 필요하고, 보안상 위험할 수 있기 때문에 네트워크 분리된 CI 클러스터에서만 사용한다.
2. Kaniko, Buildah 같은 Build 도구 사용
Docker 데몬 없이 이미지 빌드가 가능한 도구
진행 중인 프로젝트는 단일 클러스터 내에 Jenkins를 배포한 것이기 때문에 1번은 불가하다고 판단했고, 2번에서 Kaniko를 활용하기로 결정했다.
kaniko란?
Kaniko 공식문서에서는 Kaniko를 다음과 같이 설명한다.
- Kaniko는 Kubernetes 또는 컨테이너 환경에서 Dockerfile을 기반으로 컨테이너 이미지를 안전하게 빌드할 수 있는 도구.
- 기존의
docker build
명령은 Docker 데몬이 필요하지만, Kaniko는 docker 데몬 없이도 Dockerfile을 실행하여 컨테이너 이미지를 만들 수 있음. - Kaniko는 컨테이너 내에서 작동하면서도 루트 권한 없이 실행 가능하다는 점에서 보안적으로도 안전한 빌드 환경을 제공한다.
- Kaniko는 컨테이너 내부에서 실제로 컨테이너를 실행하지 않음.
RUN
명령은 컨테이너 런타임 없이 직접 rootfs 위에서 실행 - 이미지 안에서 daemon이 돌거나, network access가 필요한 복잡한 빌드에는 제한적일 수 있음.
Kaniko가 필요한 이유
- 기존의 Docker build는 다음과 같은 한계를 갖는다.
- Docker daemon이 필요함 -> 루트 권한 필요
- Kubernetes 내부에서 빌드하려면 특수 권한이 필요하거나 Docker-in-Docker(DinD)를 사용해야 한다.
- 보안 위험 : 도커 소켓을 마운트해야 하는 경우 컨테이너가 호스트를 제어할 수 있다.
- Kaniko는 이러한 제약을 해결한다.
- Docker Daemon 없이도 작동
- Kubernetes 클러스터 내부에서 안전하게 이미지 빌드 가능
- 보안 격리를 유지하면서도 Dockerfile을 그대로 지원한다.
Kaniko 구성 요소
Kaniko는 크게 두 가지 바이너리로 구성된다.
- Exceutor
- Dockerfile을 파싱하고 각 명령어를 실행함
- 최종적으로 컨테이너 이미지를 생성함
- Warmer
- 캐시를 미리 로드하여 빌드 속도를 개선하기 위한 도구
Kaniko의 동작 방식
- 사용자로부터 다음을 입력 받음
- Dockerfile
- 컨텍스트(context,
.
디렉토리 등) - 베이스 이미지
- Dockerfile을 파싱하고 명령어를 순차적으로 실행함
COPY
,ADD
RUN
:/bin/sh -c
환경에서 명령 실행- 컨테이너를 실제로 실행하지 않고, chroot 및 사용자 공간 파일 시스템에서 명령을 처리함.
- 모든 명령이 적용된 후 OCI 호환 이미지를 생성하여, 지정된 레지스트리에 푸시함
- GCR, Docker Hub, ECR 등
이미지 캐싱 지원
- Kaniko는 레이어 캐싱 기능도 제공, 이전에 빌드한 레이어가 변경되지 않았을 경우 재사용 가능
--cache=true
옵션으로 캐시 활성화--cache-repo=gcr.io/my-porject/kaniko-cache
등으로 캐시 저장소 지정 가능
Jenkins Pipeline
Jenkins Pod는쿠버네티스 위에 올렸기 때문에 agent { kubernetes { ... } }
를 사용해야 한다.
또한 Jenkins가 해당 Pod에 명령을 보내기 위해서는 jnlp
컨테이너가 필요하다!!
jnlp란?
- Jenkins의 빌드 명령을 수신받아 실행하는 에이전트 컨테이너
- Master(Controller)와 통신해서 build 작업을 실행
JENKINS_URL
,JENKINS_SECRET
환경변수로 Controller와 연결- Jenkins pipeline의 명령을 실행
- 기본 경로는
/home/jenkins/agent
(즉, WORKSPACE 디렉토리) - Jenkins pipeline의 build logic을 실행
- Kaniko는
jnlp
가 준비한 코드(Git clone 결과물)를 기반으로 이미지 빌드 및 푸시 - 둘은 같은 Pod 안에서
emptyDir
볼륨으로 작업 디렉토리를 공유한다.
+-------------------------------+
| Jenkins Pod |
| |
| +------------------------+ |
| | jnlp (inbound agent) | | <-- Jenkins가 명령 내림
| | - git clone | |
| | - gradlew build | |
| +------------------------+ |
| || 공유 볼륨 |
| VV |
| +------------------------+ |
| | kaniko container | | <-- Dockerfile 빌드 & push
| | - /kaniko/executor | |
| +------------------------+ |
+-------------------------------+
이제 파이프라인을 작성하기 위한 사전 작업을 해보자!!
config.json
Docker Hub 로그인 정보가 담긴 config.json 파일 생성
{
"auths": {
"https://index.docker.io/v1/": {
"username": "YOUR_DOCKERHUB_USERNAME",
"password": "YOUR__DOCKERHUB_ACCESS_TOKEN"
}
}
}
docker-config-secret
만들기
kubectl create secret generic docker-config-secret \
--from-file=config.json=<PATH_TO_CONFIG_JSON> \
-n jenkins
JAR
을 직접 빌드하는 방식이 아닌 미리 빌드된 JAR
을 복사하는 Dockerfile
FROM eclipse-temurin:21-jre
ARG JAR_FILE=build/libs/<서비스명>.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
Pod Template 등록
Jenkins - Jenkins 관리 - Clouds - New Cloud - Type(Kubernetes)
- Kubernetes URL : https://<클러스터 public IP>:6443
- Kubernetes Namespace : Jenkins의 namespace
- Credentials : Jenkins Credential에서 등록한
kubelet.conf
, 해당 파일 안에certificate-authority-data
이 값이 들어가 있어야함. - Jenkins URL : Jenkins 접근 URL
- Jenkins tunnel : <Jenkins service 접속 host>:50000(ex. jenkins의 namespace.jenkins의 service명.svc.cluster.local:50000
생성한 cloud - Pod Template
- Namespace : Jenkins Pod에 설정한 namespace
- Labels : pipeline에서 설정할 kubernetes label
- Raw YAML for the Pod
apiVersion: v1 kind: Pod spec: containers: - name: kaniko image: gcr.io/kaniko-project/executor:debug entrypoint: [""] command: - cat tty: true volumeMounts: - name: kaniko-secret mountPath: /kaniko/.docker - name: workspace-volume mountPath: /home/jenkins/agent/workspace - name: jnlp image: cheonwoo/bouldermort:jnlp-v1 volumeMounts: - name: workspace-volume mountPath: /home/jenkins/agent/workspace volumes: - name: kaniko-secret secret: secretName: docker-config-secret items: - key: .dockerconfigjson path: config.json - name: workspace-volume emptyDir: {}
- 진행중인 프로젝트에서 JAVA 21버전을 사용하고 있었기 때문에 JAVA 17버전 까지만 지원하는 jenkins/inbound-agent:latest 이미지는 사용할 수 없엇다. 따라서 Dockerfile을 통해 jenkins/inbound-agent:latest에 직접 java 21버전을 설치한 후 도커 이미지 커스텀하여 사용했다.
- docker-config-secret에는 DockerHub 로그인 정보를
kubectl create secret generic docker-config-secret ...
를 통해 설정해주면 됨. - ImagePullSecrets : DockerHub에 접근하기 위한 Docker Huib Secret Credential
Multibranch Pipeline 생성
Jenkins - 새로운 Item - Multibranch Pipeline
- GitLab Project
- Server : Gitlab URL이 gitlab.com이 아닌 다른 Gitlab URL인 경우 Jenkins - Jenkins 관리 - System - GitLab - GitLab Server에서 Server URL을 설정해줘야 함.
- Checkout Credeintials : gitlab계정 personal accss token
- Owner : Group or Namespace
- Projects : Owner 설정하면 자동으로 나옴, Project명
- Discover branches
해당 설정을 해줘야 브랜치 필터가 가능함- Strategy : All Branches
- Branches to always include : 브랜치 검색 시 반드시 포함되어야 할 브랜치명
- Filter by name(with regular expression) : 필터할 브랜치 명, 정규식 사용해야 적용됨. 예를 들어, crew 서비스의 브랜치를 필터하고 싶다면
^be/crew/deploy$
- Build Configuration
- Mode : by Jenkinsfile
- Script Path : Jenkinsfile이 위치한 디렉토리, 루트 디렉토리부터 작성해야함.
- Scan GitLab Project Tirgger - Trigger token : GitLab에서 Webhook 설정시 필요한 토큰명
위와 같이 Multibranch Pipeline을 생성하면 GitLab에 자동으로 Webhook이 생성됨. 해당 Webhook에서 trigger 설정하면 됨.
pipeline 일부
pipeline {
agent {
kubernetes {
label 'kaniko-pod'
}
}
...
stages {
...
stage('Build and Push Docker Image') {
steps {
container('kaniko') {
script {
def version = env.NEXT_SERVICE_VERSION
sh """
/kaniko/executor \
--dockerfile=${env.WORKSPACE}/backend/${env.SERVICE_NAME}-service/Dockerfile \
--context=${env.WORKSPACE}/backend/${env.SERVICE_NAME}-service \
--destination=docker.io/${env.DOCKER_HUB_NICKNAME}/${env.DOCKER_HUB_REPO}:${version}
"""
}
}
}
}
...
}
}
Jenkins 설정하면서 여러 에러들을 봤다. 특히 버전 문제로 상당히 많은 에러를 만났고, 브랜치별 CI/CD를 진행하려다 보니 설정해야 하는 값들이 많았다. Kubernetes Jenkins는 구글링 해도 자료가 많이 안나오고 Jenkins 버전마다 UI가 조금씩 다르다보니 많이 헤맸다. 사실상 공식문서 보면서 다 했는데 혹시나 누군가 Kubernetes로 Jenkins를 띄웠을 때 도움이 되면 좋겠다.
막연히 Jenkins에서 이미지 빌드 및 푸시는 Docker로만 진행하는 줄 알았는데 이번 프로젝트를 진행하며 Kaniko와 jnlp에 대해 공부할 수 있었다.
참고
https://velog.io/@milkskfk5677/CICD-특정-브랜치에-대한-GitHub-Webhook-보내기