Cloud

Gitops CI & CD 구성(Jenkins & ArgoCD)

Hanhorang31 2025. 11. 2. 03:25

로컬 환경에서 CI/CD를 구성합니다.

 

CI : Jenkins(Docker)

컨테이너 저장소 : DockerHub

CD서버 : ArgoCD

소스 저장소 : Gogs

 

사전 환경 구성

1. K8S 클러스터 구성

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


docker ps 
CONTAINER ID   IMAGE                  COMMAND                   CREATED         STATUS         PORTS                                                           NAMES
0a30fc5ca99f   kindest/node:v1.32.8   "/usr/local/bin/entr…"   4 minutes ago   Up 4 minutes   0.0.0.0:30000-30003->30000-30003/tcp, 0.0.0.0:57714->6443/tcp   myk8s-control-plane
ae01d2c00f27   kindest/node:v1.32.8   "/usr/local/bin/entr…"   4 minutes ago   Up 4 minutes                                                                   myk8s-worker

# 접근 확인 
ifconfig | grep 192.

# 클러스터 확인
curl https://192.168.1.5:57714/version -k
{
  "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/arm64"
}

2. Jenkins 설치

/var/run/docker.sock:/var/run/docker.sock : DooD 설정 진행 컨테이너 안에서 도커 호스트의 도커 데몬과 직접 통신하기 위해 설정 → 젠킨스 컨테이너에서 도커 호스트(필자 컴퓨터 환경 - 맥) 데몬과 직접 통신하기 위해 설정


mkdir cicd-labs
cd cicd-labs

# cicd-labs 작업 디렉토리 IDE(VSCODE 등)로 열어두기

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

  jenkins:
    container_name: jenkins
    image: jenkins/jenkins
    restart: unless-stopped
    networks:
      - cicd-network
    ports:
      - "8080:8080"
      - "50000:50000"                      # Jenkins Agent - Controller : JNLP
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./jenkins_home:/var/jenkins_home   # (방안1) 권한등으로 실패 시 ./ 제거하여 도커 볼륨으로 사용 (방안2)

  gogs:
    container_name: gogs
    image: gogs/gogs
    restart: unless-stopped
    networks:
      - cicd-network
    ports:
      - "10022:22"                         # Git Clinet - Git SSH Service : push, pull, clone
      - "3000:3000"
    volumes:
      - ./gogs-data:/data                  # (방안1) 권한등으로 실패 시 ./ 제거하여 도커 볼륨으로 사용 (방안2)

volumes:
  jenkins_home:
  gogs-data:

networks:
  cicd-network:
    driver: bridge
EOT

# 구동 
docker compose up -d
docker compose ps

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

>> container : jenkins <<
jenkins
/


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

2.1 젠킨스 초기 설정

# 초기 비밀번호 확인 
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword


# 접속, 초기 비밀번호 입력
open "http://127.0.0.1:8080" # macOS

# 초기 플러그인 설치, 환경 설정 진행

IP는 접속 가능한 로컬 IP 입력

2.2 Jenkin 컨테이너에서 호스트 도커 데몬 사용 (Docker-out-of-Docker)

# 젠킨스 컨테이너 루트 유저 접근 
docker compose exec --privileged -u root jenkins bash

# Install docker-ce-cli
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 wget -y

# Jenkins 컨테이너에서 Jenkins 유저 docker 사용 권한 부여 
groupadd -g 2000 -f docker  # macOS(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
---
srwxr-xr-x 1 root docker 0 Nov  1 12:25 /var/run/docker.sock
docker:x:2000:jenkins

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

# 최종 접근 확인
docker compose exec jenkins id
uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins),2000(docker)

(참고) Mac 재부팅 이후 다시 환경 구성시


# Mac 재부팅 이후 환경 구성
docker compose start
docker compose ps

docker compose exec --privileged -u root jenkins ls -l /var/run/docker.sock
docker compose exec --privileged -u root jenkins chgrp docker /var/run/docker.sock
docker compose exec jenkins docker info 

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

3. Gogs 환경 설정


# Gogs 초기 환경 설정
open "http://127.0.0.1:3000/install"

3.1 초기 관리자 설정

  • 데이터베이스 유형 : SQLite3
  • 애플리케이션 URL : http://<각자 자신의 IP>:3000/
  • 기본 브랜치 : main

3.2 저장소 2개 생성

3.3 토큰 발급 : Your Settings → Applications : Generate New Token


# gogs 컨테이너 접근
docker exec -it gogs bash
 
# 초기 환경 설정
TOKEN=08abcecaa7b2de2d908cee99cb5ab6be86ed6caa
MyIP=192.168.1.5

# 생성한 Repo Clone 
git clone http://devops:$TOKEN@$MyIP:3000/devops/dev-app.git 

# Git 설정
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 remote -v
---
* main
origin  http://devops:08abcecaa7b2de2d908cee99cb5ab6be86ed6caa@192.168.1.5:3000/devops/dev-app.git (fetch)
origin  http://devops:08abcecaa7b2de2d908cee99cb5ab6be86ed6caa@192.168.1.5:3000/devops/dev-app.git (push)
d3e4e9075f8a:/app/gogs/dev-app# 


# 소스 코드 생성 
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

# 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

(gogs) 에러 트러블슈팅

초기 설정을 잘못하여 CSS가 깨진다면 다음과 같이 컨테이너 초기화를 하고 다시 설정을 진행합니다.

# gogs 컨테이너 중지 및 파일 삭제 
docker compose down gogs
rm -rf gogs-data 

# restart 
docker compose up gogs -d 

# 초기 설정 진행

4. 도커 허브

4.1 개인 도커허브 계정에서 토큰 발급

 

4.2 개인 도커허브 레파지토리 생성

Jenkins CI Pipeline 구성

1. Jenkins 설정

1.1 플러그인 설치 : Jenkins 관리 → Plugins

  • Pipeline Stage View
  • Docker Pipeline
  • Gogs(릴리즈가 오래되어 실무에서는 사용 금지)

1.2 자격증명 설정 : Jenkins 관리 → Credentials → Globals → Add Credentials

  • gogs-crd : 패스워드 (gogs 토큰)
  • dockerhub-crd : 패스워드(dockerhub 토큰)

1.3 Jenkins 파이프라인 생성 : 새로운 아이템 → Pipeline

 

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

(참고) 트러블슈팅 - Jenkins 에서 도커 권한 부족으로 실행이 안되는 경우

필자 로컬 Docker 그룹 확인시 Docker 가 아닌 daemon으로 설정으로 되어 있었습니다.

컨테이너 루트 유저로 접근이 되어 컨테이너 내 docker 소켓 실행을 jenkins유저에게도 부여하였습니다.


ls -l /var/run/docker.sock
lrwxr-xr-x  1 root  daemon  41 10 30 19:08 /var/run/docker.sock -> /Users/hanseungho/.docker/run/docker.sock


# 젠킨스 컨테이너 루트 유저 접근 
docker compose exec --privileged -u root jenkins bash

# 젠킨스 유저 권한 부여
chown jenkins:jenkins /var/run/docker.sock

1.4 쿠버네티스 설정

Kind 클러스터에서 도커허브에 접근하기 위해 설정

 

 

# k8s secret : 도커 자격증명 설정 
kubectl get secret -A  # 생성 시 타입 지정

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


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

# 디플로이먼트 오브젝트 업데이트 : 시크릿 적용 >> 아래 도커 계정 부분만 변경해서 배포해보자
cat <
 

1.5 Gogs Webhooks 설정

개발팀 Repo → Jenkins 자동화 설정 작업 단계입니다.

  • gogs 에 /data/gogs/conf/app.ini 파일 수정 및 재기동

docker compose exec --privileged -u root gogs bash


vi /data/gogs/conf/app.ini
--
..
[security]
INSTALL_LOCK = true
SECRET_KEY   = j2xaUPQcbAEwpIu
LOCAL_NETWORK_ALLOWLIST = 192.168.1.5 # 각자 자신의 IP 추가 


docker compose restart gogs 

Gogs - [dev-app] - Setting → Webhooks → Gogs

Jenkins → New Item → Pipeline 생성

  • 깃허브 설정(URL, 크리덴셜, 트리거)
  • Pipleline script for SCM(repo에서 스크립트 구성)

docker compose exec --privileged -u root gogs bash

# VERSION 업데이트 
# VERSION 파일 : 0.0.2 수정
# server.py 파일 : 0.0.2 수정


# Jenkins 파일 생성 
## Jenkins 파이프라인 복사 


# 깃 업데이트 && 푸시 
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main

ArgoCD Pipeline 구성

ArgoCD를 통해 Git → (DESIRED) ArgoCD (LIVE) ⇒ K8S 구성

1.1 ArgoCD 설치(3.1.9)


kubectl create ns argocd
cat < argocd-values.yaml
dex:
  enabled: false

server:
  service:
    type: NodePort
    nodePortHttps: 30002
  extraArgs:
    - --insecure  # HTTPS 대신 HTTP 사용
EOF

# Helm 설치
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd --version 9.0.5 -f argocd-values.yaml --namespace argocd

# 확인 
kubectl get pod,svc,ep,secret,cm -n argocd
kubectl get crd | grep argo


# 최초 접속 암호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d ;echo


# 접속 
open "http://127.0.0.1:30002"

1.2 관리 Repo 등록

ArgoCD Settings → Repositories → CONNECT REPO

패스워드는 gogs 토큰을 입력합니다.

1.3 예제 애플리케이션 구성과 푸시


MyIP=192.168.1.5
TOKEN=08abcecaa7b2de2d908cee99cb5ab6be86ed6caa
echo $MyIP $TOKEN

# gogs ops-deploy git clone 
git clone http://devops:$TOKEN@$MyIP:3000/devops/ops-deploy.git
cd ops-deploy

# 초기 설정
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
git --no-pager branch
git remote -v

# 예제 차트 생성
VERSION=1.26.1
mkdir nginx-chart
mkdir nginx-chart/templates


cat > nginx-chart/VERSION <<EOF
$VERSION
EOF

cat > nginx-chart/templates/configmap.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}
data:
  index.html: |
{{ .Values.indexHtml | indent 4 }}
EOF

cat > nginx-chart/templates/deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      containers:
      - name: nginx
        image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        ports:
        - containerPort: 80
        volumeMounts:
        - name: index-html
          mountPath: /usr/share/nginx/html/index.html
          subPath: index.html
      volumes:
      - name: index-html
        configMap:
          name: {{ .Release.Name }}
EOF

cat > nginx-chart/templates/service.yaml <<EOF
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
spec:
  selector:
    app: {{ .Release.Name }}
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 30000
  type: NodePort
EOF

cat > nginx-chart/values-dev.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>DEV : Nginx version $VERSION</p>
  </body>
  </html>

image:
  repository: nginx
  tag: $VERSION

replicaCount: 1
EOF

cat > nginx-chart/values-prd.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>PRD : Nginx version $VERSION</p>
  </body>
  </html>

image:
  repository: nginx
  tag: $VERSION

replicaCount: 2
EOF

cat > nginx-chart/Chart.yaml <<EOF
apiVersion: v2
name: nginx-chart
description: A Helm chart for deploying Nginx with custom index.html
type: application
version: 1.0.0
appVersion: "$VERSION"
EOF

# repo 업데이트 & 푸시
git status && git add . && git commit -m "Add nginx helm chart" && git push -u origin main

1.4 Argo CD에 App 등록 : ApplicationNEW APP


# 클러스터 환경 내 예제 네임스페이스 생성 
kubectl create ns dev-nginx

# 구성 확인
kubectl get applications -n argocd
NAME        SYNC STATUS   HEALTH STATUS
dev-nginx   OutOfSync     Missing

# ArogCD Sync (웹 콘솔)


kubectl get applications -n argocd
NAME        SYNC STATUS   HEALTH STATUS
dev-nginx   Synced        Healthy

(참고) ArgoCD 구성은 yaml로도 가능합니다.


 

MyIP=192.168.1.5

cat <https://kubernetes.default.svc
EOF

kubectl describe applications -n argocd dev-nginx

1.5 ArgoCD 즉시 반영 Webhook 설정

Gogs Repo(ops-deploy) 에서 Wehhook 설정

  • URL : ArgoCD UR

# 반영 확인
sed -i "s|replicaCount: 2|replicaCount: 3|g" values-dev.yaml
git add values-dev.yaml && git commit -m "Modify nginx-chart : values-dev.yaml" && git push -u origin main

CI & CD 전체 구성

이제 CI & CD 파이프라인을 연결하는 변경 반영만 구성하면 전체 CI & CD가 구성됩니다.

연결은 기본적으로 이미지 태그에 의해 변경됩니다.

ArgoCD에서 바라보는 이미지 태그 수정을 CI Jenkins 를 통해 진행합니다.


mkdir dev-app 
# 도커 계정 정보
DHUSER=<도커 허브 계정>
# 버전 정보 
VERSION=0.0.1

#
cat > dev-app/VERSION <<EOF
$VERSION
EOF

cat > dev-app/timeserver.yaml <<EOF
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:$VERSION
        livenessProbe:
          initialDelaySeconds: 30
          periodSeconds: 30
          httpGet:
            path: /healthz
            port: 80
            scheme: HTTP
          timeoutSeconds: 5
          failureThreshold: 3
          successThreshold: 1
      imagePullSecrets:
      - name: dockerhub-secret
EOF

cat > dev-app/service.yaml <<EOF
apiVersion: v1
kind: Service
metadata:
  name: timeserver
spec:
  selector:
    pod: timeserver-pod
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    nodePort: 30000
  type: NodePort
EOF

#
git add . && git commit -m "Add dev-app deployment yaml" && git push -u origin main

 

#
echo $MyIP

cat <https://kubernetes.default.svc
EOF

Jenkins SCM-Pipeline(SCM:git) 파일에서 단계 추가(ops-deploy


 

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
        GOGSCRD = credentials('gogs-crd')
    }
    stages {
        stage('dev-app 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")
                    }
                }
            }
        }
        stage('ops-deploy Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://<자신의 IP>:3000/devops/ops-deploy.git',  // Git에서 코드 체크아웃
                 credentialsId: 'gogs-crd'  // Credentials ID
            }
        }
        stage('ops-deploy version update push') {
            steps {
                sh '''
                OLDVER=$(cat dev-app/VERSION)
                NEWVER=$(echo ${DOCKER_TAG})
                sed -i '' "s/$OLDVER/$NEWVER/" dev-app/timeserver.yaml
                sed -i '' "s/$OLDVER/$NEWVER/" dev-app/VERSION
                git add ./dev-app
                git config user.name "devops"
                git config user.email "a@a.com"
                git commit -m "version update ${DOCKER_TAG}"
                git push http://${GOGSCRD_USR}:${GOGSCRD_PSW}@<자신의 IP>:3000/devops/ops-deploy.git
                '''
            }
        }
    }
    post {
        success {
            echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
        }
        failure {
            echo "Pipeline failed. Please check the logs."
        }
    }
}
 

동작 확인

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

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

'Cloud' 카테고리의 다른 글

ArgoCD 접근제어 설정 방법  (0) 2025.11.16
ArgoCD 정리 (1)  (0) 2025.11.08
Helm 과 lgtm Stack 맛보기(Loki)  (0) 2025.10.26
쿠버네티스 GItOps  (0) 2025.10.19
Amazon VPC Lattice for Amazon EKS  (0) 2025.04.27