로컬 환경에서 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 등록 : Application → NEW 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 |