일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- ebs 재부팅
- Terrafrom
- 테라폼 캐시
- AWS EBS
- EBS
- /etc/fstab 뜻
- docker -i -t
- xfs_quota
- 볼륨추가
- 리눅스
- Mac Terraform
- 테라폼 맥
- 컨테이너 터미널
- 컨테이너 터미널 로그아웃
- 테라폼 설치
- Authenticator
- EBS 최적화
- 텔레메트리란
- 디스크 성능테스트
- EC2
- 테라폼 자동완성
- 리눅스 시간대
- epxress-generator
- AWS
- MFA 분실
- MFA 인증
- 볼륨 연결
- ebs 마운트
- /etc/fstab 설정
- docker 상태
- Today
- Total
I got IT
K8S 환경에서 CI/CD 구축하기 - 1 본문
※ 해당 글은 CloudNet@ gasida 님의 EKS 스터디 내용을 참고하여 작성하였습니다.
들어가며

CI/CD는 Continuous Integration과 Continuous Delivery(또는 Deployment)의 약자로, 소프트웨어 개발 과정에서 지속적으로 코드의 통합과 배포를 자동화하여 품질과 속도를 높이는 개발 방식입니다.
특히 Micro Service Architecture 환경에서는 작은 단위로 나뉜 서비스가 독립적으로 빠르게 배포 및 업데이트되어야 하므로 CI/CD가 필수적입니다.
또한, CI/CD는 소프트웨어 개발 주기(SDLC) 전반에 걸쳐 버그를 빠르게 발견하고 해결하여 신속한 배포와 안정성을 보장합니다.
CI/CD 에는 다양한 툴이 사용되고 CNCF에 등록된 다양한 오픈소스들도 포함이 됩니다. 사용자의 필요에 따라 자신에게 맞는 툴을 사용하고 익히는 것이 중요합니다.
이번 포스팅에서는 K8S 환경에서는 일반적으로 CI/CD를 어떻게 구축하는 지, 어떤 툴을 사용하는 지 등을 실습을 통해 확인해 보도록하겠습니다.
K8S CI/CD 실습
실습 아키텍처

이번 실습의 CI/CD 과정은 다음과 같습니다. 가장 일반적인 CI/CD 과정입니다.
먼저, 개발팀의 GitHub Repository에서 변경된 코드가 CI 서버(Jenkins 등)로 전달됩니다.
CI 서버는 전달받은 코드를 빌드하여 컨테이너 이미지로 만든 후, 컨테이너 이미지 저장소에 업로드합니다.
이후 DevOps 팀의 GitHub Repository에는 배포를 위한 설정이나 매니페스트가 저장되어 있으며, 변경 사항이 생기면 CD 서버(Argo CD 등)가 이를 감지하여 Kubernetes 클러스터(kind 환경)에 자동으로 배포하거나 삭제하는 작업을 수행합니다.
마지막으로 Kubernetes 클러스터는 컨테이너 이미지 저장소에서 이미지를 다운로드하여 어플리케이션을 실행합니다.
실습 환경 구성
MacOS 환경에서 실습하였습니다.
// 회사 정책으로 인해 docker-compose를 사용할 수 없어 EC2 AmazonLinux2 에서 실습을 하려 하였으나 kind 클러스터 생성 시 여러차례 에러가 발생하여 맥북에서 Docker Desktop 대체재인 colima 를 설치하였습니다.
colima도 아직까지 불안정해서 인지 컨테이너 내부 도커 소켓(/var/run/docker.sock) 설정 시 에러가 발생하는 것을 확인하였습니다. 포드맨으로는 시도는 해보지 않았지만 지쳐서 결국 ec2 위에 우분투 올려서 진행하였습니다.
작업 워크스페이스 생성
# 작업 디렉토리 생성 후 이동
mkdir cicd-labs
cd cicd-labs
※ docker, docker-compose 설치가 안되어있다면
# 1. 기존 docker 관련 패키지 제거 (선택적)
sudo apt remove docker docker-engine docker.io containerd runc
# 2. 필수 패키지 설치
sudo apt update
sudo apt install ca-certificates curl gnupg lsb-release -y
# 3. Docker GPG 키 추가
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 4. Docker 리포지토리 추가
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. Docker 설치
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
CI 구성
CI에 사용할 *젠킨스 와 gogs 도커컴포즈 manifest 를 작성합니다.
cat <<EOT > docker-compose.yaml
**services**:
**jenkins**:
container_name: jenkins
image: jenkins/**jenkins**
restart: unless-stopped
networks:
- cicd-network
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:
- cicd-network
ports:
- "10022:22"
- "**3000**:3000"
volumes:
- **gogs-data**:/data
**volumes**:
jenkins_home:
gogs-data:
**networks**:
cicd-network:
driver: bridge
EOT
💡
Mainfest 상세
- 컨테이너 서비스 (services)
- jenkins 컨테이너: Jenkins CI 서버를 띄움
- gogs 컨테이너: Gogs Git 서버를 띄움
- 네트워크 (networks)
- 두 컨테이너가 같은 네트워크(cicd-network)에서 서로 통신 가능하게 합니다.
- jenkins
- ports:
- "8080:8080"
- "50000:50000"
- ports:
- gogs
- ports:
- "10022:22"
- "3000:3000"
- ports:
- jenkins
- 볼륨 (volumes)
- jenkins_home: Jenkins 데이터를 저장
- gogs-data: Gogs 데이터를 저장
*젠킨스(Jenkins)는 오랜 역사를 가진 오픈소스 CI 도구로서, 유연한 플러그인 생태계와 강력한 자동화 기능 덕분에 업계에서 널리 사용되고 있습니다. 특히 다양한 기술 스택 및 환경과의 호환성이 뛰어나 복잡한 CI/CD 파이프라인 구축에 적합한 특징을 지닙니다. 다만 설정과 관리에 있어서 러닝커브가 있기 때문에 관련한 학습을 조금 필요로합니다.
*Gogs는 Go 언어로 작성된 오픈소스 Git 서버로, 매우 가볍고 빠르며 설치와 관리가 간편한 것이 특징입니다.
작성한 젠킨스를 배포합니다.
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

도커를 이용하여 각 컨테이너로 접속해봅니다.
docker compose exec jenkins bash
*exit*
docker compose exec gogs bash
*exit*
젠킨스 컨테이너 초기 설정
Jenkins 초기 암호 확인
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
http://본인서버IP:8080
Jenkins 웹에 접속하여 계정/암호 설정을 진행합니다. (계정 / 암호 입력 >> admin / qwe123)
- 로컬에서 테스트 중이라면 127.0.0.1 입력
- ec2 ip 찾기: curl ipconfig.io/ip

Install suggested plugin 버튼을 눌러 기본적인 플러그인을 설치하여 줍니다.


젠킨스 설정
Jenkins 컨테이너에서 호스트에 도커 데몬 사용 설정 (Docker-out-of-Docker)

- 일반적으로 컨테이너 내에서 Docker를 사용할 때는 호스트의 Docker 데몬을 직접 이용하는 방식을 씁니다.
- Docker 컨테이너 내부에서 /var/run/docker.sock을 마운트하는 방식이기 때문에, Docker out of Docker 구조로 동작합니다.
이는 위의 docker-compose.yaml 파일에서도 설정한 내용입니다.

Docker in Docker (DinD)와 차이점은?
- Docker in Docker(DinD): 컨테이너 내부에 독립적인 Docker 데몬을 실행
- Docker out of Docker(DoD): 호스트의 Docker 데몬을 컨테이너 내부에서 직접 호출해 사용하는 방식
Jenkins 컨테이너의 루트 사용자(root) 권한을 가지고 bash 셸로 접속합니다. 이는 권한 제한 없이 컨테이너 내부를 관리할 수 있음을 의미합니다.
**docker compose exec --privileged -u root jenkins bash**
id 명령어를 통해 사용자권한을 확인합니다. 명백한 최상위 권한 root 입니다.
젠킨스 컨테이너에서 도커를 설치합니다.
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
Root 권한으로 호스트 권한이 previleged 됐기 때문에 호스트의 컨테이너를 확인할 수 있습니다.

이제 젠킨스 컨테이너 내부에서 root가 아닌 jenkins 유저도 docker를 실행할 수 있도록 권한을 부여합니다. 이는 빌드를 위해 반드시 필요한 설정입니다.
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
docker 유저그룹을 만들고 /var/run/docker.sock 의 그룹권한 사용을 변경하여 jenkins유저도 사용할 수 있게끔 합니다.
젠킨스 컨테이너를 빠져나와서 젠킨스를 재시작 하여줍니다.
*jenkins item 실행 시 docker 명령 실행 권한 에러 발생 : 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

Gogs 설정
리포지토리 설정 Repo(Private) - dev-app , ops-deploy
1️⃣ Gogs Install
서버의 3000번 포트로 접속합니다.
Gogs 접속 초기 화면입니다. install 페이지로 리다이렉션 됩니다. 해당 페이지에서 초기 설정을 구성합니다.

다음 설정 외에 기본설정을 유지합니다.
- 데이터베이스 유형 : SQLite3
- 애플리케이션 URL :
http://<각자 자신의 PC IP>:3000/
- 기본 브랜치 : main
- 관리자 계정 설정 클릭 : 이름(계정명 - 닉네임 사용 devops), 비밀번호(계정암호 qwe123), 이메일 입력


2️⃣ 토큰 생성
로그인 후 → Your Settings → Applications : Generate New Token 클릭 - Token Name(devops) ⇒ Generate Token 클릭 : 메모해두기!

3️⃣ 저장소 생성

각각 용도 별로 리포지토리를 생성합니다

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 주소 확인
이제 생성한 리포지토리를 개발환경에서 연동하여줍니다.
*# (옵션) GIT 인증 정보 초기화
git credential-cache exit*
#
git config --list --show-origin
#
TOKEN=***<각자 Gogs Token>***
TOKEN=3e3882af4b7b732cc1f7a313bc98fa09173ef2bc
MyIP=***<각자 자신의 PC IP> # Windows (WSL2) 사용자는 자신의 WSL2 Ubuntu eth0 IP 입력 할 것!***
MyIP=192.168.254.127
git clone ***<각자 Gogs dev-app repo 주소>***
**git clone *http://*devops:$TOKEN@$MyIP*:3000/devops/dev-app.git***
*Cloning into 'dev-app'...
...*
혹은 localhost로 접근합니다.
저장소를 사용하기 위해 git config 설정을 해줍니다.
cd 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

간단한 웹 애플리케이션을 파이썬으로 작성합니다
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
웹서버 코드로, 루트(/) 요청 시 현재 시간과 호스트 이름을 반환하고, /healthz 요청 시 “Healthy” 상태를 응답합니다. 기본적으로 80번 포트에서 동작합니다.
※ 해당 코드는 파이썬 3.10 이상버전에서만 지원되기 때문에 업그레이드가 필요할 수도있습니다.
이제 해당 코드를 컨테이너로 말아주기 위해서 도커파일을 생성합니다.
cat > **Dockerfile** <<EOF
FROM python:3.12
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
CMD python3 server.py
EOF
버전을 명시하기위해서 버전 파일을 생성합니다.
echo "**0.0.1**" > VERSION
이제 저장소에 푸시하여 줍니다.
git status
git add .
git commit -m "Add dev-app"
**git push -u origin main**

Docker hub 설정
이미지 저장소로는 도커 허브를 사용할 것입니다. 자신의 도커 허브 계정에 Token 발급합니다.
1️⃣ dev-app (Private) repo 생성

2️⃣ Personal access token 생성
다음과 같은 과정으로 인증 토큰을 생성합니다.
- 계정 → Account settings
- Security → Personal access tokens
- Generate new token : 만료일(편한대로), Access permissions(Read, Write, Delete)
- 발급된 토큰 메모
배포환경 구성: kind
이제 실제 애플리케이션을 배포하기 위해 배포 환경 즉 인프라를 구성해줍니다.
kind 설치
1️⃣ kind 설치
# kind 바이너리 다운로드 및 설치
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
2️⃣ kind 설치 확인
kind version

3️⃣ kind 관련 툴 설치
아래 쉘스크립트를 실행하여 줍니다.
#!/bin/bash
# 현재 디렉토리 출력
cd "$PWD"
pwd
# 필수 패키지 설치
sudo apt update -y
sudo apt install -y \
bridge-utils \
net-tools \
jq \
tree \
unzip \
git \
bash-completion \
curl
# kubecolor 설치
sudo curl -Lo /usr/local/bin/kubecolor https://github.com/hidetatz/kubecolor/releases/latest/download/kubecolor_Linux_amd64
sudo chmod +x /usr/local/bin/kubecolor
# kubectx 및 kubens 설치
sudo git clone https://github.com/ahmetb/kubectx /opt/kubectx
sudo ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens
# Kind 설치
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind --version
# kubectl 설치
snap install kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/kubectl
kubectl version --client=true
# Helm 설치
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
helm version
# bash-completion 설정
echo 'source /etc/bash_completion' >> ~/.bashrc
kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl > /dev/null
echo 'source /etc/bash_completion.d/kubectl' >> ~/.bashrc
# kubectl alias 설정
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc
# kube-ps1 설치
git clone https://github.com/jonmosco/kube-ps1.git ~/.kube-ps1
echo 'source ~/.kube-ps1/kube-ps1.sh' >> ~/.bashrc
cat <<"EOT" >> ~/.bashrc
KUBE_PS1_SYMBOL_ENABLE=true
function get_cluster_short() {
echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT
# 적용
source ~/.bashrc
echo "✅ 설치가 완료되었습니다!"
설치 후 한번더 source ~/.bashrc 하거나 터미널 다시 켜기
K8S 클러스터 생성 (kind)
클러스터 배포 전 docker 프로세스를 확인합니다.
docker ps

클러스터 API서버에 각자 PC의 IP를 입력합니다.
MyIP=<각자 자신의 PC IP>
MyIP=192.168.254.127
k8s 클러스터를 생성합니다.
cat > kind-3node.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "$MyIP"
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
- containerPort: 30002
hostPort: 30002
- containerPort: 30003
hostPort: 30003
- role: worker
- role: worker
EOF
**kind create cluster --config kind-3node.yaml --name myk8s** --image kindest/node:v1.32.2
- 마스터 1 + 워커노드 2 구성.
- 1.32.2 버전인 클러스터를 생성합니다.

docker ps를 통해 도커프로세스를 살펴봅니다.

설치 확인
kind get clusters
kubectl cluster-info
**kubectl get node -o wide**
# 네임스페이스 확인 >> 도커 컨테이너에서 배운 네임스페이스와 다릅니다!
kubectl get namespaces
kube-ops-viewer 설치
실습에 유용한 kube-ops-viewer를 설치하여 쿠버네티스 배포를 모니터링합니다.
# 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://127.0.0.1:30001/#scale=1.5"
open "http://127.0.0.1:30001/#scale=2"

젠킨스 파이프라인 만들기
※ 참고: 젠킨스 개념 정리 ※
젠킨스(Jenkins)는 소프트웨어 개발에서 자동화된 작업(빌드, 테스트, 배포 등) 을 처리해주는 도구입니다.
그 중심에는 바로 작업(Job)이라는 개념이 있습니다.
젠킨스에서는 작업을 의미하는 용어로 Project, Item, Job이라는 표현을 번갈아 사용하지만, 실제로는 거의 같은 뜻으로 이해하면 됩니다. Jenkins에서 새로운 Job 하나 만들자” = “프로젝트 하나 등록하자” 라고 생각하시면 됩니다.
젠킨스 Job은 다음과 같이 구성되어있습니다.
- Trigger: 작업을 언제 시작할지 정함
- Build Step: 해야 할 작업을 단계별로 정의
- ex)
- 소스코드 가져오기
- 테스트 실행하기
- 빌드(컴파일) 하기
- 결과물(아티팩트) 생성하기
- ex)
- Post-build Action: 작업이 끝난 후 처리할 후속 동작 지정
플러그인 설치
실습 젠킨스에 사용할 플러그인을 설치합니다.
- Pipeline Stage View - Docs
- Docker Pipeline : building, testing, and using Docker images from Jenkins Pipeline - Docs
- Gogs : Webhook Plugin - Docs


자격증명 설정
깃헙, 도커허브, k8s 등 ci/cd 파이프라인의 각각의 영역에서 사용할 권한을 설정해주어야 합니다.
- Gogs Repo 자격증명 설정 : gogs-crd
- Kind : Username with password
- Username : devops
- Password : *<Gogs 토큰>*
- ID : gogs-crd
- 도커 허브 자격증명 설정 : dockerhub-crd
- Kind : Username with password
- Username : *<도커 계정명>*
- Password : *<도커 계정 암호 혹은 토큰>*
- ID : dockerhub-crd
- k8s(kind) 자격증명 설정 : k8s-crd
- Kind : Secret file
- File : *<kubeconfig 파일 업로드>*
- ID : k8s-crd
- ⇒* macOS 사용자 경우,
cp ~/.kube/config ./kube-config
복사 후 해당 파일 업로드 하자 - ⇒ Windows 사용자 경우, kubeconfig 파일은 메모장으로 직접 작성 후 업로드 하자*
관리 > credentials 페이지 이동

글로벌 도메인 클릭

위 세개의 credentials 을 유형에 맞게 저장합니다.
1️⃣Gogs Repo 자격증명 설정 : gogs-crd

2️⃣ 도커 허브 자격증명 설정 : dockerhub-crd

3️⃣ k8s(kind) 자격증명 설정 : k8s-crd
Secret file 유형으로 선택합니다.
~/.kube/config 에 있는 kubeconfig 를 별도 파일로 저장하여 업로드합니다.

등록된 credentials 을 확인합니다.

파이프라인(item) 생성
대시보드로 돌아와서 좌측의 새로운 item 버튼을 클릭합니다

item-name: pipeline-ci

Configuration 페이지에서 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."
}
}
}

save 합니다.
플러그인 테스트
도커허브 저장소에 접속합니다.
https://hub.docker.com/repository/docker//dev-app/general
이미지가 따로 없는 것을 확인합니다.

젠킨스 파이프라인을 실행하여 도커허브에 이미지가 업로드 되는지 확인합니다. 지금 빌드 버튼을 클릭합니다.

빌드 내역을 눌러 콘솔 아웃풋을 확인하면 빌드 로그를 볼 수 있습니다.


도커 허브에가서 확인해보면 이미지가 업로드 된것을 확인 가능합니다
배포 테스트
k8s 클러스터에서 도커허브로부터 이미지를 받아와 배포해보는 테스트를 해보겠습니다.
도커 허브 설정을 위해 환경변수를 등록합니다
# 디플로이먼트 오브젝트 배포 : 리플리카(파드 2개), 컨테이너 이미지 >> 아래 도커 계정 부분만 변경해서 배포해보자
DHUSER=<도커 허브 계정명>
아래 코드를 입력하여 파드를 배포합니다.
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
K8S 이벤트를 확인합니다.
**kubectl get events -w --sort-by '.lastTimestamp'**
다음과 같이 실패하는 것을 확인할 수 있습니다. 이는 도커허브 관련 인증설정을 따로 해주지 않았기 때문입니다.

🔐 도커 허브 자격증명(토큰) 생성하기
도커 허브 personal access token을 DHPASS 환경변수로 입력합니다
# k8s secret : 도커 자격증명 설정
DHUSER=gasida
DHPASS=dckr_pat_KWx-0N27iEd1lk8aNvRzzzzzzz
echo $DHUSER $DHPASS
k8s secret 리소스를 생성합니다.
kubectl create secret docker-registry dockerhub-secret \
--docker-server=https://index.docker.io/v1/ \
--docker-username=$DHUSER \
--docker-password=$DHPASS
# 시크릿 리소스 확인
kubectl get secret
base64 인코딩 확인
ubectl get secrets -o yaml | kubectl neat
이제 해당 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
도커허브 인증이 성공하여 파드가 정상적으로 올라오는 것을 확인할 수 있습니다.

파드가 업데이트 되면서 evict 및 create 되고 있습니다.
curl 명령어를 수행하기 위한 curl 파드를 만듭니다.
kubectl run curl-pod --image=curlimages/**curl:latest** --command -- sh -c "while true; do sleep 3600; done"
curl 대상 파드의 환경변수 지정
PODIP1=<timeserver-Y 파드 IP>
timeserver 파드df로 curl 명령어를 날려봅니다.
kubectl exec -it curl-pod -- curl $PODIP1
kubectl exec -it curl-pod -- curl $PODIP1/healthz | more

CI 자동화 해보기
gogs 웹훅 생성: Jenkins Job Trigger

CI/CD 파이프라인 중 위 빨강 박스의 과정을 자동화 해보겟습니다.
1️⃣ gogs 에 /data/gogs/conf/app.ini
파일 수정 후 컨테이너 재기동 - issue
gogs 컨테이너에 접근하여 해당 파일을 아래와 같이 수정합니다.
docker compose exec gogs bash
vi /data/gogs/conf/app.ini
[security]
INSTALL_LOCK = true
SECRET_KEY = j2xaUPQcbAEwpIu
LOCAL_NETWORK_ALLOWLIST = *192.168.254.127* # 각자 자신의 PC IP
변경 설정 적용을 위해 컨테이너를 재기동합니다.
**docker compose restart gogs**
2️⃣ gogs 에 Webhooks 설정
Jenkins job Trigger - Setting → Webhooks → Gogs 클릭
곡스 웹화면에 들어가면 컨테이너를 초기화했기 때문에 재로그인이 필요합니다. dev-app 레포에 들어와서 설정 메뉴버튼을 클릭합니다.

웹훅 메뉴를 눌러 웹후에 gogs를 선택합니다.

이후 다음과 같이 설정합니다.

- Payload URL :
http://***<자신의 집 IP>***:8080/gogs-webhook/?job=**SCM-Pipeline**/
- Content Type :
application/json
- Secret :
qwe123
- When should this webhook be triggered? : Just the push event
- Active : Check
젠킨스 파이프라인 생성

위에서 설정한 이름의 젠킨스 잡을 생성합니다.

다음과 같은 설정값을 지정합니다.
- GitHub project :
http://***<your IP>***:3000/***<Gogs 계정명>***/dev-app
← .git 은 제거- GitHub project : http://10.0.11.24: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)
- Repo URL(
- Script Path : Jenkinsfile
- SCM : Git
파이프라인 설정 시 이제 git을 통해서 파이프라인 스크립트를 관리하도록 설정할 것입니다.

이제 스크립트를 직접 깃 저장소에서 관리하도록 합니다.

Jenkinsfile 생성
깃 작업디렉토리로 이동하여 실제 jenkins 파일을 만들어보고 push 하도록 합니다.
cd dev-app
touch **Jenkinsfile**
# VERSION 파일 : 0.0.3 수정
vi VERSION
# server.py 파일 : 0.0.3 수정
vi server.py
Jenkinsfile
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."
}
}
}
Git 트리거 발생
변경 내용을 커밋하고 푸시합니다.
❗이때, 이전의 Gogs 웹훅 설정으로 인해 push 이벤트가 발생하면 젠킨스 빌드가 동작하는 것을 확인하도록 합니다.
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main
gogs 웹에 업데이트된 것을 확인

젠킨스 파이프라인이 실행된 것을 확인

신규 버전을 배포하여 docker hub에 이미지가 업로드(빌드)가 잘 되었는지 확인합니다.
# 신규 버전 적용
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"

CD 자동화 해보기 (Jenkins)
이제 배포까지 자동화하도록 해봅니다.
Jenkins 컨테이너 내부에 툴 설치 : kubectl(v1.32), helm
젠킨스 컨테이너 내부에 쿠버네티스 툴과 helm을 설치합니다.
**docker compose exec --privileged -u root jenkins bash**
--------------------------------------------
#curl -LO "https://dl.k8s.io/release/v1.32.2/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/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

Jenkins k8s 테스트
젠킨스에서 k8s 명령어가 수행이 가능한지 간단하게 테스트 해봅니다.
Jenkins Item 생성(Pipeline) : item name(k8s-cmd)
pipeline script 입력
pipeline {
agent any
environment {
KUBECONFIG = credentials('k8s-crd')
}
stages {
stage('List Pods') {
steps {
sh '''
# Fetch and display Pods
kubectl get pods -A --kubeconfig "$KUBECONFIG"
'''
}
}
}
}
- k8s-crd 에 구성된 credential을 참조하여 kubectl 명령어를 수행합니다.
파이프라인을 실행하면 다음과 같이 console output에 kubectl 명령어가 성공한 것을 확인할 수 있습니다.

K8S CD 실습 (blue-green)
1️⃣ 블루 그린 배포에 사용할 실습 리소스 manifest를 생성 합니다.
#
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
git push 를 통해 트리거를 발생시킵니다.
tree
git add . && git commit -m "Add echo server yaml" && git push -u origin main
push와 동시에 젠킨스파이프라인(SCM-Pipeline) 이 실행됩니다.

2️⃣ 배포 전 준비
기존 실습에 사용한 리소스를 삭제합니다.
kubectl delete deploy,svc timeserver
서비스 모니터링을 위한 반복 접속을 미리 실행 시켜놓습니다.
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
3️⃣ 새로운 젠킨스잡을 생성합니다. (k8s-bluegreen)
새로 적용할 파이프라인 스크립트를 작성합니다.
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"
}
}
}
}
}
}
4️⃣ 지금 빌드 클릭
젠킨스 잡을 생성한 뒤 빌드 하면 다음과 같이 진행이 됩니다.

터미널로 이동해 어떤 변화가 발생했는 지 확인합니다.

블루 버전까지 배포가된 걸 확인할 수 있습니다.
approve green version 스테이지에 마우스 커서를 갖다대면 다음과 같이 작은 팝업이 하나 나타납니다.

yes 버튼을 클릭합니다. green 파드가 새로 배포됩니다.



하지만 아직 서비스는 블루가 실행중입니다.
이는 아직 blue → green 으로 전환이 되지 않았기 때문입니다.
이제 다음 스테이지인 approve version switching 에 마우스 커스를 가져다대면 다음과 같이 스위칭을 할것인지에 대해 묻는 팝업이 나타납니다.

yes를 클릭하면 서비스가 스위칭 되어 curl 명령어 결과가 green 으로 나타나는 것을 볼 수있습니다.

젠킨스에서 새로운 스테이지(Blue Rollback)이 생겨난걸 확인 가능합니다. 이제 서비스가 예상한대로 배포가 되었다면 done을, 아니라면 rollback 을 선택합니다.

done을 선택하면 blue는 자동으로 내려가게 되고 파이프라인이 succeeded 됩니다.
단일 repo 방식의 한계
현재 dev-app 리포지토리는 개발 및 배포를 단일 repo에서 관리하는 구성을 띠고 있습니다. 이렇게 개발과 배포를 통합하여 사용하는 것에 있어서는 구성이 간소화되고 편리하다는 장점이 있지만 서비스 복잡도 및 인력이 커지면 비효율적이여질 수 있습니다.
PR 충돌 증가
- 여러 개발자/팀이 같은 repo에 PR을 보내다 보면 deployment.yaml이나 helm/ 같은 디렉토리를 건드릴 가능성도 커집니다. 결과적으로 충돌이 많아지고, 작업 영역이 섞이면서 협업이 느려집니다.
dev 와 ops 가 분리가 안됨
- 어떤 개발자는 “앱 코드”만 바꾸고 싶은데, 누군가는 “배포 방식”만 바꾸고 싶지만 같은 repo에 있다 보니 불필요한 코드리뷰 대상이 되고, PR도 복잡해집니다.
CD 자동화 해보기 (Argo CD)
Argo CD 란
Argo CD는 Kubernetes 배포를 자동으로 관리해주는 도구입니다. Git 저장소에 있는 설정 파일(YAML)을 기준으로 클러스터 상태를 유지합니다. 개발자가 Git에 코드만 올리면, Argo CD가 자동으로 K8s에 반영해줍니다.(=GitOps)
Argo CD 컴포넌트

- API Server : Web UI 대시보드, k8s api 처럼 API 서버 역할
- Repository Server : Git 연결 및 배포할 yaml 생성
- Application Controller : k8s 리소스 모니터링, Git과 비교
- Redis : k8s api와 git 요청을 줄이기 위한 캐싱
- Notification : 이벤트 알림, 트리거
- Dex : 외부 인증 관리
- ApplicationSet Controller : 멀티 클러스터를 위한 App 패키징 관리
Argo CD 설치
argocd 를 위한 네임스페이스를 생성합니다.
kubectl create ns **argocd**
cat <<EOF > argocd-values.yaml
dex:
enabled: false
server:
service:
type: NodePort
nodePortHttps: 30002
extraArgs:
- --insecure # HTTPS 대신 HTTP 사용
EOF
- 실습 편의를 위해 http 사용
- 호스트 30002 포트 사용
helm으로 설치
helm repo add argo https://argoproj.github.io/argo-helm
helm install **argocd** argo/argo-cd --version 7.8.13 -f argocd-values.yaml --namespace argocd # 7.7.10
argocd 설치 확인
kubectl get pod,svc,ep,secret,cm -n argocd
**kubectl get crd | grep argo**
argocd 서버의 최초 접속 암호 확인. 메모해둡니다.
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d ;echo
서버의 30002 포트로 접근하여 접속합니다.

userinfo에서 비밀번호를 변경하여줍니다.

Argo CD repo 등록 (ops-deploy)
Settings → Repositories → CONNECT REPO 클릭


다음 설정 적용하여 리포지토리를 연결합니다.

- connection method: VIA HTTPS
- Type : git
- Project : default
- Repo URL :
http://***<자신의 집 IP>***:3000/devops/ops-deploy
http://10.0.11.24:3000/devops/ops-deploy - Username : devops
- Password : *<Gogs 토큰>*
자격증명이 성공적으로 통과되면 다음과 같이 상태가 successful인것을 확인할 수 있습니다.

ops-deploy 에 배포하기
Repo(ops-deploy) 에 nginx helm chart 를 Argo CD를 통한 배포 - 1
1️⃣ gogs repo 연동
gogs 의 ops-deploy 리포지토리를 클론합니다.
git clone http://devops:$TOKEN@$MyIP:3000/devops/**ops-deploy.git**
cd ops-deploy
git config 설정을 맞춰줍니다.
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 원격 리포 연동 확인
git --no-pager branch
git remote -v
2️⃣ helm 차트 생성
이제 nginx 헬름 차트를 생성할 디렉토리와 환경변수 설정을 해줍니다.
VERSION=1.26.1
mkdir nginx-chart
mkdir nginx-chart/**templates**
cat > nginx-chart/**VERSION** <<EOF
$VERSION
EOF
helm 차트 작성
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:
- values.yaml이나 환경별 오버라이드 파일을 사용해서 DEV/PRD 환경에 따라 다른 설정으로 NGINX를 배포합니다.
- 환경마다 replicaCount가 다르게 구성 (DEV=1, PRD=2)
helm 차트 구성을 확인합니다.
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
helm template 명령어를 사용하여 values가 적용된 모습을 확인합니다.
helm template dev-nginx nginx-chart -f nginx-chart/values-dev.yaml
helm template prd-nginx nginx-chart -f nginx-chart/values-prd.yaml
이어지는 내용은 다음 포스팅에서 소개하겠습니다.
'AWS > EKS' 카테고리의 다른 글
EKS Upgrades (0) | 2025.04.06 |
---|---|
K8S 환경에서 CI/CD 구축하기 - 2 (0) | 2025.03.30 |
EKS Auto Mode (0) | 2025.03.23 |
EKS Security - OPA 로 정책 적용하기 (0) | 2025.03.16 |
EKS Security - ECR Enhanced Scanning (0) | 2025.03.16 |