CICD STUDY-3주차

dev_suha·2025년 11월 1일
0

실습 환경 구성

kind k8s

(|N/A:N/A) root@DESKTOP-8S932ET:~# 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
Creating cluster "myk8s" ...
 ✓ Ensuring node image (kindest/node:v1.32.8) 🖼
 ✓ Preparing nodes 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-myk8s"
You can now use your cluster with:

kubectl cluster-info --context kind-myk8s

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~# ls
yes  yes.pub
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~# ls
yes  yes.pub
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~# cd /usr/local/
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:/usr/local# ls
bin  etc  games  include  lib  man  sbin  share  src  tektoncd-cli-0.42.0_Linux-64bit.deb
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:/usr/local# cd
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~# ls
yes  yes.pub
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~# mkdir ~/cicd-labs
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~# ls
cicd-labs  yes  yes.pub
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~# cd cicd-labs/
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~/cicd-labs# kind get nodes --name myk8s
myk8s-control-plane
myk8s-worker
(|kind-myk8s:N/A) root@DESKTOP-8S932ET:~/cicd-labs# kubens default
✔ Active namespace is "default"

(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED         STATUS         PORTS                                                           NAMES
c97fa93d706c   kindest/node:v1.32.8   "/usr/local/bin/entr…"   2 minutes ago   Up 2 minutes   0.0.0.0:30000-30003->30000-30003/tcp, 0.0.0.0:45769->6443/tcp   myk8s-control-plane
052045c35377   kindest/node:v1.32.8   "/usr/local/bin/entr…"   2 minutes ago   Up 2 minutes  

kube config 파일 확인

cat ~/.kube/config 

server: https://0.0.0.0:45769

ifconfig

(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.21.203.45  netmask 255.255.240.0  broadcast 172.21.207.255
        inet6 fe80::215:5dff:fecb:7947  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:cb:79:47  txqueuelen 1000  (Ethernet)
        RX packets 7085  bytes 10081448 (10.0 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1525  bytes 122892 (122.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
}(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# curl -k https://172.21.203.45:45769/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"
}(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs#

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://172.21.203.45:30001/#scale=2"

docker compose

docker network ls

(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
98c6ea9dcb48   bridge    bridge    local
d1a972e5533d   host      host      local
70c1f4e24e42   kind      bridge    local
023ed59033a3   none      null      local

docker-compose.yaml


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**
인터페이스 이름역할생성 주체비고
eth0실제 물리/가상 네트워크 카드OS (Ubuntu)외부 인터넷 연결용 (진짜 IP: 172.21.203.45)

jenkins 설정

Jenkins 컨테이너에서 호스트에 도커 데몬 사용 설정 (Docker-out-of-Docker) : Windows (WSL2 + Docker) 사용자

# 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를 지정~~

groupadd -g 2000 -f docker
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

gogs

hp980724

TOKEN = 47c99740cc9e14bf3f490ace539765f389706ad0

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/hp980724/dev-app.git***
*Cloning into 'dev-app'...
...*

#
cd /data/dev-app

#
git --no-pager config --local --list
git config --local user.name "hp980724"
git config --local user.email "hp980724@mail.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**
...

docker hub

dckr_pat_dmkS8UMD8bxc_dYXsRRLAvo29BA

jenkins

일련의 자동화 작업의 순서들의 집합인 Pipeline을 통해 CI/CD 파이프라인을 구축한다.

구성요소설명
Master(Node)웹 UI 및 Job 스케줄러 역할 (요즘은 Controller라고 부름)
Agent(Node)실제 빌드 작업이 수행되는 워커 노드
Job / Pipeline자동화 스크립트 (Jenkinsfile)
PluginGit, Docker, Kubernetes, Slack 등과 연동하는 확장 기능
CredentialGit, Docker Hub 등 외부 서비스 로그인 정보
WorkspaceJob이 실행되는 디렉터리

Jenkins CI Pipeline

플러그인 설치

자격증명 생성

jenkins pipeline script

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = 'hp980724/dev-app' // Docker 이미지 이름
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://http://172.21.203.45/: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*

트러블 슈팅

Every 2.0s: kubectl get deploy,rs,pod -o wide                                                                                                                                                      DESKTOP-8S932ET: Mon Oct 27 23:46:59 2025

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE    CONTAINERS             IMAGES                             SELECTOR
deployment.apps/timeserver   0/2     2            0           117s   timeserver-container   docker.io/hp980724/dev-app:0.0.1   pod=timeserver-pod

NAME                                    DESIRED   CURRENT   READY   AGE    CONTAINERS             IMAGES                             SELECTOR
replicaset.apps/timeserver-7fc659bdcd   2         2         0       117s   timeserver-container   docker.io/hp980724/dev-app:0.0.1   pod=timeserver-pod,pod-template-hash=7fc659bdcd

NAME                              READY   STATUS             RESTARTS   AGE    IP           NODE           NOMINATED NODE   READINESS GATES
pod/timeserver-7fc659bdcd-d8cp5   0/1     ImagePullBackOff   0          117s   10.244.1.3   myk8s-worker   <none>           <none>
pod/timeserver-7fc659bdcd-vqrdv   0/1     ImagePullBackOff   0          117s   10.244.1.4   myk8s-worker   <none>           <none> 

image pull error (ErrImagePull / ErrImagePullBackOff) 발생

주로 다음과 같은 3가지의 경우에 발생

  • 컨테이너 이미지 정보를 잘못 기입하는 경우에 발생
  • 이미지 저장소에 이미지가 없는 경우
  • 이미지 가져오는 자격 증명이 없는 경우에 발생

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

secret 적용해서 다시 가져오기

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
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# kubectl get deploy,pod
NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/timeserver   1/2     2            1           5m1s

NAME                              READY   STATUS              RESTARTS   AGE
pod/timeserver-7b5b869d64-2zr4h   0/1     ContainerCreating   0          0s
pod/timeserver-7b5b869d64-w65ph   1/1     Running             0          70s
pod/timeserver-7fc659bdcd-d8cp5   0/1     ImagePullBackOff    0          5m1s
pod/timeserver-7fc659bdcd-vqrdv   0/1     Terminating         0          5m1s

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

curl http://127.0.0.1:30000
curl http://127.0.0.1:30000
curl http://127.0.0.1:30000/healthz
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# kubectl get service,ep timeserver -owide
NAME                 TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE   SELECTOR
service/timeserver   NodePort   10.96.77.129   <none>        80:30000/TCP   17s   pod=timeserver-pod

NAME                   ENDPOINTS                     AGE
endpoints/timeserver   10.244.1.5:80,10.244.1.6:80   17s
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# curl http://127.0.0.1:30000
The time is 2:51:47 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-w65ph
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# curl http://127.0.0.1:30000/healthz
Healthy(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicdwhile true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; sleep 1 ; done ; done
for i in {1..100};  do curl -s http://127.0.0.1:30000 | grep name; done | sort | uniq -c | sort -nr
The time is 2:52:09 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-w65ph
The time is 2:52:20 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 2:52:11 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-w65ph
The time is 2:52:12 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-w65ph
The time is 2:52:13 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-w65ph
^C
     52 Server hostname: timeserver-7b5b869d64-2zr4h
     48 Server hostname: timeserver-7b5b869d64-w65ph
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# kubectl scale deployment timeserver --replicas 4
deployment.apps/timeserver scaled
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# kubectl get service,ep timeserver -owide
NAME                 TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE   SELECTOR
service/timeserver   NodePort   10.96.77.129   <none>        80:30000/TCP   68s   pod=timeserver-pod

NAME                   ENDPOINTS                                               AGE
endpoints/timeserver   10.244.1.5:80,10.244.1.6:80,10.244.1.7:80 + 1 more...   68s
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# 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
The time is 2:52:32 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 2:52:33 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 2:52:34 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-7vczh
The time is 2:52:35 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-vgzrz
The time is 2:52:36 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 2:52:37 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-7vczh
The time is 2:52:38 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 2:52:39 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 2:52:40 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-vgzrz
^C
     26 Server hostname: timeserver-7b5b869d64-vgzrz
     26 Server hostname: timeserver-7b5b869d64-2zr4h
     25 Server hostname: timeserver-7b5b869d64-7vczh
     23 Server hostname: timeserver-7b5b869d64-w65ph
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs#

server.py 코드 변경

docker exec -it gogs bash
cd /data/dev-app

# VERSION 변경 : 0.0.2
# server.py 변경 : 0.0.2
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main

확인

(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# 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
The time is 3:08:15 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-vgzrz
The time is 3:08:16 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 3:08:17 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 3:08:27 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-7vczh
The time is 3:08:19 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 3:08:20 PM, VERSION 0.0.1
Server hostname: timeserver-7b5b869d64-2zr4h
The time is 3:08:21 PM, VERSION 0.0.1

     27 Server hostname: timeserver-7b5b869d64-7vczh
     26 Server hostname: timeserver-7b5b869d64-w65ph
     24 Server hostname: timeserver-7b5b869d64-vgzrz
     23 Server hostname: timeserver-7b5b869d64-2zr4h
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# 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"
deployment.apps/timeserver image updated
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# kubectl get deploy,rs,pod,svc,ep -owide
NAME                         READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS             IMAGES                   SELECTOR
deployment.apps/timeserver   3/4     3            3           23m   timeserver-container   hp980724/dev-app:0.0.2   pod=timeserver-pod

NAME                                    DESIRED   CURRENT   READY   AGE   CONTAINERS             IMAGES                             SELECTOR
replicaset.apps/timeserver-64b4759458   3         3         1       15s   timeserver-container   hp980724/dev-app:0.0.2             pod=timeserver-pod,pod-template-hash=64b4759458
replicaset.apps/timeserver-7b5b869d64   2         2         2       20m   timeserver-container   docker.io/hp980724/dev-app:0.0.1   pod=timeserver-pod,pod-template-hash=7b5b869d64
replicaset.apps/timeserver-7fc659bdcd   0         0         0       23m   timeserver-container   docker.io/hp980724/dev-app:0.0.1   pod=timeserver-pod,pod-template-hash=7fc659bdcd

NAME                              READY   STATUS              RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
pod/timeserver-64b4759458-btfhx   0/1     ContainerCreating   0          2s    <none>       myk8s-worker   <none>           <none>
pod/timeserver-64b4759458-mjs7d   1/1     Running             0          15s   10.244.1.9   myk8s-worker   <none>           <none>
pod/timeserver-64b4759458-mkjdp   0/1     ContainerCreating   0          15s   <none>       myk8s-worker   <none>           <none>
pod/timeserver-7b5b869d64-2zr4h   1/1     Running             0          18m   10.244.1.6   myk8s-worker   <none>           <none>
pod/timeserver-7b5b869d64-7vczh   1/1     Terminating         0          16m   10.244.1.7   myk8s-worker   <none>           <none>
pod/timeserver-7b5b869d64-vgzrz   1/1     Terminating         0          16m   10.244.1.8   myk8s-worker   <none>           <none>
pod/timeserver-7b5b869d64-w65ph   1/1     Running             0          20m   10.244.1.5   myk8s-worker   <none>           <none>

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE    SELECTOR
service/kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        172m   <none>
service/timeserver   NodePort    10.96.77.129   <none>        80:30000/TCP   17m    pod=timeserver-pod

NAME                   ENDPOINTS                                   AGE
endpoints/kubernetes   172.18.0.3:6443                             172m
endpoints/timeserver   10.244.1.5:80,10.244.1.6:80,10.244.1.9:80   17m
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# kubectl get deploy timeserver
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
timeserver   4/4     4            4           24m
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# kubectl get pods -l pod=timeserver-pod
NAME                          READY   STATUS        RESTARTS   AGE
timeserver-64b4759458-btfhx   1/1     Running       0          17s
timeserver-64b4759458-gwsdk   1/1     Running       0          14s
timeserver-64b4759458-mjs7d   1/1     Running       0          30s
timeserver-64b4759458-mkjdp   1/1     Running       0          30s
timeserver-7b5b869d64-2zr4h   1/1     Terminating   0          19m
timeserver-7b5b869d64-7vczh   1/1     Terminating   0          16m
timeserver-7b5b869d64-vgzrz   1/1     Terminating   0          16m
timeserver-7b5b869d64-w65ph   1/1     Terminating   0          20m
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# ```bash
# 반복 접속 해두기 : 부하분산 확인
**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
```^C
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# 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
The time is 3:09:17 PM, VERSION 0.0.2
Server hostname: timeserver-64b4759458-mjs7d
The time is 3:09:18 PM, VERSION 0.0.2
Server hostname: timeserver-64b4759458-mkjdp
The time is 3:09:19 PM, VERSION 0.0.2
Server hostname: timeserver-64b4759458-btfhx
The time is 3:09:20 PM, VERSION 0.0.2
Server hostname: timeserver-64b4759458-mkjdp
The time is 3:09:21 PM, VERSION 0.0.2

Gogs Webhooks 설정 : Jenkins Job Trigger

docker exec -it gogs bash

/data/gogs/conf/app.ini 수정

BRAND_NAME = Gogs
RUN_USER   = git
RUN_MODE   = prod

[database]
TYPE     = sqlite3
HOST     = 127.0.0.1:5432
NAME     = gogs
SCHEMA   = public
USER     = gogs
PASSWORD =
SSL_MODE = disable
PATH     = data/gogs.db

[repository]
ROOT           = /data/git/gogs-repositories
DEFAULT_BRANCH = main

[server]
DOMAIN           = localhost
HTTP_PORT        = 3000
EXTERNAL_URL     = http://172.21.203.45:3000/
DISABLE_SSH      = false
SSH_PORT         = 22
START_SSH_SERVER = false
OFFLINE_MODE     = false

[email]
ENABLED = false

[auth]
REQUIRE_EMAIL_CONFIRMATION  = false
DISABLE_REGISTRATION        = false
ENABLE_REGISTRATION_CAPTCHA = true
REQUIRE_SIGNIN_VIEW         = false

[user]
ENABLE_EMAIL_NOTIFICATION = false

[picture]
DISABLE_GRAVATAR        = false
ENABLE_FEDERATED_AVATAR = false

[session]
PROVIDER = file

[log]
MODE      = file
LEVEL     = Info
ROOT_PATH = /app/gogs/log

[security]
INSTALL_LOCK = true
SECRET_KEY   = SeG2qVPPLWBL5zh
LOCAL_NETWORK_ALLOWLIST = 172.21.203.45
**docker compose restart gogs**

gogs web-hook 설정



  • test 진행시 jenkins 미설정으로 error

jenkins 설정

  • gogs webhook payload에 맞게 item name 작성하기

jenkins file 생성 (gogs)

docker exec -it gogs bash
9657b615ff9e:/data/dev-app# pwd
/data/dev-app
9657b615ff9e:/data/dev-app# tree
.
├── Dockerfile
├── VERSION
└── server.py

0 directories, 3 files
9657b615ff9e:/data/dev-app#

Jenkinsfile

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = 'hp980724/dev-app'
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://172.21.203.45:3000/hp980724/dev-app.git',
                 credentialsId: 'gogs-crd'  // Credentials ID
            }
        }
        stage('Read VERSION') {
            steps {
                script {

                    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."
        }
    }
}

troubleshooting

ERROR: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Head "http://%2Fvar%2Frun%2Fdocker.sock/_ping": dial unix /var/run/docker.sock: connect: permission denied

docker.sock 접근을 위한 수정

(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# stat -c '%g' /var/run/docker.sock
989
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# echo DOCKER_GID=989 > .env
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# ls
docker-compose.yaml
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# vi docker-compose.yaml
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# which docker
/usr/bin/docker
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# vi docker-compose.yaml
(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# cat docker-compose.yaml
services:

  jenkins:
    container_name: jenkins
    image: jenkins/jenkins
    networks:
      - kind
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - jenkins_home:/var/jenkins_home
      - /usr/bin/docker:/usr/bin/docker:ro
    group_add:
      - "${DOCKER_GID}"
    restart: unless-stopped

  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

1️⃣ /usr/bin/docker:/usr/bin/docker:ro

  • 의미: Jenkins 컨테이너 안에 도커 명령어(docker CLI)를 복사하는 것입니다.
    • 왼쪽 /usr/bin/docker → 호스트에 설치된 Docker 실행파일
    • 오른쪽 /usr/bin/docker → Jenkins 컨테이너 안의 경로
    • :ro → 읽기 전용(Read-Only)으로 마운트 (수정 불가, 안전함)
  • 왜 필요한가? Jenkins는 내부에서 sh 'docker build …' 같은 명령을 실행합니다. 하지만 Jenkins 이미지에는 docker 명령어가 기본 설치돼 있지 않습니다. 그래서 호스트의 docker CLI 바이너리를 그대로 빌려 쓰는 것입니다.

🔌 2️⃣ group_add: - "${DOCKER_GID}"

  • 의미: Jenkins 컨테이너 안의 jenkins 사용자에게, 호스트 도커 소켓(/var/run/docker.sock)이 속한 그룹(GID)의 권한을 추가합니다.
  • 왜 필요한가? Docker는 /var/run/docker.sock 파일을 통해 통신하는데, 이 파일은 보통 다음처럼 되어 있습니다:
    srw-rw---- root docker /var/run/docker.sock
    
    즉, rootdocker 그룹에 속해야 접근 가능합니다. Jenkins는 기본적으로 jenkins 유저로 실행되기 때문에 접근이 막힙니다. 따라서 .env 파일에 DOCKER_GID=989를 적어두고, 이 값을 group_add로 전달하여 Jenkins 컨테이너 안에서도 jenkins 유저가 도커 그룹(989번)에 포함되도록 만든 것입니다.

(|kind-myk8s:default) root@DESKTOP-8S932ET:~/cicd-labs# 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
deployment.apps/timeserver image updated
The time is 12:46:39 PM, VERSION 0.0.3
Server hostname: timeserver-7577795f7b-q4whg
The time is 12:46:40 PM, VERSION 0.0.3
Server hostname: timeserver-7577795f7b-q4whg

Jenkins Generic Webhook Trigger 플러그인으로 설정 구현

Generic Webhook Trigger 플러그인 설치

  • pipeline 생성후 Generic Webhook Trigger 활성화
  • Post content parameters 클릭
Name of variableExpressionJSONPath
ref$.ref0
repository_name$.repository.name0
pusher_name$.pusher.username0
commit_msg$.commits[0].message0
  • token : my-webhook-token

pipeline script

pipeline {
  agent any

  triggers {
    GenericTrigger(
      genericVariables: [
        [key: 'ref',             value: '$.ref'],
        [key: 'repository_name', value: '$.repository.name'],
        [key: 'pusher_name',     value: '$.pusher.username'],
        [key: 'commit_msg',      value: '$.commits[0].message']
      ],
      causeString: 'Triggered on $ref by $pusher_name',
      token: 'my-webhook-token',                 // Gogs Webhook URL의 token과 일치
      printContributedVariables: true,           // 초기 디버그용(안정화 후 false)
      printPostContent: true,                    // 초기 디버그용(안정화 후 false)
      regexpFilterText: '$ref',
      regexpFilterExpression: '^refs/heads/(main|develop)$'  // main, develop만 처리
    )
  }

  environment {
    GIT_URL = 'http://172.21.203.45:3000/hp980724/dev-app.git'
    IMAGE   = 'hp980724/dev-app'
  }

  stages {
    stage('Checkout') {
      steps {
        // Git에서 소스 코드 체크아웃
        git url: env.GIT_URL, branch: 'main', credentialsId: 'gogs-crd'
      }
    }

    stage('Read VERSION') {
      steps {
        // VERSION 파일 읽기
        script { env.VERSION = readFile('VERSION').trim() }
        echo "Version: ${env.VERSION}, Ref: ${env.ref}, Pusher: ${env.pusher_name}"
      }
    }

    stage('Docker Build & Push') {
      when {
        expression { return env.ref == 'refs/heads/main' || env.ref == 'refs/heads/develop' }
      }
      steps {
        withCredentials([usernamePassword(credentialsId: 'dockerhub-crd', usernameVariable: 'DOCKERHUB_USER', passwordVariable: 'DOCKERHUB_PASS')]) {
          // Docker Hub 로그인
          sh 'echo "$DOCKERHUB_PASS" | docker login --username "$DOCKERHUB_USER" --password-stdin'
        }
        // Docker 빌드
        sh 'docker build -t ${IMAGE}:${VERSION} .'
        // Docker 푸시
        sh 'docker push ${IMAGE}:${VERSION}'
        sh 'docker tag ${IMAGE}:${VERSION} ${IMAGE}:latest'
        sh 'docker push ${IMAGE}:latest'
      }
    }
  }

  post {
    success { echo "✅ Pushed: ${IMAGE}:${VERSION} (and latest)" }
    failure { echo "❌ Pipeline failed. Check logs." }
    always  { echo "Build# ${env.BUILD_NUMBER}, Repo=${env.repository_name}, Msg=${env.commit_msg}" }
  }
}

빌드 이미지 TagGit Commit ID(SHA) 로 매칭(코드 버전과 이미지 버전 식별)하는 이미지 빌드 CI Pipeline 작성

jenkins file 수정

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = 'hp980724/dev-app'
    }
    stages {
        stage('Checkout') {
            steps {
                git branch: 'main',
                    url: 'http://172.21.203.45:3000/hp980724/dev-app.git',
                    credentialsId: 'gogs-crd'  // Gogs credentials ID
            }
        }
        stage('Get Commit SHA') {
            steps {
                script {
                    def commitSHA = sh(script: "git rev-parse HEAD", returnStdout: true).trim()
                    echo "Commit SHA: ${commitSHA}"
                    env.DOCKER_TAG = commitSHA
                }
            }
        }
        stage('Docker Build and Push') {
            steps {
                script {
                    docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-crd') {
                        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."
        }
    }
}

디플로이먼트에 파드 컨테이너 이미지에 latest 사용해서 배포 후 이미지 버전 업데이트 후 적용

명령어

kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:0.0.2 
  • kubectl set image: timeserver 배포의 timeserver-container 파드에 이미지를 새로 설정 (0.0.2 버전 사용)
  • CI 이후에
kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:latest
  • dev-app:latest 를 적용하여도 이전꺼 가져옴

deployment 수정

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: timeserver
spec:
  replicas: 4
  selector:
    matchLabels:
      pod: timeserver-pod
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      labels:
        pod: timeserver-pod
    spec:
      containers:
      - name: timeserver-container
        image: hp980724/dev-app:latest  # latest 이미지
        imagePullPolicy: Always  # 항상 최신 이미지를 가져옴
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 80
            scheme: HTTP
          initialDelaySeconds: 30
          periodSeconds: 30
          successThreshold: 1
          timeoutSeconds: 5
      imagePullSecrets:
      - name: dockerhub-secret
EOF
kubectl rollout status deployment timeserver
```![](https://velog.velcdn.com/images/sly0724/post/76d6320a-bfef-482a-8171-d2d1882b64bf/image.png)

0개의 댓글