Ssoon

Jenkins + ArgoCD : Jenkins CI + K8S(Kind) 본문

CICD Study [1기]

Jenkins + ArgoCD : Jenkins CI + K8S(Kind)

구구달스 2025. 10. 19. 15:04

0️⃣ Jenkins

🌱 1. 파이프라인이란 무엇인가?

  • Jenkins Pipeline은 Jenkins에서 연속적인 배포(CD: Continuous Delivery) 파이프라인을 코드로 정의할 수 있게 해 주는 플러그인들의 모음
  • 즉, 소스 코드가 버전 관리(예: Git)에서 커밋된 후 “빌드 → 테스트 → 배포”까지 이어지는 과정을 자동화하고 표준화하는 흐름을 모델링
  • 이때 중요한 개념이 “Pipeline as Code”로, 파이프라인 정의를 Jenkinsfile 같은 코드 파일로 저장해 버전관리하고 리뷰/감사

🌱 2. 왜 Pipeline을 써야 할까?

  • 코드로 관리된다 → 소스코드와 마찬가지로 리뷰, 변경이력, 추적 가능
  • 내구성 있음(Durable) → Jenkins 재시작이나 노드 문제 등이 있어도 파이프라인 실행이 중단되지 않고 이어질 수 있음
  • 일시 정지 가능(Pausable) → 필요시 사람의 승인이나 입력을 대기
  • 유연성(Ver­satile) & 확장성(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을 자동 트리거

 

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

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

  • Jenkins Item 생성

 

  • ✅ 주요 구성 요약

       1. Checkout

    • Gogs 저장소에서 main 브랜치를 체크아웃
      • credentialsId: 'gogs-crd'를 통해 인증 처리
    2. Read VERSION
    • VERSION 파일을 읽어 Docker 태그로 사용
    3. Docker Build and Push
    • docker.withRegistry()를 통해 Docker Hub 인증 (dockerhub-crd)
      • ${DOCKER_IMAGE}:${DOCKER_TAG}로 이미지 빌드 및 푸시
      • latest 태그도 함께 푸시
    4. Post
    • 성공/실패 메시지 출력
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)
      • 단계별 작업

                     ✅ Checkout

        • Gogs 저장소에서 main 브랜치의 코드를 가져옴
        • 인증 정보는 gogs-crd 사용
        ✅ Read VERSION
        • VERSION 파일을 읽어 Docker 이미지 태그로 사용
        • env.DOCKER_TAG에 저장
        ✅ Docker Build and Push
        • 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
Comments