Ssoon
Jenkins + ArgoCD : Jenkins CI + K8S(Kind) 본문
0️⃣ Jenkins
🌱 1. 파이프라인이란 무엇인가?
- Jenkins Pipeline은 Jenkins에서 연속적인 배포(CD: Continuous Delivery) 파이프라인을 코드로 정의할 수 있게 해 주는 플러그인들의 모음
- 즉, 소스 코드가 버전 관리(예: Git)에서 커밋된 후 “빌드 → 테스트 → 배포”까지 이어지는 과정을 자동화하고 표준화하는 흐름을 모델링
- 이때 중요한 개념이 “Pipeline as Code”로, 파이프라인 정의를 Jenkinsfile 같은 코드 파일로 저장해 버전관리하고 리뷰/감사
🌱 2. 왜 Pipeline을 써야 할까?
- 코드로 관리된다 → 소스코드와 마찬가지로 리뷰, 변경이력, 추적 가능
- 내구성 있음(Durable) → Jenkins 재시작이나 노드 문제 등이 있어도 파이프라인 실행이 중단되지 않고 이어질 수 있음
- 일시 정지 가능(Pausable) → 필요시 사람의 승인이나 입력을 대기
- 유연성(Versatile) & 확장성(Extensible) 있음 → 병렬 실행, 분기, 커스텀 단계 등이 가능해 복잡한 CD 흐름도 표현
🌱 3. 주요 개념 정리
- Pipeline: 전체 자동화된 배포 흐름을 모델링한 것. 예: ‘코드 커밋 → 빌드 → 테스트 → 스테이징 배포 → 프로덕션 배포’.
- Node: Jenkins가 실행될 수 있는 머신 또는 에이전트를 의미
- Stage (단계): 전체 파이프라인을 여러 단계로 나눈 것. 예: Build, Test, Deploy 같은 논리적 구분.
- Step (단계 안의 작업): 실제로 실행되는 작은 단위 작업.
🌱 4. 선언형(Declarative) vs 스크립트형(Scripted) Pipeline
파이프라인을 정의하는 문법에는 크게 두 가지 스타일이 있어요.
- Declarative Pipeline:
- 문법이 좀 더 구조화돼 있고 읽기/쓰기가 쉬운 형태
- 예시 코드:
pipeline { agent any stages { stage('Build') { steps { sh 'make' } } stage('Test') { steps { sh 'make check' junit 'reports/**/*.xml' } } stage('Deploy') { steps { sh 'make publish' } } } } ``` :contentReference[oaicite:12]{index=12} -
- Scripted Pipeline:
- 좀 더 자유롭고 Groovy 스크립트에 가까운 형태. 복잡한 로직이 필요
node {
stage('Build') {
sh 'make'
}
stage('Test') {
sh 'make check'
junit 'reports/**/*.xml'
}
if (currentBuild.currentResult == 'SUCCESS') {
stage('Deploy') {
sh 'make publish'
}
}
}
``` :contentReference[oaicite:14]{index=14}
1️⃣Jenkins CI Pipeline
- Jenkins Plugin 설치
- Pipeline Stage View
- 기능: Jenkins Pipeline의 각 스테이지 상태(실행, 완료, 실패 등)를 시각화해 줍니다.
- Docker Pipeline (docker-workflow)
- 기능: Jenkins Pipeline에서 Docker 컨테이너를 사용해 단계별 빌드/테스트 환경을 구성하거나 Docker 이미지를 직접 빌드
- Gogs Webhook Plugin
- 기능: Gogs와 깃 webhook을 연결하여 커밋 푸시 시 Jenkins Job을 자동 트리거
- Pipeline Stage View

- 자격증명 설정 : Jenkins 관리 → Credentials → Globals → Add Credentials
- Gogs Repo 자격증명 설정 : gogs-crd

- 도커 허브 자격증명 설정 : dockerhub-crd

- Jenkins Item 생성

- ✅ 주요 구성 요약
1. Checkout
-
- Gogs 저장소에서 main 브랜치를 체크아웃
- credentialsId: 'gogs-crd'를 통해 인증 처리
- VERSION 파일을 읽어 Docker 태그로 사용
- docker.withRegistry()를 통해 Docker Hub 인증 (dockerhub-crd)
- ${DOCKER_IMAGE}:${DOCKER_TAG}로 이미지 빌드 및 푸시
- latest 태그도 함께 푸시
- 성공/실패 메시지 출력
- Gogs 저장소에서 main 브랜치를 체크아웃
pipeline {
agent any
environment {
DOCKER_IMAGE = 'kschoi728/dev-app' // Docker 이미지 이름
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://172.18.234.111:3000/devops/dev-app.git', // Git에서 코드 체크아웃
credentialsId: 'gogs-crd' // Credentials ID
}
}
stage('Read VERSION') {
steps {
script {
// VERSION 파일 읽기
def version = readFile('VERSION').trim()
echo "Version found: ${version}"
// 환경 변수 설정
env.DOCKER_TAG = version
}
}
}
stage('Docker Build and Push') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-crd') {
// DOCKER_TAG 사용
def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
appImage.push()
appImage.push("latest")
}
}
}
}
}
post {
success {
echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
}
failure {
echo "Pipeline failed. Please check the logs."
}
}
}
- 지금 빌드 → 콘솔 Output 확인

- 도커 허브 확인

root@DESKTOP-72C919S:~/cicd-labs# DHUSER=kschoi728
root@DESKTOP-72C919S:~/cicd-labs# cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: timeserver
spec:
replicas: 2
selector:
matchLabels:
pod: timeserver-pod
template:
metadata:
labels:
pod: timeserver-pod
spec:
containers:
- name: timeserver-container
image: docker.io/$DHUSER/dev-app:0.0.1
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 30
httpGet:
path: /healthz
port: 80
scheme: HTTP
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
EOF
deployment.apps/timeserver created
- image pull error (ErrImagePull / ErrImagePullBackOff)
- 이미지 가져오는 자격 증명이 없는 경우에 발생
root@DESKTOP-72C919S:~/cicd-labs# kubectl get pod
NAME READY STATUS RESTARTS AGE
timeserver-7659c4f8c8-9mwzv 0/1 ErrImagePull 0 72s
timeserver-7659c4f8c8-hjlmp 0/1 ErrImagePull 0 72s
root@DESKTOP-72C919S:~/cicd-labs# kubectl describe pod timeserver-7659c4f8c8-hjlmp
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 88s default-scheduler Successfully assigned default/timeserver-7659c4f8c8-hjlmp to myk8s-worker
Normal Pulling 37s (x3 over 87s) kubelet Pulling image "docker.io/kschoi728/dev-app:0.0.1"
Warning Failed 35s (x3 over 84s) kubelet Failed to pull image "docker.io/kschoi728/dev-app:0.0.1": failed to pull and unpack image "docker.io/kschoi728/dev-app:0.0.1": failed to resolve reference "docker.io/kschoi728/dev-app:0.0.1": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
Warning Failed 35s (x3 over 84s) kubelet Error: ErrImagePull
Normal BackOff 10s (x4 over 83s) kubelet Back-off pulling image "docker.io/kschoi728/dev-app:0.0.1"
Warning Failed 10s (x4 over 83s) kubelet Error: ImagePullBackOff
- k8s secret : 도커 자격증명 설정
- Secret 이름: dockerhub-secret
- 타입: kubernetes.io/dockerconfigjson
- 내용 확인: username, password, auth 필드가 정상적으로 포함됨
root@DESKTOP-72C919S:~/cicd-labs# kubectl get secret -A
NAMESPACE NAME TYPE DATA AGE
kube-system bootstrap-token-abcdef bootstrap.kubernetes.io/token 6 152m
root@DESKTOP-72C919S:~/cicd-labs# DHUSER=kschoi728
root@DESKTOP-72C919S:~/cicd-labs# DHPASS=dckr_pat_UmKumId1Y7qVZeHScjOG_Igqle0
root@DESKTOP-72C919S:~/cicd-labs# kubectl create secret docker-registry dockerhub-secret \
--docker-server=https://index.docker.io/v1/ \
--docker-username=$DHUSER \
--docker-password=$DHPASS
secret/dockerhub-secret created
root@DESKTOP-72C919S:~/cicd-labs# kubectl get secret
NAME TYPE DATA AGE
dockerhub-secret kubernetes.io/dockerconfigjson 1 5s
root@DESKTOP-72C919S:~/cicd-labs# kubectl get secret dockerhub-secret -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq
{
"auths": {
"https://index.docker.io/v1/": {
"username": "kschoi728",
"password": "dckr_pat_UmKumId1Y7qVZeHScjOG_Igqle0",
"auth": "a3NjaG9pNzI4OmRja3JfcGF0X1VtS3VtSWQxWTdxVlplSFNjak9HX0lncWxlMA=="
}
}
}
- 시크릿 적용 >> 아래 도커 계정 부분만 변경해서 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: timeserver
spec:
replicas: 2
selector:
matchLabels:
pod: timeserver-pod
template:
metadata:
labels:
pod: timeserver-pod
spec:
containers:
- name: timeserver-container
image: docker.io/$DHUSER/dev-app:0.0.1
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 30
httpGet:
path: /healthz
port: 80
scheme: HTTP
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
imagePullSecrets:
- name: dockerhub-secret
EOF
root@DESKTOP-72C919S:~/cicd-labs# kubectl get deploy,pod,rs
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/timeserver 2/2 2 2 5m7s
NAME READY STATUS RESTARTS AGE
pod/timeserver-8cd66568d-8djtk 1/1 Running 0 14s
pod/timeserver-8cd66568d-942p5 1/1 Running 0 44s
NAME DESIRED CURRENT READY AGE
replicaset.apps/timeserver-7659c4f8c8 0 0 0 5m7s
replicaset.apps/timeserver-8cd66568d 2 2 2 44s
- Publishing your Service
root@DESKTOP-72C919S:~/cicd-labs# cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: timeserver
spec:
selector:
pod: timeserver-pod
ports:
- port: 80
targetPort: 80
protocol: TCP
nodePort: 30000
type: NodePort
EOF
service/timeserver created
root@DESKTOP-72C919S:~/cicd-labs# kubectl get service,ep timeserver -owide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/timeserver NodePort 10.96.145.46 <none> 80:30000/TCP 14s pod=timeserver-pod
NAME ENDPOINTS AGE
endpoints/timeserver 10.244.1.4:80,10.244.1.5:80 14s
root@DESKTOP-72C919S:~/cicd-labs# curl http://127.0.0.1:30000
The time is 10:52:08 AM, VERSION 0.0.1
Server hostname: timeserver-8cd66568d-942p5
- Updating your application

- 샘플 앱 server.py 코드 변경
- VERSION 변경 : 0.0.2
response_string = now.strftime("The time is %-I:%M:%S %p, VERSION 0.0.2\n")
de0560772841:/data/dev-app# git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main
[main fc27b9f] VERSION 0.0.1 Changed
1 file changed, 1 insertion(+), 1 deletion(-)
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 291 bytes | 291.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
To http://172.18.234.111:3000/devops/dev-app.git
2700ff5..fc27b9f main -> main
branch 'main' set up to track 'origin/main'.

- 젠킨스(지금 빌드 실행)


- 태그 버전 정보
root@DESKTOP-72C919S:~# kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:0.0.2
root@DESKTOP-72C919S:~# curl http://127.0.0.1:30000
The time is 11:11:33 AM, VERSION 0.0.2
Server hostname: timeserver-6cbf9d8c6d-66tbk
2️⃣ Gogs Webhooks 설정
- 파일 수정
de0560772841:/data/gogs/conf# vi app.ini
...
[security]
INSTALL_LOCK = true
SECRET_KEY = k0oTjOTCrTigbeq
LOCAL_NETWORK_ALLOWLIST = 172.18.234.111
- 컨테이너 재기동
root@DESKTOP-72C919S:~/cicd-labs# docker compose restart gogs
[+] Restarting 1/1
✔ Container gogs Started
- Jenkins job Trigger - [dev-app] - Setting → Webhooks → Gogs 클릭


3️⃣ Jenkins Item 생성(Pipeline)



4️⃣ Jenkinsfile 작성 후 Git push

- Jenkinsfile 및 소스 코드 작업
- VERSION 파일 : 0.0.3 수정
- server.py 파일 : 0.0.3 수정
de0560772841:/data/dev-app# touch Jenkinsfile
de0560772841:/data/dev-app# vi VERSION
de0560772841:/data/dev-app# vi server.py
de0560772841:/data/dev-app# tree
.
├── Dockerfile
├── Jenkinsfile
├── VERSION
└── server.py
- Jenkinsfile 파일 작성
- 🧪 Jenkins Pipeline 요약
- 환경 변수 설정
- DOCKER_IMAGE: Docker Hub에 푸시할 이미지 이름 (kschoi728/dev-app)
- 단계별 작업
- 환경 변수 설정
- 🧪 Jenkins Pipeline 요약
✅ Checkout
-
-
-
- Gogs 저장소에서 main 브랜치의 코드를 가져옴
- 인증 정보는 gogs-crd 사용
- VERSION 파일을 읽어 Docker 이미지 태그로 사용
- env.DOCKER_TAG에 저장
- Docker Hub에 로그인 (dockerhub-crd)
- ${DOCKER_IMAGE}:${DOCKER_TAG}로 이미지 빌드
- 태그된 이미지와 latest 이미지 모두 푸시
- 결과 처리
- 성공 시: 빌드 및 푸시 완료 메시지 출력
- 실패 시: 실패 메시지 출력
-
-
pipeline {
agent any
environment {
DOCKER_IMAGE = 'kschoi728/dev-app' // Docker 이미지 이름
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://172.18.234.111:3000/devops/dev-app.git', // Git에서 코드 체크아웃
credentialsId: 'gogs-crd' // Credentials ID
}
}
stage('Read VERSION') {
steps {
script {
// VERSION 파일 읽기
def version = readFile('VERSION').trim()
echo "Version found: ${version}"
// 환경 변수 설정
env.DOCKER_TAG = version
}
}
}
stage('Docker Build and Push') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-crd') {
// DOCKER_TAG 사용
def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
appImage.push()
appImage.push("latest")
}
}
}
}
}
post {
success {
echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
}
failure {
echo "Pipeline failed. Please check the logs."
}
}
}
- 작성된 파일 push
de0560772841:/data/dev-app# git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main
[main 0d46ca4] VERSION 0.0.3 Changed
3 files changed, 48 insertions(+), 2 deletions(-)
create mode 100644 Jenkinsfile
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 12 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 1.01 KiB | 1.01 MiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
To http://172.18.234.111:3000/devops/dev-app.git
0b2c342..0d46ca4 main -> main
branch 'main' set up to track 'origin/main'.



- k8s 에 신규 버전 적용
root@DESKTOP-72C919S:~/cicd-labs# kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:0.0.3
deployment.apps/timeserver image updated
root@DESKTOP-72C919S:~/cicd-labs# curl http://127.0.0.1:30000
The time is 11:49:19 AM, VERSION 0.0.3
Server hostname: timeserver-7bbcdf8c5f-rxr6t'CICD Study [1기]' 카테고리의 다른 글
| Jenkins + ArgoCD : Argo CD + K8S(Kind) (0) | 2025.10.19 |
|---|---|
| Jenkins + ArgoCD : Jenkins CD by K8S(Kind) (0) | 2025.10.19 |
| Jenkins + ArgoCD : 실습 환경 구성 (0) | 2025.10.19 |
| Cloud Native CI/CD : 6.5 Containerize an Application Using a Tekton Taskand Buildah (0) | 2025.10.19 |
| Cloud Native CI/CD : 6.4 Create a Task to Compile and Package an App fromPrivate Git (0) | 2025.10.19 |
Comments