Kubernetes CI/CD Study 1기 | 3주차 #1 Jenkins + ArgoCD

2025. 10. 31. 16:48·Kubernetes
cloudNet@ 팀의 가시다 님이 진행하는 쿠버네티스 CI/CD 스터디 3주차 내용입니다.

 

 

환경 구성

1. 쿠버네티스 환경 배포(kind)

  • 클러스터 배포
# 클러스터 배포 전 확인
sudo su -
docker ps
mkdir ~/cicd-labs
cd ~/cicd-labs

# 
kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  apiServerAddress: "0.0.0.0"
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  - containerPort: 30003
    hostPort: 30003
- role: worker
EOF

# 확인
kind get nodes --name myk8s
kubens default

# 컨트롤플레인/워커 노드(컨테이너) 확인 : 도커 컨테이너 이름은 myk8s-control-plane , myk8s-worker 임을 확인
docker ps
docker images

# 디버그용 내용 출력에 ~/.kube/config 권한 인증 로드
kubectl get pod -v6

# kube config 파일 확인
cat ~/.kube/config
...
  server: https://0.0.0.0:40925  # << 포트 정보 메모

# k8s api 호출을 위한 IP 확인
ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.27.165.105  netmask 255.255.240.0  broadcast 172.27.175.255  # << IP 메모
        inet6 fe80::215:5dff:fe59:9d43  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:59:9d:43  txqueuelen 1000  (Ethernet)
        RX packets 9379  bytes 13903481 (13.9 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3274  bytes 238716 (238.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

docker ps

## 위 메모한 IP:Port 호출 확인
curl -k https://172.27.165.105:40925/version
{
  "major": "1",
  "minor": "32",
  "gitVersion": "v1.32.8",
  "gitCommit": "2e83bc4bf31e88b7de81d5341939d5ce2460f46f",
  "gitTreeState": "clean",
  "buildDate": "2025-08-13T14:21:22Z",
  "goVersion": "go1.23.11",
  "compiler": "gc",
  "platform": "linux/amd64"
  • kube-ops-view 설치
# kube-ops-view
# helm show values geek-cookbook/kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30001 --set env.TZ="Asia/Seoul" --namespace kube-system

# 설치 확인
kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율)
open "http://<각자 자신의 WSL Ubuntu Eth0 IP>:30001/#scale=1.5"
open "http://<각자 자신의 WSL Ubuntu Eth0 IP>:30001/#scale=2"

# 클러스터 삭제
kind delete cluster --name myk8s

2. docker compose

  • Jenkins, gogs 컨테이너 기동
# 작업 디렉토리 생성 후 이동
mkdir cicd-labs
cd cicd-labs

# kind 설치를 먼저 진행하여 docker network(kind) 생성 후 아래 Jenkins,gogs 생성 할 것
# docker network 확인 : kind 를 사용
docker network ls
...
7e8925d46acb   kind      bridge    loca
...

# 
cat <<EOT > docker-compose.yaml
services:

  jenkins:
    container_name: jenkins
    image: jenkins/jenkins
    restart: unless-stopped
    networks:
      - kind
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - jenkins_home:/var/jenkins_home

  gogs:
    container_name: gogs
    image: gogs/gogs
    restart: unless-stopped
    networks:
      - kind
    ports:
      - "10022:22"
      - "3000:3000"
    volumes:
      - gogs-data:/data

volumes:
  jenkins_home:
  gogs-data:

networks:
  kind:
    external: true
EOT


# 배포
docker compose up -d
docker compose ps
docker inspect kind


# 기본 정보 확인
for i in gogs jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "whoami && pwd"; echo; done

# 도커를 이용하여 각 컨테이너로 접속
docker compose exec jenkins bash
exit

docker compose exec gogs bash
exit
  • Jenkins 컨테이너 초기 설정
# Jenkins 초기 암호 확인
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

# Jenkins 웹 접속 주소 확인 : 계정 / 암호 입력 >> admin / qwe123
웹 브라우저에서 http://<WSL2 Ubuntu Eth0 IP>:8080 접속 # Windows

# (참고) 로그 확인 : 플러그인 설치 과정 확인
docker compose logs jenkins -f
  • Jenkins 컨테이너에서 호스트에 도커 데몬 사용 설정
# Jenkins 컨테이너 내부에 도커 실행 파일 설치
docker compose exec --privileged -u root jenkins bash
-----------------------------------------------------
id

curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update && apt install docker-ce-cli curl tree jq yq -y

docker info
docker ps
which docker

# Jenkins 컨테이너 내부에서 root가 아닌 jenkins 유저도 docker를 실행할 수 있도록 권한을 부여
groupadd -g 989 -f docker  # Windows WSL2(Container) >> cat /etc/group 에서 docker 그룹ID를 지정

chgrp docker /var/run/docker.sock
ls -l /var/run/docker.sock
usermod -aG docker jenkins
cat /etc/group | grep docker

exit
--------------------------------------------

# Jenkins 컨테이너 재기동으로 위 설정 내용을 Jenkins app 에도 적용 필요
docker compose restart jenkins

# jenkins user로 docker 명령 실행 확인
docker compose exec jenkins id
docker compose exec jenkins docker info
docker compose exec jenkins docker ps
docker compose exec jenkins cat /etc/group
window환경에서는 root유저가 아닌 jenkis유저도 docker를 실행할 수 있도록 권한을 줘야 한다.
# 1. 컨테이너 내부에 docker 그룹 확인
cat /etc/group | grep docker
 → 아무 결과 없음
 
# 2. 호스트 Docker 소켓에 그룹 권한 확인
ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Oct 31 00:28 /var/run/docker.sock

  • 권한 확인 시 root 또는 docker 그룹만 접근이 가능하다,
    • Jenkins 컨테이너에는 Docker 그룹이 없어, /var/run/docker.sock에 접근할 권한이 없는 상태다
# 3. 호스트의 Docker 소켓 GID 확인
stat -c '%g' /var/run/docker.sock
988

# 4. jenkins 컨테이너 내부에 동일한 GID의 그룹 생성
groupadd -g 988 -f docker
cat /etc/group | grep docker
docker:x:988:
usermod -aG docker jenkins
cat /etc/group | grep docker

해당 권한 에러 없이 모든 값이 출력되어야 한다.

3. gogs

  • Gogs 컨테이너 초기 설정
# 초기 설정 웹 접속
open "http://127.0.0.1:3000/install" # macOS
웹접속 "http://<Ubuntu Eth0 IP>:3000/install" # Windows
  • 초기 설정
    • 데이터베이스 유형 : SQLite3
    • 애플리케이션 URL : http://<각자 자신의 IP>:3000/
    • 기본 브랜치 : main
    • 관리자 계정 설정 클릭 : 이름(계정명 - 닉네임 사용 devops), 비밀번호(계정암호 qwe123), 이메일 입력
      • 나머지는 default
  • Gogs 설치하기 클릭 ⇒ 관리자 계정으로 로그인 후 접속
  • 로그인 후 → Your Settings → Applications : Generate New Token 클릭 - Token Name(devops) ⇒ Generate Token 클릭 : 메모 필요

 

  • New Repository 1 : 개발팀용
    • Repository Name : dev-app
    • Visibility : (Check) This repository is Private ← 가시성 위에것 체크
    • .gitignore : Python
    • Readme : Default → (Check) initialize this repository with selected files and template
    ⇒ Create Repository 클릭 : Repo 주소 확인
  • New Repository 2 : 데브옵스팀용
    • Repository Name : ops-deploy
    • Visibility : (Check) This repository is Private
    • .gitignore : Python
    • Readme : Default → (Check) initialize this repository with selected files and template
    ⇒ Create Repository 클릭 : Repo 주소 확인
  • Gogs 실습을 위한 저장소 설정
docker exec -it gogs bash
----------------------------------------------
# (옵션) GIT 인증 정보 초기화
git credential-cache exit

# 
TMOUT=0
pwd
ls
cd /data # 호스트 mount 볼륨 공유 경로

#
git config --list --show-origin

#
TOKEN=<각자 Gogs Token>
TOKEN=7aa8e88fac50bb72cbed54d212f1262342108261

MyIP=<각자 자신의 IP> # mac(PC IP), windows(ubuntu eth0)
MyIP=192.168.254.110

git clone <각자 Gogs dev-app repo 주소>
git clone http://devops:$TOKEN@$MyIP:3000/devops/dev-app.git
Cloning into 'dev-app'...
...

#
cd /data/dev-app

#
git --no-pager config --local --list
git config --local user.name "devops"
git config --local user.email "a@a.com"
git config --local init.defaultBranch main
git config --local credential.helper store
git --no-pager config --local --list
cat .git/config

#
git --no-pager branch
git remote -v

# server.py 파일 작성
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
import socket

class RequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        match self.path:
            case '/':
                now = datetime.now()
                hostname = socket.gethostname()
                response_string = now.strftime("The time is %-I:%M:%S %p, VERSION 0.0.1\n")
                response_string += f"Server hostname: {hostname}\n"                
                self.respond_with(200, response_string)
            case '/healthz':
                self.respond_with(200, "Healthy")
            case _:
                self.respond_with(404, "Not Found")

    def respond_with(self, status_code: int, content: str) -> None:
        self.send_response(status_code)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(bytes(content, "utf-8")) 

def startServer():
    try:
        server = ThreadingHTTPServer(('', 80), RequestHandler)
        print("Listening on " + ":".join(map(str, server.server_address)))
        server.serve_forever()
    except KeyboardInterrupt:
        server.shutdown()

if __name__== "__main__":
    startServer()
EOF

# (참고) python 실행 확인
python3 server.py
curl localhost
curl localhost/healthz


# Dockerfile 생성
cat > Dockerfile <<EOF
FROM python:3.12
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app 
CMD python3 server.py
EOF


# VERSION 파일 생성
echo "0.0.1" > VERSION

#
tree
git status
git add .
git commit -m "Add dev-app"
git push -u origin main
...

사용한 버저닝 기법

 

4. dockerhub token & Private Repository 생성

  • token은 저번에 사용한 토큰을 사용했다.
  • Private Repository 생성(dev-app)

 

 

  • 환경 삭제
docker compose down --remove-orphans && rm -rf gogs-data jenkins_home

 

Jenkins CI + K8S(Kind)

1. Jenkins CI Pipeline

    • Jenkins 설정 : Plugin 설치, 자격증명 설정.
      • Pipeline Stage View - Docs
      • Docker Pipeline : building, testing, and using Docker images from Jenkins Pipeline - Docs
      • Gogs : Webhook Plugin - Docs → Generic Webhook Trigger 플러그인으로 설정 구현해보자 - Link , Sample
        • 예시 : http(s)://<< jenkins-server >>/gogs-webhook/?job=<<jobname>>

  • 자격증명 설정 : Jenkins 관리 → Credentials → Globals → Add Credentials
    1. Gogs Repo 자격증명 설정 : gogs-crd
      • Kind : Username with password
      • Username : devops
      • Password : <Gogs 토큰>
      • ID : gogs-crd
    2. 도커 허브 자격증명 설정 : dockerhub-crd
      • Kind : Username with password
      • Username : <도커 계정명>
      • Password : <도커 계정 암호 혹은 토큰>
      • ID : dockerhub-crd

  • Jenkins Item 생성(Pipeline)
    • appImage.push 는 각각 ‘버전 tag, latest tag’ 수행한다

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://<자신의 IP>: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."
        }
    }
}

  • Deploying to Kubernetes
# 디플로이먼트 오브젝트 배포 : 리플리카(파드 2개), 컨테이너 이미지 >> 아래 도커 계정 부분만 변경해서 배포해보자
DHUSER=<도커 허브 계정명>
DHUSER=gasida

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
watch -d kubectl get deploy,rs,pod -o wide

# 배포 상태 확인 : kube-ops-view 웹 확인
kubectl get events -w --sort-by '.lastTimestamp'
kubectl get deploy,pod -o wide
kubectl describe pod
...
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  53s                default-scheduler  Successfully assigned default/timeserver-7cf7db8f6c-mtvn7 to myk8s-worker
  Normal   BackOff    19s (x2 over 50s)  kubelet            Back-off pulling image "docker.io/gasida/dev-app:latest"
  Warning  Failed     19s (x2 over 50s)  kubelet            Error: ImagePullBackOff
  Normal   Pulling    4s (x3 over 53s)   kubelet            Pulling image "docker.io/gasida/dev-app:latest"
  Warning  Failed     3s (x3 over 51s)   kubelet            Failed to pull image "docker.io/gasida/dev-app:latest": failed to pull and unpack image "docker.io/gasida/dev-app:latest": failed to resolve reference "docker.io/gasida/dev-app:latest": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
  Warning  Failed     3s (x3 over 51s)   kubelet            Error: ErrImagePull

TROUBLESHOOTING : image pull error (ErrImagePull / ErrImagePullBackOff)
  • 컨테이너 이미지 이름이나 태그를 잘못 입력한 경우 발생
  • 혹은 이미지 저장소에 이미지가 없거나, 이미지 가져오는 자격 증명이 없는 경우에 발생
    • 이번 에러는 자격증명을 미등록으로 인한 문제 → 등록 진행
# k8s secret : 도커 자격증명 설정 
kubectl get secret -A  # 생성 시 타입 지정

DHUSER=<도커 허브 계정>
DHPASS=<도커 허브 암호 혹은 토큰>
echo $DHUSER $DHPASS

DHUSER=gasida
DHPASS=dckr_pat_JLKruUO5Ee8BGWhqxgRz50_jmT0
echo $DHUSER $DHPASS

kubectl create secret docker-registry dockerhub-secret \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username=$DHUSER \
  --docker-password=$DHPASS

# 확인 : base64 인코딩 확인
kubectl get secret
kubectl get secret dockerhub-secret -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq

# 디플로이먼트 오브젝트 업데이트 : 시크릿 적용 >> 아래 도커 계정 부분만 변경해서 배포해보자
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
watch -d kubectl get deploy,rs,pod -o wide

# 확인
kubectl get events -w --sort-by '.lastTimestamp'
kubectl get deploy,pod

 

  • Deploying the Service
# 서비스 생성
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

#
kubectl get service,ep timeserver -owide
NAME                 TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE   SELECTOR
service/timeserver   NodePort   10.96.236.37   <none>        80:30000/TCP   25s   pod=timeserver-pod

NAME                   ENDPOINTS                                   AGE
endpoints/timeserver   10.244.1.2:80,10.244.2.2:80,10.244.3.2:80   25s

# Service(NodePort)로 접속 확인 "노드IP:NodePort"
curl http://127.0.0.1:30000
curl http://127.0.0.1:30000
curl http://127.0.0.1:30000/healthz

# 반복 접속 해두기 : 부하분산 확인
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; sleep 1 ; done
for i in {1..100};  do curl -s http://127.0.0.1:30000 | grep name; done | sort | uniq -c | sort -nr

# 파드 복제복 증가 : service endpoint 대상에 자동 추가
kubectl scale deployment timeserver --replicas 4
kubectl get service,ep timeserver -owide

# 반복 접속 해두기 : 부하분산 확인
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; sleep 1 ; done
for i in {1..100};  do curl -s http://127.0.0.1:30000 | grep name; done | sort | uniq -c | sort -nr

  • k8s Deploying an application with Jenkins
    • 샘플 앱 server.py 코드 변경 → 젠킨스(지금 빌드 실행) : 새 0.0.2 버전 태그로 컨테이너 이미지 빌드 → 컨테이너 저장소 Push ⇒ k8s deployment 업데이트 배포
# VERSION 변경 : 0.0.2
# server.py 변경 : 0.0.2
echo "0.0.2" > VERSION
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main

 

  • 태그의 버전 정보 사용
# 반복 접속 해두기 : 부하분산 확인
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; sleep 1 ; done
for i in {1..100};  do curl -s http://127.0.0.1:30000 | grep name; done | sort | uniq -c | sort -nr

#
kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:0.0.2 && watch -d "kubectl get deploy,ep timeserver -owide; echo; kubectl get rs,pod"

# 롤링 업데이트 확인
watch -d kubectl get deploy,rs,pod,svc,ep -owide
kubectl get deploy,rs,pod,svc,ep -owide

# kubectl get deploy $DEPLOYMENT_NAME
kubectl get deploy timeserver
kubectl get pods -l pod=timeserver-pod

#
curl http://127.0.0.1:30000
  • Gogs Webhooks 설정
    • gogs 에 /data/gogs/conf/app.ini 파일 수정 후 컨테이너 재기동
    • gogs 에 Webhooks 설정 : Jenkins job Trigger - [dev-app] - Setting → Webhooks → Gogs 클릭
      • Payload URL : http://192.168.254.110:8080/gogs-webhook/?job=**SCM-Pipeline**/ # 각자 자신의 IP
      • Content Type : application/json
      • Secret : qwe123
      • When should this webhook be triggered? : Just the push event
      • Active : Check ⇒ Add webhook → Test Delivery 시도 시, 현재는 Jenkins 미설정 상태로 404 실패
[security]
INSTALL_LOCK = true
SECRET_KEY   = j2xaUPQcbAEwpIu
LOCAL_NETWORK_ALLOWLIST = 192.168.254.110 # 각자 자신의 IP

# 재기동
docker compose restart gogs

  • Jenkins Item 생성(Pipeline) : item name(SCM-Pipeline)
      • GitHub project : http://***자신의 IP>***:3000/***<Gogs 계정명>***/dev-app ← .git 은 제거
        • *GitHub project : http://54.180.154.83:3000/devops/dev-app*
      • Use Gogs secret : qwe123
      • Build Triggers : Build when a change is pushed to Gogs 체크
      • Pipeline script from SCM
        • SCM : Git
          • Repo URL(http://***<mac IP>***:3000/***<Gogs 계정명>***/dev-app)
          • Credentials(devops/***)
          • Branch(*/main)
        • Script Path : Jenkinsfile

  • Jenkinsfile 작성 후 Git push
    • Jenkinsfile 및 소스 코드 작업
# Jenkinsfile 빈 파일 작성
tree
├── Dockerfile
├── README.md
├── VERSION
└── server.py

touch Jenkinsfile

# VERSION 파일 : 0.0.3 수정
# server.py 파일 : 0.0.3 수정

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://<자신의 IP>: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
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main

  • 신규 버전 적용
# 신규 버전 적용
kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:0.0.3 && while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; sleep 1 ; done

# 확인
watch -d "kubectl get deploy,ep timeserver; echo; kubectl get rs,pod"

 

Jenkins CD + K8S

  • Jenkins 컨테이너 내부에 툴 설치 : kubectl(v1.32), helm
# Install kubectl, helm
docker compose exec --privileged -u root jenkins bash
--------------------------------------------
# kubectl download
#curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl"  # macOS
#curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"  # WindowOS
curl -LO "https://dl.k8s.io/release/v1.32.8/bin/linux/arm64/kubectl"  # macOS
curl -LO "https://dl.k8s.io/release/v1.32.8/bin/linux/amd64/kubectl"  # WindowOS

install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client=true

#
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version

exit
--------------------------------------------
docker compose exec jenkins kubectl version --client=true
docker compose exec jenkins helm version
  • Jenkins 설정 : 자격증명 설정 - k8s-crd
    • 자격증명 설정 : Jenkins 관리 → Credentials → Globals → Add Credentials

1. myk8s-control-plane 컨테이너 IP 확인

# myk8s-control-plane 컨테이너 IP 확인
docker inspect myk8s-control-plane | grep IPAddress
                    "IPAddress": "192.168.97.3",
                    
# Jenkins 컨테이너에서 myk8s-control-plane api 호출 확인
docker exec -it jenkins curl https://192.168.97.3:6443/version -k
...

2. k8s(kind) 자격증명 설정 : k8s-crd

  • Kind : Secret file
  • File : <kubeconfig 파일 업로드>
  • ID : k8s-crd
    • 메모장으로 직접 작성 후 업로드
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJQTdOSnBqU3NlRm93RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRFd01qVXdPVEUyTlRGYUZ3MHpOVEV3TWpNd09USXhOVEZhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURTOEcyMDVUeXQzYm1jMGtKQmtWMjMvdmV3VWp2MFlCb29yeDNhVk00UUtXdzVuWDVZOHFVSmNSa2IKUkJSc2dYaGRTWXU1RkpOblFlcE1xcEVRZjQwQXErSlZGanF4RElSL2xVejUrSWRyU2lpQ1NicGZPbHB5OU51NQoxTGNWY3JzY2VaMkpMUEovRjNIcGZPR2FROTczRXRUR3pTYkhIUVNlN0RDazBzZEN2d0xLMFQ4S1pvb240dTIrCnpsTG5BalIwcUFhTkFyWWtRdUVNaUpBb1BSMW94Sjd0TnZEaXhYNlF6R2RKOEZWYkRFQkFTUmlYcysrdzZMOVMKSzhJRTZnN2k4ZXFmSHVFM3ZWUG0xRmt3enFxcURyTXB6dEFLV2JQYlhzZEt0VERWQTdsUHdNRXNNWUF1eFpDVwpIRkFjUDRLYkZyRGNxVlY1V0ppVWRLRXdjcDU5QWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRVDNvMkJKUHFLYXJlMmJoVVRqYTFtOWxNSDlUQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQW9nMEN0ZlgzSwpzU0pIblo3Vkgvai9CUnNCUEY2Z1hieUtJTEtNeU8vblgxejF1UEJGbklPZ0QvME4zUnJiVStYVVRQc2djTGgrCjZMY0xJSUhmNWpzRlRkL0prVDI0VXZkVzRxcnZqSWRWYU5GbS9Gcm91YkFoU1Y0UEhSMHFBaCtWODY5U1llMisKMHBUL1REVDkwNklqRzVGb2NhZlFBZkVrWUVQNEptMXhXdloxVXNFNlp3ZVVWNVUvUzZNWmVDZC9UZU1SQ0cybApwMTVLNEVWTGdlVGwzK1dCUENBYnBIK0hTa0grdi96OFY2byszYmlrMFdKOVBJZGZuUjZXeENMRDhnaTFzNGZICnpiMm9iYmt0SnlsZEpUUGNyU0wwUGhZMml3ZWtad3R3WWFJNk84aytRckdnUW5PNGp4Q0VUWVN6OVc5ekdoWGcKVW9lU09mdXBGM0hZCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
    server: https://<myk8s-control-plane 컨테이너 IP>:6443 # 자신의 환경에 맞게 변경
  name: kind-myk8s
contexts:
- context:
    cluster: kind-myk8s
    user: kind-myk8s
  name: kind-myk8s
current-context: kind-myk8s
kind: Config
users:
- name: kind-myk8s
  user:
    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJQTBpUUN6VFJiTm93RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRFd01qVXdPVEUyTlRGYUZ3MHlOakV3TWpVd09USXhOVEZhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDK1pLYkcKOERDUHYzQXJOZmJJYVllKzNsMkN3VXFDaktOaWZXNUtMNWUyQ1ZXSzNJK3I2aHZQTEhUMDFyaHc0cHpVNEFlYwpLbUp2L2UrdnFHMG9Za0FzRWltb1YvZi9rdXVORkkvK1loYmhMMmg2NVhYeXY1Q3BOek9NQWdaN1hkMHZoODRKCndQTmg1MWdpWnRuZ1JPQm0ybEV6ejkvWUllY1dYcjc5TlBDdEE1Yldsa1F5YVdBZEE2UHRFcEZLMUZyRWhuZmwKcS8wV3hPaDc2LzNZa3huTFlNOGp2a2lBZkxObnF3djYycWxJOXQ4MUJORDZ1bXBOSytGdmQ3dXpveWJRMlVRQQprdWVUdE1GMTNuNjE3ZUtva05IVkorZWl6OTlENm9TSTNscldlSEFyYklITmpkNmRLcXFWY0s3dytnQXozaFhqCjNoeS9rQ1RQbWIreWJHY1hBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkJQZWpZRWsrb3BxdDdadQpGUk9OcldiMlV3ZjFNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJJRWNsRGhpc1hjeTVleGlvOEI5R1pVRC9hCkxtQ0ppYjJuL3JlcG9pNk1Va2xVZUN2NUloemVpeFZ4aTMwaUZ2R0dEcmVvcFdJdWNFY0ZINHNZUUUxTTBtbTcKY3VRWFBLMkZDaHUrM1hSbjdGS0VZbEwxa25GSjY0endtbXpTNFRTRVI1MVhYQTV1UnRBVlF5TWE5KzZIeHA4NApDMXpzSlVPZTIwYThLRG03am10cFZ2R0xlNzBmNDJGbmdTallSK3dFaC9hQVI0Wk5vQWZNZnR2QmtEby9JUDIzCjhjbG1hL2t4ZTFVbG5nOGw3OVMrMXBvSnJVd3B0OHFqVWVqYllISC9jcThkWFlncnFwT1lpekRGaVlXOEFibXMKNVJRT1M3OS9nL1o0bzJYRlp3Q3dFVDBxR21iZll2Rnc3c0g4SW4zR1BmdUtlQVZYa3JhTE5JaSsyeG5wCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
    client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdm1TbXh2QXdqNzl3S3pYMnlHbUh2dDVkZ3NGS2dveWpZbjF1U2krWHRnbFZpdHlQCnErb2J6eXgwOU5hNGNPS2MxT0FIbkNwaWIvM3ZyNmh0S0dKQUxCSXBxRmYzLzVMcmpSU1AvbUlXNFM5b2V1VjEKOHIrUXFUY3pqQUlHZTEzZEw0Zk9DY0R6WWVkWUltYlo0RVRnWnRwUk04L2YyQ0huRmw2Ky9UVHdyUU9XMXBaRQpNbWxnSFFPajdSS1JTdFJheElaMzVhdjlGc1RvZSt2OTJKTVp5MkRQSTc1SWdIeXpaNnNMK3RxcFNQYmZOUVRRCitycHFUU3ZoYjNlN3M2TW0wTmxFQUpMbms3VEJkZDUrdGUzaXFKRFIxU2Zub3MvZlErcUVpTjVhMW5od0syeUIKelkzZW5TcXFsWEN1OFBvQU05NFY0OTRjdjVBa3o1bS9zbXhuRndJREFRQUJBb0lCQUU0T3pBV2g4ZmJ0WU5xRwpiR0FzUy9nb0NLU1VCSzFUWmFUNmtkNGVkdyt0OGdGVmZoM1loSUJDMU15UXY5dWdQUFpWeHlqeWc1c3d2RFVEClU0V01DbzFIQVFkQTBhOVpsL01tYkFhNlJuRWFVN2FYSHUxZ05ybjVwTXVSQlFGTk1XTE1SZC9mMktqYUJWdE4KV2FSRitNNlNVVnB4cm05Wkx2b1A3RE81b2JiOG5CNVRtTWZrYmtFb0NXa2VKUGJpM3BpN3h0aEJYd1VKdFZhRgprVHUvaGYyVEE0YlJxNkZhT2VzdDhNb1liTzVwaC96Tk1UODU5bE9tQUEvcWIxRnFaMXk1OXc5WTZ1V0RFWEFIClRFTVFISXpMSjVSeUp0bjNmMmJIWktodUZ2bHBMalNJS0wrWUo3eGlkRHlleU9zanBDZ1FkSGVKai9Pc1Ntc2QKZXNSYjh1a0NnWUVBKyt4ZUVXZUwyYjBzMXpQMnlZVEtqM0NxNXZpWkFzU1dJNWxPMGFQYjByQUZaUTNuMUxTRgp4VVZTaEVDU2RPWkp2R0U5V3JmWVoxZVhEODl6OWwraklmLzhsZis3L2NlMlIwRDNTbm9KSXRoZ05LZFNSR1RnCjUwdUxxWTZIMmlJMkFWaVRMNFh1aXp1TDdWcVpTdTdpbGtqczhHVmpTcFpGeWZXd1dyZzN5aHNDZ1lFQXdYbGkKdGNQamw3Zi9CVEU4OUg4UlpJdmk5ckR6eFo2UE5IaTI0WG9qR3ZiTW1rUzlvTnpTQ1UybWpZb0tqUEp4N1ZOMQpVRWVpbStINnptTHRoK240QnZWWEs0SkJMR2tXVmRIRkZTSFFDaXNiS2Q1Mk5JNnlUODJ5cUMyRjdZWmM4VkZmCmw3MDIyZXE5dFRUeGlnRmhqamliSlBFSCtDTHhVNzFicmpDWHByVUNnWUVBOUd4M01HZkl0cS9uSzcyTk9pU08KNi9FaXdBcC9Xc0lsOGRBek93L0NubjZPSFdnS2dNUE43b0s5OXZjM29oZzFmb0xTSm95dGVFYnhtRE50amFOcApBZm4rdGdKMEFWeWRyRENLUGtaOVNzT3BSQ3o3a3NSVnVkUUFZN2lZY3JveXI4ZWl0cjg1blBsZllDRkJEZ1VNCjcwYytMRmdTcURGNDhPUmlBUU12amZVQ2dZQjBRNVNzOUEyVnc4MHRlcHhOdFBwbnVLUm9hSWZsVHRaeHlzVGoKbEhqNklDdHQzVGN6THQvTkZXdXNETE13WmhWT2IrUEVuWGU3UXo1cnZnbE5ycTBNeVd4YnlnZU9QNHhiZ0JaMgpEMzZzVGFFaU5QeEZzeWEyVEQ4N1R6ZjNOOUlzZGlFQzE0TFp5M1c3S2hpb1BSSTUzQXhuVTZ6ZFVXcENUejJECjNOR1ZMUUtCZ0NCczAwYmJQU1B6SUtsYURac0tGNkxUbzRQUGhVQnFKbFZYUm1VQnA2NXZNL2ZqUW5sWWtyNDMKV20wMFVFRTZvOHlsWHZnVnVzY2tjM25kWm8yUVFKOTRqTXdEYWV6cXF1M0cyQnA3VXFMblhrZ1V0bnkrK0dJcQoyQm05Uk5nSjZvUXRZYWNrYjJhaTlEa056QU1abzF4TWNLK0pWWmgzWVltM3RKOFRtZVNmCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
  • Jenkins Item 생성(Pipeline) : item name(k8s-cmd)
    • 생성 후 빌드 진행 및 결과 확인
pipeline {
    agent any
    environment {
        KUBECONFIG = credentials('k8s-crd')
    }
    stages {
        stage('List Pods') {
            steps {
                sh '''
                # Fetch and display Pods
                kubectl get pods -A --kubeconfig "$KUBECONFIG"
                '''
            }
        }
    }
}
  • Jenkins 를 이용한 blue-green 배포 준비
    • 사용했던 디플로이먼트, 서비스 삭제
kubectl delete deploy,svc timeserver
  • 디플로이먼트 / 서비스 yaml 파일 작성
# 
cd dev-app

#
mkdir deploy

#
cat > deploy/echo-server-blue.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-server-blue
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echo-server
      version: blue
  template:
    metadata:
      labels:
        app: echo-server
        version: blue
    spec:
      containers:
      - name: echo-server
        image: hashicorp/http-echo
        args:
        - "-text=Hello from Blue"
        ports:
        - containerPort: 5678
EOF

cat > deploy/echo-server-service.yaml <<EOF
apiVersion: v1
kind: Service
metadata:
  name: echo-server-service
spec:
  selector:
    app: echo-server
    version: blue
  ports:
  - protocol: TCP
    port: 80
    targetPort: 5678
    nodePort: 30000
  type: NodePort
EOF

cat > deploy/echo-server-green.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-server-green
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echo-server
      version: green
  template:
    metadata:
      labels:
        app: echo-server
        version: green
    spec:
      containers:
      - name: echo-server
        image: hashicorp/http-echo
        args:
        - "-text=Hello from Green"
        ports:
        - containerPort: 5678
EOF

#
tree
git add . && git commit -m "Add echo server yaml" && git push -u origin main
  • 직접 블루-그린 업데이트 실행 시
#
cd deploy
kubectl delete deploy,svc --all
kubectl apply -f .

#
kubectl get deploy,svc,ep -owide
curl -s http://127.0.0.1:30000

#
kubectl patch svc echo-server-service -p '{"spec": {"selector": {"version": "green"}}}'
kubectl get deploy,svc,ep -owide
curl -s http://127.0.0.1:30000

#
kubectl patch svc echo-server-service -p '{"spec": {"selector": {"version": "blue"}}}'
kubectl get deploy,svc,ep -owide
curl -s http://127.0.0.1:30000

# 삭제
kubectl delete -f .
cd ..
  • Jenkins Item 생성(Pipeline) : item name(k8s-bluegreen) - Jenkins 통한 k8s 기본 배포
    • 반복 접속 미리 실행
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; echo ; sleep 1  ; kubectl get deploy -owide ; echo ; kubectl get svc,ep echo-server-service -owide ; echo "------------" ; done
혹은 
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; date ; echo "------------" ; sleep 1 ; done
  • pipeline script
pipeline {
    agent any

    environment {
        KUBECONFIG = credentials('k8s-crd')
    }

    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://<자신의 IP>:3000/devops/dev-app.git',  // Git에서 코드 체크아웃
                 credentialsId: 'gogs-crd'  // Credentials ID
            }
        }

        stage('container image build') {
            steps {
                echo "container image build"
            }
        }

        stage('container image upload') {
            steps {
                echo "container image upload"
            }
        }

        stage('k8s deployment blue version') {
            steps {
                sh "kubectl apply -f ./deploy/echo-server-blue.yaml --kubeconfig $KUBECONFIG"
                sh "kubectl apply -f ./deploy/echo-server-service.yaml --kubeconfig $KUBECONFIG"
            }
        }

        stage('approve green version') {
            steps {
                input message: 'approve green version', ok: "Yes"
            }
        }

        stage('k8s deployment green version') {
            steps {
	        	sh "kubectl apply -f ./deploy/echo-server-green.yaml --kubeconfig $KUBECONFIG"
            }
        }

        stage('approve version switching') {
            steps {
                script {
                    returnValue = input message: 'Green switching?', ok: "Yes", parameters: [booleanParam(defaultValue: true, name: 'IS_SWITCHED')]
                    if (returnValue) {
                        sh "kubectl patch svc echo-server-service -p '{\"spec\": {\"selector\": {\"version\": \"green\"}}}' --kubeconfig $KUBECONFIG"
                    }
                }
            }
        }

        stage('Blue Rollback') {
            steps {
                script {
                    returnValue = input message: 'Blue Rollback?', parameters: [choice(choices: ['done', 'rollback'], name: 'IS_ROLLBACk')]
                    if (returnValue == "done") {
                        sh "kubectl delete -f ./deploy/echo-server-blue.yaml --kubeconfig $KUBECONFIG"
                    }
                    if (returnValue == "rollback") {
                        sh "kubectl patch svc echo-server-service -p '{\"spec\": {\"selector\": {\"version\": \"blue\"}}}' --kubeconfig $KUBECONFIG"
                    }
                }
            }
        }
    }
}
  • 배포 후 동작 확인
    • scipt 내에 approval 정책이 있어서 선택해야만 넘어간다
    • Green switching 알림에서 Yes 를 눌러주면 자동으로 전환된다

  • 실습 완료 후 삭제
kubectl delete deploy echo-server-blue echo-server-green ; kubectl delete svc echo-server-service

'Kubernetes' 카테고리의 다른 글

Kubernetes CI/CD Study 1기 | 4주차 ArgoCD 1/3  (0) 2025.11.08
Kubernetes CI/CD Study 1기 | 3주차 #2 Jenkins + ArgoCD  (0) 2025.11.01
Kubernetes CI/CD Study 1기 | 2주차 #1 : Helm  (0) 2025.10.22
Kubernetes CI/CD Study 1기 | 1주차 #3 : Kustomize  (0) 2025.10.16
Kubernetes CI/CD Study 1기 | 1주차 #2 Container Build Tool(Jib, Buildah, Buildpack)  (1) 2025.10.14
'Kubernetes' 카테고리의 다른 글
  • Kubernetes CI/CD Study 1기 | 4주차 ArgoCD 1/3
  • Kubernetes CI/CD Study 1기 | 3주차 #2 Jenkins + ArgoCD
  • Kubernetes CI/CD Study 1기 | 2주차 #1 : Helm
  • Kubernetes CI/CD Study 1기 | 1주차 #3 : Kustomize
nooh_ij
nooh_ij
  • nooh_ij
    HUUN_기술 블로그
    nooh_ij
  • 전체
    오늘
    어제
    • 분류 전체보기 (20)
      • AWS (2)
      • Kubernetes (14)
      • 오류 (0)
      • IT (1)
      • Terraform (1)
      • Codestates (0)
      • 회고록 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
nooh_ij
Kubernetes CI/CD Study 1기 | 3주차 #1 Jenkins + ArgoCD
상단으로

티스토리툴바