[AWS EKS] K8S CI/CD - Jenkins, ArgoCD

주영·2025년 3월 29일
0

AWS EKS Workshop Study 3기

목록 보기
27/31

이 글은 CloudNet@팀의 AWS EKS Workshop Study(AEWS) 3기 스터디 내용을 바탕으로 작성되었습니다.
AEWS는 CloudNet@의 '가시다'님께서 진행하는 스터디로, EKS를 학습하는 과정입니다.
EKS를 깊이 있게 이해할 기회를 주시고, 소중한 지식을 나눠주시는 가시다님께 다시 한번 감사드립니다.
이 글이 EKS를 학습하는 분들께 도움이 되길 바랍니다.

1. 실습 환경 개요

  • CI 서버(Jenkins)에서 컨테이너 이미지를 빌드하여 이미지 저장소에 푸시합니다.
  • CD 서버(Argo CD)는 해당 이미지를 참조하여 K8S(kind) 클러스터에 배포합니다.
  • 전체 구성은 로컬 PC 상에서 모두 운영되며, 학습 및 테스트 목적의 경량 클러스터인 kind를 활용합니다.

2. WSL2 설치 및 Ubuntu 배포판 구성

[AWS EKS] Windows에서 EKS 클러스터 배포 및 Network 실습 환경 구성 - 2. Windows 환경 설정 (WSL2 + Ubuntu 24.04)

3. WSL2에 Docker 설치

# WSL2 에 Docker 설치 : 아래 스크립트 실행 후 20초 대기하면 스크립트 실행 됨
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh
...

# 설치 확인
docker info
docker ps
sudo systemctl status docker
cat /etc/group | grep docker

4. kind 및 관련 툴 설치

4.1 필수 패키지 설치

# 기본 사용자 디렉터리 이동
cd $PWD
pwd

#
sudo systemctl stop apparmor && sudo systemctl disable apparmor

# 
sudo apt update && sudo apt-get install bridge-utils net-tools jq tree unzip kubectx kubecolor -y

4.2 kind 설치

# Install 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

4.3 kubectl 설치

# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv ./kubectl /usr/bin
sudo kubectl version --client=true

4.4 Helm 설치

# Install Helm
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
helm version

5. Shell 환경 설정

# 자동 완성
# Source the completion
source <(kubectl completion bash)
echo 'source <(kubectl completion bash)' >> ~/.bashrc

# Alias kubectl to k
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc

# Install Kubeps & Setting PS1
git clone https://github.com/jonmosco/kube-ps1.git
echo -e "source $PWD/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

# .bashrc 적용을 위해서 logout 후 터미널 다시 접속 하자
exit

6. 실습 환경 구성

  • 컨테이너 2대 (Jenkins, Gogs)를 실행합니다.
  • 컨테이너는 WSL2 환경의 Docker에서 동작하며, 호스트 OS 포트 노출(port expose) 을 통해 웹 브라우저로 접근할 수 있습니다.
  • 컨테이너는 kind 네트워크에 연결되므로, 사전에 kind 설치를 완료해야 합니다.

6.1 Jenkins 및 Gogs 컨테이너 생성

⚠️ Jenkins와 Gogs를 배포하기 전, 반드시 kind를 먼저 설치하여 kind 네트워크가 생성되어 있어야 합니다.

# 작업 디렉토리 생성 후 이동
mkdir cicd-labs
cd cicd-labs

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

# docker network 확인 : kind 를 사용
docker network ls
# 출력 예시
...
7e8925d46acb   kind      bridge    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
  • Jenkins는 8080, Gogs는 3000 포트를 통해 접속 가능하게 설정되어 있습니다.
  • kind 네트워크를 external 네트워크로 명시했기 때문에, 해당 네트워크가 반드시 존재해야 합니다.

# 컨테이너 배포 및 상태 확인
docker compose up -d
docker compose ps
docker inspect kind

# 기본 정보 확인 및 컨테이너 진입 테스트
for i in gogs jenkins ; do
  echo ">> container : $i <<"
  docker compose exec $i sh -c "whoami && pwd"
  echo
done

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

docker compose exec gogs bash
exit

6.2 Jenkins 컨테이너 초기 설정

# Jenkins 초기 비밀번호 확인
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# 출력 예시
09a21116f3ce4f27a0ede79372febfb1

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

# (참고) 로그 확인 : 플러그인 설치 과정 확인
docker compose logs jenkins -f

6.3 Jenkins URL 설정

  • Jenkins에서 외부 접속 URL을 설정할 때, WSL2 Ubuntu 환경의 IP 주소를 활용합니다.
  • 해당 IP는 eth0 인터페이스의 IP를 사용합니다.

WSL2 IP 확인

ifconfig eth0

위에서 inet 항목의 IP 주소(ex. 172.23.129.5)를 사용하여 Jenkins URL을 다음과 같이 설정합니다: http://172.23.129.5:8080/

이 설정을 통해 다른 장치에서 동일 네트워크 상의 Jenkins에 접근할 수 있습니다.

6.4 Jenkins 컨테이너에서 Docker 데몬 사용 설정 (Docker-out-of-Docker)

6.4.1 Docker-out-of-Docker (DooD) 방식

https://daniel00324.tistory.com/17

  • 기본적으로 컨테이너 내부에서는 독립된 환경으로 동작하므로 호스트의 Docker 엔진에 접근할 수 없습니다.
  • Jenkins 파이프라인에서 docker build, docker push 등 Docker CLI를 사용하려면 Docker CLI 도구 설치 + 호스트 Docker 소켓 공유 + 권한 부여 작업이 필요합니다.
  • 보통 Jenkins와 Docker를 연동할 때 사용됩니다.
# Jenkins 컨테이너 내부에 도커 실행 파일 설치
docker compose exec --privileged -u root jenkins bash
-----------------------------------------------------
# 현재 사용자 정보 확인
id

# Docker 공식 GPG 키 등록
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

# Docker 저장소 설정
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
  
# Docker CLI 및 유틸리티 설치
apt-get update && apt install docker-ce-cli curl tree jq yq -y

# 설치 확인
docker info
docker ps
which docker # /usr/bin/docker 등 위치 확인

# 호스트의 Docker 소켓(/var/run/docker.sock)은 root 또는 docker 그룹에 속한 사용자만 접근 가능
# Jenkins 컨테이너 내부에서 root가 아닌 jenkins 유저도 docker를 실행할 수 있도록 권한을 부여
# docker 그룹 생성 및 GID 지정
groupadd -g 1001 -f docker  # Windows WSL2(Container) >> cat /etc/group 에서 docker 그룹ID를 지정 (cat /etc/group | grep docker 명령어를 통해 실제 GID를 확인 후 호스트의 도커 그룹 ID와 동일하게 적용)

# 도커 소켓의 그룹을 docker로 설정
chgrp docker /var/run/docker.sock
# 권한 확인
ls -l /var/run/docker.sock
# jenkins 사용자에게 docker 그룹 권한 추가
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 포함 확인
docker compose exec jenkins docker info   # Docker 데몬 정보 출력
docker compose exec jenkins docker ps     # 현재 컨테이너 목록 출력

=> Windows WSL2(Container) 상의 docker group ID인 989로 설정
=> 989는 WSL2 호스트 (Ubuntu) 시스템에서 도커 설치 시 자동으로 생성된 GID

6.5 Gogs 컨테이너 초기 설정 및 개인 Git 저장소 생성

6.5.1 Gogs란?

  • Gogs는 경량화된 오픈소스 셀프 호스팅 Git 서비스입니다.
  • 설치 및 운영이 간편하며, 개인 혹은 소규모 팀의 Git 저장소 관리에 적합합니다.

📌 참고 자료:

6.5.2 Gogs 컨테이너 초기 설정

1. 웹 UI 접속

WSL2 환경에서 기동한 Gogs 컨테이너는 아래 주소를 통해 웹 브라우저에서 접근 가능합니다.

http://127.0.0.1:3000/install

위 주소는 Windows 브라우저에서 접근하며, 설치 마법사가 자동으로 실행됩니다.

2. 초기 설정 값 입력

Gogs 설치 마법사에서 다음 값을 설정합니다.

  • 데이터베이스 유형: SQLite3
    • 간편한 로컬 환경 구성을 위한 내장형 DB
  • 애플리케이션 URL:
    • http://<각자 자신의 WSL2 Ubuntu eth0 IP>:3000/
    • ifconfig eth0 명령어로 WSL2 IP를 확인한 뒤, 해당 IP를 입력합니다.
  • 기본 브랜치: main
  • 관리자 계정 설정
    • 이름: devops (닉네임 사용)
    • 비밀번호: qwe123
    • 이메일: 자유롭게 입력

모든 항목 입력 후, 페이지 하단의 [Gogs 설치하기] 버튼을 클릭합니다.

3. 관리자 계정 로그인

6.5.3 Personal Access Token 생성

CI/CD에서 Gogs 저장소에 접근할 때 인증 수단으로 사용할 Access Token을 생성합니다.

생성 절차:

  1. 우측 상단 프로필 아이콘 클릭 → Your Settings 메뉴 진입
  2. 좌측 메뉴에서 Applications 선택
  3. Generate New Token 클릭
  4. Token Name: devops 입력
  5. Generate Token 버튼 클릭

➡ 생성된 토큰은 한 번만 표시되므로 반드시 메모해두어야 합니다.
이 토큰은 Jenkins 등 외부 시스템에서 Gogs 인증을 대신해 사용하는 비밀번호 대체용으로 활용됩니다.

6.5.4 Git 저장소 생성

CI/CD 실습을 위한 두 개의 Private 저장소를 생성합니다.

Repository 1: dev-app (개발팀용)

  • Repository Name: dev-app
  • Visibility: (✔️) This repository is Private
  • .gitignore 템플릿: Python
  • Readme: 기본값(Default) 선택 → (✔️) initialize this repository with selected files and template

완료 후, [Create Repository] 클릭 → 저장소 생성 및 주소 확인

Repository 2: ops-deploy (데브옵스팀용)

  • Repository Name: ops-deploy
  • Visibility: (✔️) This repository is Private
  • .gitignore 템플릿: Python
  • Readme: 기본값(Default) 선택 → (✔️) initialize this repository with selected files and template

완료 후, [Create Repository] 클릭 → 저장소 생성 및 주소 확인

6.6 Gogs 실습을 위한 저장소 설정

  • Gogs에서 생성한 Private 저장소(예: dev-app)를 로컬 환경에서 직접 Git으로 관리합니다.
  • 애플리케이션 소스코드와 Dockerfile을 작성하여 Gogs 원격 저장소에 Push합니다.

📍 Git 작업 위치

  • Windows 사용자 기준으로, 작업은 WSL2 Ubuntu 환경 또는 Windows 자체 환경에서 모두 가능합니다.
  • 단, IP 설정은 WSL2의 eth0 인터페이스 주소를 기준으로 해야 정상 작동합니다.
# (옵션) GIT 인증 정보 초기화
git credential-cache exit

#
git config --list --show-origin

# 환경 변수 설정
# Gogs Access Token 입력
TOKEN=<각자 Gogs Token>
TOKEN=3e3882af4b7b732cc1f7a313bc98fa09173ef2bc

MyIP=<각자 자신의 PC IP> # Windows (WSL2) 사용자는 자신의 WSL2 Ubuntu eth0 IP 입력 할 것!
MyIP=192.168.254.127

# 저장소 Clone
git clone <각자 Gogs dev-app repo 주소>
git clone http://devops:$TOKEN@$MyIP:3000/devops/dev-app.git
# 출력 예시
Cloning into 'dev-app'...
...

# 
cd dev-app

# 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   # 이후 인증 정보를 저장하여 입력 없이 Push 가능
git --no-pager config --local --list
cat .git/config

# 저장소 상태 확인
git --no-pager branch
git remote -v

# server.py 애플리케이션 파일 작성
# / 요청 시 현재 시간과 서버 호스트명을 출력
# /healthz 경로는 헬스 체크용으로 Healthy 응답 반환
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
# 실행 종료는 CTRL+C

# 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

# Git 커밋 및 원격 저장소에 푸시
tree   # 파일 구조 확인 (옵션)
git status   # 변경 파일 확인
git add .    # 전체 파일 스테이징
git commit -m "Add dev-app"   # 커밋
git push -u origin main       # 원격 저장소에 푸시
...

결과 확인

  • Gogs 웹 UI에 접속하여 dev-app 저장소의 파일 업로드 상태를 확인합니다.
  • 코드(server.py), Dockerfile, 버전(VERSION) 파일이 정상적으로 반영되어야 합니다.

6.7 Docker Hub에서 Private Repository 생성 및 Token 발급

6.7.1 Private Repository 생성

1. Docker Hub 로그인

웹 브라우저에서 https://hub.docker.com 접속 후, 본인의 Docker Hub 계정으로 로그인합니다.

2. 새로운 저장소 생성

  1. 메뉴에서 Repositories 선택 → Create repository 클릭
  2. 아래와 같이 입력합니다:
    • Repository Name: dev-app
    • Visibility: Private 선택
  3. Create 버튼 클릭하여 저장소를 생성합니다.

Private 저장소로 설정하면 도커 이미지가 외부에 노출되지 않습니다.

6.7.2 Personal Access Token 발급

Docker Hub는 보안 강화를 위해 일반 비밀번호 대신 Personal Access Token을 권장합니다. 해당 토큰은 docker login 명령어 또는 자동화된 배포 파이프라인에서 인증 수단으로 사용됩니다.

1. Account Settings 접근

우측 상단 프로필 아이콘 클릭 → Account Settings 메뉴 진입

2. Token 생성

  1. 좌측 메뉴에서 Personal Access Tokens 선택
  2. Generate new token 버큰 클릭
  3. 아래 항목 입력:
    • Token Name: 예) k8s test
    • Expiration: 30일, 60일, Custom 등 원하는 기간 선택
    • Access permissions: Read, Write, Delete 모두 활성화
  4. Generate 클릭

생성된 토큰은 한 번만 표시되므로 반드시 메모해둬야 합니다.

7. kind로 K8S 클러스터 배포

Windows (WSL2) 환경에서 kind를 이용해 Kubernetes 클러스터를 배포하는 과정을 정리한 내용입니다.

7.1 kind 클러스터 구성 및 배포

# 클러스터 배포 전 확인
docker ps

# WSL2 Ubuntu eth0 IP를 지정
ifconfig eth0

MyIP=<각자 자신의 WSL2 Ubuntu eth0 IP>
MyIP=172.19.21.65

# cicd-labs 디렉터리에서 kind 클러스터 설정 파일 작성
cd ..
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개의 워커 노드로 구성됩니다.
  • NodePort 접근을 위한 포트 매핑도 사전에 설정합니다.

7.2 클러스터 상태 확인

# 클러스터 상태 확인
kind get nodes --name myk8s
kubens default   # 현재 네임스페이스 확인 및 설정

# Docker 네트워크 및 컨테이너 정보
# kind 는 별도 도커 네트워크 생성 후 사용 : 기본값 172.18.0.0/16
docker network ls
docker inspect kind | jq
  • kind는 자체 bridge 네트워크를 생성하여 Kubernetes 클러스터 내부 통신을 처리합니다.
  • 기본 네트워크 대역: 172.18.0.0/16

# k8s api 주소 확인 : 어떻게 로컬에서 접속이 되는 걸까?
kubectl cluster-info

# 노드 정보 확인 : CRI 는 containerd 사용
kubectl get node -o wide

# 파드 정보 확인 : CNI 는 kindnet 사용
kubectl get pod -A -o wide

# 네임스페이스 확인 >> 도커 컨테이너에서 배운 네임스페이스와 다릅니다!
kubectl get namespaces

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

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

# kube config 파일 확인 : "server: https://172.19.21.65:35413" 부분에 접속 주소 잘 확인해두자!
cat ~/.kube/config
ls -l ~/.kube/config

7.3 kube-ops-view 설치 및 접속

kube-ops-view는 쿠버네티스 클러스터 자원(노드, 파드 등)의 상태를 시각적으로 확인할 수 있는 오픈소스 대시보드입니다.

# kube-ops-view
# helm show values geek-cookbook/kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30001 --set env.TZ="Asia/Seoul" --namespace kube-system

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

# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : 아래 접속 주소를 자신의 웹 브라우저에서 접속할 것!
echo "http://127.0.0.1:30001/#scale=1.5"
echo "http://127.0.0.1:30001/#scale=2"

8. Jenkins 설정 : Plugin 설치 및 자격증명 등록

다음은 Jenkins 설정을 위한 플러그인 설치 및 자격증명 등록 절차를 정리한 설명입니다. Jenkins를 활용한 CI/CD 파이프라인 구성에 필수적인 단계로, 실습 환경에서는 Gogs와 Docker Hub, Kubernetes(kind)와의 연동을 위해 각각의 인증 정보를 등록하게 됩니다.

8.1 Jenkins Plugin 설치

8.1.1 필수 플러그인 목록

플러그인명용도설명
Pipeline Stage View파이프라인 시각화파이프라인 실행 결과를 그래픽으로 보여주는 UI 제공 🔗 Docs
Docker PipelineDocker 사용Jenkins Pipeline에서 Docker 빌드/테스트/사용을 가능하게 해주는 플러그인 🔗 Docs
GogsGit Webhook 연동Gogs 저장소에서 Jenkins Job 트리거 가능하도록 하는 Webhook 수신 플러그인 🔗 Docs
  • Webhook 예시 URL (Gogs에서 설정할 Webhook 주소):
    http(s)://<< jenkins-server >>/gogs-webhook/?job=<<jobname>>

8.1.2 설치 방법

  • Jenkins 웹 UI에서 [Jenkins 관리] → [Plugin] 메뉴로 이동합니다.
  • [Available plugins] 탭에서 플러그인 이름을 검색하여 설치합니다.





8.2 Jenkins 자격증명 설정

Jenkins에서는 외부 시스템과의 연동을 위해 Credentials(자격증명) 을 미리 등록해야 합니다. 등록된 자격증명은 파이프라인 내부에서 ID를 통해 참조할 수 있습니다.

8.2.1 접근 경로

Jenkins 웹 UI 상단 메뉴 → [Jenkins 관리] → [Credentials] → [Global] (전역 범위) → [Add Credentials]

8.2.2 등록할 자격증명 목록

① Gogs Repository 자격증명 - gogs-crd

항목
KindUsername with password
Usernamedevops
PasswordGogs에서 발급한 Access Token
IDgogs-crd
  • Gogs 저장소에 Git 접근을 위한 자격증명입니다.
  • 파이프라인에서 Git checkout 등 작업 시 사용됩니다.

② Docker Hub 자격증명 - dockerhub-crd

항목
KindUsername with password
UsernameDocker Hub 계정명
PasswordDocker Hub 비밀번호 또는 Personal Access Token
IDdockerhub-crd
  • Jenkins에서 Docker 이미지를 Docker Hub에 Push 또는 Pull하기 위해 필요한 인증 정보입니다.
  • 보안을 위해 Access Token 사용을 권장합니다.

③ Kubernetes(kind) 자격증명 - k8s-crd

항목
KindSecret file
Filekubeconfig 파일 (직접 업로드)
IDk8s-crd
  • Kubernetes 클러스터에 배포(예: kubectl apply, helm install) 하기 위한 인증 정보입니다.
  • kubeconfig 파일은 로컬 클러스터(kind)의 API 서버 접속 정보를 담고 있습니다.

💡 파일 준비 방법 (Windows 사용자)

  1. WSL2 Ubuntu에서 cat ~/.kube/config 명령어로 kubeconfig 내용을 복사합니다.
  2. 메모장 등으로 열어 내용을 붙여 넣고 kube-config 파일로 저장한 뒤 Jenkins에 업로드합니다.

이 자격증명들은 Jenkins Pipeline 내에서 다음과 같이 참조될 수 있습니다:

withCredentials([usernamePassword(credentialsId: 'gogs-crd', ...)])
withCredentials([file(credentialsId: 'k8s-crd', ...)])

9. Jenkins Pipeline Item 생성 및 Pipeline 구성

다음은 Jenkins에서 Pipeline Item을 생성하고 Docker 이미지를 빌드 및 푸시하는 파이프라인을 구성하는 과정을 정리한 설명입니다. 본 파이프라인은 Gogs에 저장된 애플리케이션 소스를 Git으로 가져와 Docker 이미지를 빌드하고, Docker Hub에 푸시하는 과정까지 자동화하는 데 목적이 있습니다.

9.1 Jenkins Pipeline Item 생성

  1. Jenkins 웹 UI에서 [새로운 Item] 클릭
  2. Item name: pipeline-ci 입력
  3. Item type 선택: Pipeline
  4. 하단의 [OK] 클릭

9.2 Pipeline Script 작성

파이프라인 스크립트에서 다음 항목들은 사용자 환경에 맞게 반드시 수정해야 합니다.

  • DOCKER_IMAGE: 본인의 Docker Hub 계정명을 반영
    예시: gasida/dev-app
  • Git 저장소 URL: Gogs에 등록된 저장소 주소
    예시: http://192.168.254.127:3000/devops/dev-app.git
  • Credentials ID: Jenkins에 등록한 자격증명 ID
    • Gogs: gogs-crd
    • DockerHub: dockerhub-crd

전체 스크립트 예시

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

9.3 파이프라인 실행 및 결과 확인

9.3.1 빌드 실행

  • Jenkins에서 pipeline-ci Job 진입 → [지금 빌드] 버튼 클릭

9.3.2 콘솔 출력 확인

  • 실행 중인 빌드 번호 클릭 → [콘솔 출력(Console Output)] 클릭
  • 다음 항목이 출력되어야 정상 작동입니다:
    • Git 저장소에서 코드 체크아웃 성공
    • VERSION 파일 읽기 성공 (Version found: 0.0.1 등)
    • Docker Hub 푸시 성공 (latest: digest: sha256:...)

9.3.3 Docker Hub 확인

  • Docker Hub 웹사이트 접속 후 dev-app 저장소 확인
  • 0.0.1, latest 등의 태그가 정상 등록되어 있어야 합니다

10. Jenkins와 K8S를 연계하여 애플리케이션 배포

10.1 애플리케이션 배포

timeserver 애플리케이션을 2개의 파드로 배포합니다.

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

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: timeserver
spec:
  replicas: 2
  selector:
    matchLabels:
      pod: timeserver-pod
  template:
    metadata:
      labels:
        pod: timeserver-pod
    spec:
      containers:
      - name: timeserver-container
        image: docker.io/$DHUSER/dev-app:0.0.1
        livenessProbe:
          initialDelaySeconds: 30
          periodSeconds: 30
          httpGet:
            path: /healthz
            port: 80
            scheme: HTTP
          timeoutSeconds: 5
          failureThreshold: 3
          successThreshold: 1
EOF

watch -d kubectl get deploy,rs,pod -o wide

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

10.2 이미지 Pull 오류(ErrImagePull / ImagePullBackOff) 해결

10.2.1 오류 원인

  • 보통 컨테이너 이미지 정보를 잘못 기입하는 경우
  • Docker Hub에 이미지가 존재하지 않은 경우
  • Private 이미지 접근을 위한 인증 정보가 부족한 경우

10.2.2 Secret 생성 및 Deployment에 적용으로 해결

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

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

DHUSER=gasida
DHPASS=dckr_pat_KWx-0N27iEd1lk8aNvRz8pDrQlI
echo $DHUSER $DHPASS

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

# 확인
kubectl get secret
kubectl describe secret
kubectl get secrets -o yaml | kubectl neat  # base64 인코딩 확인

SECRET=eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJnYXNpZGEiLCJwYXNzd29yZCI6ImRja3JfcGF0X0tXeC0wTjI3aUVkMWxrOGFOdlJ6OHBEclFsSSIsImF1dGgiOiJaMkZ6YVdSaE9tUmphM0pmY0dGMFgwdFhlQzB3VGpJM2FVVmtNV3hyT0dGT2RsSjZPSEJFY2xGc1NRPT0ifX19
echo "$SECRET" | base64 -d ; echo

# 디플로이먼트 오브젝트 업데이트 : 시크릿 적용 >> 아래 도커 계정 부분만 변경해서 배포해보자
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: timeserver
spec:
  replicas: 2
  selector:
    matchLabels:
      pod: timeserver-pod
  template:
    metadata:
      labels:
        pod: timeserver-pod
    spec:
      containers:
      - name: timeserver-container
        image: docker.io/$DHUSER/dev-app:0.0.1
        livenessProbe:
          initialDelaySeconds: 30
          periodSeconds: 30
          httpGet:
            path: /healthz
            port: 80
            scheme: HTTP
          timeoutSeconds: 5
          failureThreshold: 3
          successThreshold: 1
      imagePullSecrets:
      - name: dockerhub-secret
EOF

watch -d kubectl get deploy,rs,pod -o wide

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

# 접속을 위한 curl 파드 생성
kubectl run curl-pod --image=curlimages/curl:latest --command -- sh -c "while true; do sleep 3600; done"
kubectl get pod -owide

# timeserver 파드 IP 1개 확인 후 접속 확인
PODIP1=<timeserver-Y 파드 IP>
PODIP1=10.244.1.3

kubectl exec -it curl-pod -- curl $PODIP1
kubectl exec -it curl-pod -- curl $PODIP1/healthz

# 로그 확인
kubectl logs deploy/timeserver
kubectl logs deploy/timeserver -f
kubectl stern deploy/timeserver
kubectl stern -l pod=timeserver-pod

10.3 Service 생성 및 Scaling

# 서비스 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: timeserver
spec:
  selector:
    pod: timeserver-pod
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    nodePort: 30000
  type: NodePort
EOF

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

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

# 접속 테스트 - 클러스터 내부
# Service(ClusterIP)로 접속 확인 : 도메인네임, ClusterIP
kubectl exec -it curl-pod -- curl timeserver
kubectl exec -it curl-pod -- curl timeserver/healthz
kubectl exec -it curl-pod -- curl $(kubectl get svc timeserver -o jsonpath={.spec.clusterIP})

# 접속 테스트 - 클러스터 외부
# Service(NodePort)로 접속 확인 "노드IP:NodePort"
curl http://127.0.0.1:30000
curl http://127.0.0.1:30000
curl http://127.0.0.1:30000/healthz

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

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

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

10.4 애플리케이션 업데이트

10.4.1 코드 및 VERSION 수정

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

10.4.2 Jenkins에서 빌드 실행

Docker Hub에 새로운 태그로 이미지가 Push 됩니다.

10.4.3 Rolling Update 실행

# 파드 복제복 증가
kubectl scale deployment timeserver --replicas 4
kubectl get service,ep timeserver -owide

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

#
kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:0.0.Y && watch -d "kubectl get deploy,ep timeserver; echo; kubectl get rs,pod"
kubectl set image deployment timeserver timeserver-container=$DHUSER/dev-app:0.0.2 && watch -d "kubectl get deploy,ep timeserver; 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

11. Gogs Webhooks를 설정하여 Jenkins Pipeline을 자동으로 트리거하는 방법

다음은 Gogs Webhooks를 설정하여 Jenkins Pipeline을 자동으로 트리거하는 방법을 정리한 설명입니다. 이 설정을 통해 Gogs 저장소에 변경 사항(Push) 이 발생하면 Jenkins Job이 자동으로 실행되도록 연동할 수 있습니다.

11.1 Gogs에서 Webhooks가 동작하도록 네트워크 설정 변경

기본적으로 Gogs는 외부 네트워크 접근 제어를 통해 Webhook 요청을 제한할 수 있습니다. 따라서, Jenkins가 설치된 호스트(예: WSL2 Ubuntu IP)가 Webhook 허용 대상임을 명시해야 합니다.

11.1.1 Gogs 설정 파일 수정

VSCode에서 Docker Extension을 설치합니다.

컨테이너 내부의 설정 파일 /data/gogs/conf/app.ini 파일에서 다음 항목을 수정합니다.

[security]
INSTALL_LOCK = true
SECRET_KEY   = j2xaUPQcbAEwpIu
LOCAL_NETWORK_ALLOWLIST = 192.168.254.127
  • LOCAL_NETWORK_ALLOWLIST 항목에 자신의 WSL2 Ubuntu IP 주소를 입력합니다.
    • ifconfig eth0 명령어로 확인 가능
  • 이 설정은 Webhook 요청을 수락할 IP 대역을 지정하는 역할을 합니다.

11.1.2 Gogs 컨테이너 재시작

설정 변경 후, 변경 사항을 적용하기 위해 컨테이너를 재시작합니다.

docker compose restart gogs

11.2 Gogs 저장소에 Webhook 등록

이제 특정 저장소에서 Jenkins Pipeline Job을 트리거할 수 있도록 Webhook을 설정합니다.

11.2.1 Gogs 저장소 설정 메뉴 이동

Gogs 저장소 페이지에서 [Settings] → [Webhooks] → [Gogs] 클릭

11.2.2 Webhook 구성 값

항목설정값
Payload URLhttp://<자신의 집 IP>:8080/gogs-webhook/?job=SCM-Pipeline/
Content Typeapplication/json
Secretqwe123
Trigger EventJust the push event
Active체크(On)
  • Payload URL - Windows (WSL2) 사용자는 자신의 WSL2 Ubuntu eth0 IP
  • Jenkins에서 위 주소의 Webhook을 수신하려면, 사전에 Gogs Webhook 플러그인이 설치되어 있어야 하며, Jenkins Job 이름은 정확히 일치해야 합니다.

11.3 Jenkins Pipeline Job 생성 (SCM-Pipeline)

Webhooks를 통해 Jenkins에서 자동으로 빌드가 트리거되기 위해서는 Gogs 저장소와 연동된 Jenkins Pipeline Job이 먼저 구성되어 있어야 합니다.

11.3.1 Jenkins에서 Item 생성

by ChatGPT

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

11.4 애플리케이션 버전 수정 및 Jenkinsfile 작성

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

# Jenkinsfile 빈 파일 작성
touch 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 저장소에 Push

git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main

Gogs 저장소에 코드 변경 및 Jenkinsfile을 Push하면, Webhook을 통해 Jenkins Job이 자동으로 트리거됩니다.

11.5 연동 결과 확인

Gogs Webhook 기록 확인

정상 작동 시 HTTP 200 OK 상태로 표시됩니다.

Jenkins 빌드 트리거 확인

Docker Hub 저장소 확인

11.6 Kubernetes에 신규 버전 반영

이미 배포된 Deployment 리소스를 업데이트하여 새로운 버전의 이미지를 사용하도록 지정합니다.

# 신규 버전 적용
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"


12. Jenkins를 활용한 K8s(Kind) 기반 CI/CD 및 Blue-Green 배포 자동화 실습

12.1 Jenkins 컨테이너 내 K8s 명령 도구 설치

Jenkins에서 Kubernetes 자원을 직접 다루기 위해 Jenkins 컨테이너에 kubectl과 helm 도구를 설치합니다.

# Install kubectl, 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/arm64/kubectl"  # macOS
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"  # WindowOS

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

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

exit
--------------------------------------------
docker compose exec jenkins kubectl version --client=true
docker compose exec jenkins helm version

12.2 Jenkins Item 생성: K8s 명령 테스트 (k8s-cmd)

pipeline {
    agent any
    environment {
        KUBECONFIG = credentials('k8s-crd')
    }
    stages {
        stage('List Pods') {
            steps {
                sh '''
                # Fetch and display Pods
                kubectl get pods -A --kubeconfig "$KUBECONFIG"
                '''
            }
        }
    }
}

12.3 블루-그린 배포 준비: YAML 파일 작성 및 Git에 Push

# 
cd dev-app

#
mkdir deploy

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

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

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

#
tree

# Git에 Push
git add . && git commit -m "Add echo server yaml" && git push -u origin main

12.4 Jenkins를 통한 K8s 블루-그린 배포: Item 생성 (k8s-bluegreen)

12.4.1 사전 작업

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
혹은 
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; date ; echo "------------" ; sleep 1 ; done

12.5 Jenkinsfile – 블루-그린 배포 자동화

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

12.6 배포 결과 확인

12.7 실습 정리

kubectl delete deploy echo-server-blue echo-server-green && kubectl delete svc echo-server-service

12.8 Jenkins를 통한 Kubernetes 배포(CD)의 아쉬운 점

  • GUI 기반으로 수동 승인 단계가 많아 완전 자동화에는 부적합
  • 배포 기록과 로깅이 Jenkins 중심으로 집중되어 K8s 자체의 배포 기록과 분리됨
  • 보안상 Jenkins 내 kubeconfig 관리 부담 존재

12.9 개발 코드와 배포 코드를 하나의 저장소(dev-app)에서 같이 관리하는 방식의 단점

항목단일 저장소 (통합)분리 저장소 (개발 / 배포)
장점관리 단순, 연계 편리, 코드 변경과 배포 동기화 쉬움책임 분리, 배포 이력 독립적 관리, 보안 강화
단점권한 분리 어려움, 배포 코드 변경 시 개발 코드 영향을 받을 수 있음초기 구성 복잡, 연동 로직 추가 필요, 커뮤니케이션 비용 증가

13. Argo CD 설치 및 기본 설정

# 네임스페이스 생성 및 파라미터 파일 작성
cd cicd-labs

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

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

# 설치
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

# 확인
kubectl get pod,svc,ep,secret,cm -n argocd
kubectl get crd | grep argo
# 출력 예시
applications.argoproj.io                     2024-04-14T08:12:16Z
applicationsets.argoproj.io                  2024-04-14T08:12:17Z
appprojects.argoproj.io                      2024-04-14T08:12:16Z

# AppProject 리소스 조회
kubectl get appproject -n argocd -o yaml

# configmap
kubectl get cm -n argocd argocd-cm -o yaml
kubectl get cm -n argocd argocd-rbac-cm -o yaml
...
data:
  policy.csv: ""
  policy.default: ""
  policy.matchMode: glob
  scopes: '[groups]'


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

# Argo CD 웹 접속 주소 확인 : 초기 암호 입력 (admin 계정)
open "http://127.0.0.1:30002" # macOS
# Windows OS경우 직접 웹 브라우저에서 http://127.0.0.1:30002 접속

Argo CD 웹 접속 확인

User Info → UPDATE PASSWORD 로 admin 계정 암호 변경 (qwe12345)

Git 저장소 연결 (ops-deploy)

  • 메뉴 경로: Settings → Repositories → CONNECT REPO
  • connection method : VIA HTTPS
  • Type : git
  • Project : default
  • Repo URL : http://<자신의 집 IP>:3000/devops/ops-deploy http://192.168.254.127:3000/devops/ops-deploy
    • Windows (WSL2) 사용자는 자신의 WSL2 Ubuntu eth0 IP
  • Username : devops
  • Password : <Gogs 토큰>

14. Helm Chart 배포 실습

#
cd cicd-labs
mkdir nginx-chart
cd nginx-chart

mkdir templates

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

cat > 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 > 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 > values.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>Nginx version 1.26.1</p>
  </body>
  </html>

image:
  repository: nginx
  tag: 1.26.1

replicaCount: 1
EOF

cat > 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: "1.26.1"
EOF

# 이전 timeserver/service(nodeport) 삭제
kubectl delete deploy,svc --all

# 직접 배포 해보기
helm template dev-nginx . -f values.yaml
helm install dev-nginx . -f values.yaml
helm list
kubectl get deploy,svc,ep,cm dev-nginx -owide

#
curl http://127.0.0.1:30000
curl -s http://127.0.0.1:30000 | grep version
open http://127.0.0.1:30000


# value 값 변경 후 적용 해보기 : version/tag, replicaCount
cat > values.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>Nginx version 1.26.2</p>
  </body>
  </html>

image:
  repository: nginx
  tag: 1.26.2

replicaCount: 2
EOF

sed -i '' "s|1.26.1|1.26.2|g" Chart.yaml


# helm chart 업그레이드 적용
helm template dev-nginx . -f values.yaml # 적용 전 렌더링 확인 Render chart templates locally and display the output.
helm upgrade dev-nginx . -f values.yaml

# 확인
helm list
kubectl get deploy,svc,ep,cm dev-nginx -owide
curl http://127.0.0.1:30000
curl -s http://127.0.0.1:30000 | grep version
open http://127.0.0.1:30000

# 확인 후 삭제
helm uninstall dev-nginx

15. Argo CD를 통한 배포 1: GitOps

#
cd cicd-labs

TOKEN=<>
git clone http://devops:$TOKEN@$MyIP:3000/devops/ops-deploy.git
cd ops-deploy

#
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

#
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


tree nginx-chart
nginx-chart
├── Chart.yaml
├── VERSION
├── templates
│   ├── configmap.yaml
│   ├── deployment.yaml
│   └── service.yaml
├── values-dev.yaml
└── values-prd.yaml

# 
helm template dev-nginx nginx-chart -f nginx-chart/values-dev.yaml
helm template prd-nginx nginx-chart -f nginx-chart/values-prd.yaml
DEVNGINX=$(helm template dev-nginx nginx-chart -f nginx-chart/values-dev.yaml | sed 's/---//g')
PRDNGINX=$(helm template prd-nginx nginx-chart -f nginx-chart/values-prd.yaml | sed 's/---//g')
diff <(echo "$DEVNGINX") <(echo "$PRDNGINX")

#
git add . && git commit -m "Add nginx helm chart" && git push -u origin main

Argo CD UI로 Application 등록

K8S(Live) 수정 시도

⇒ GitOps를 위해서는, 반드시 단일 진실 공급원(Single Source Of Trush, SSOT)를 통해서 관리를 해야합니다.

1.26.2 로 업데이트(코드 수정) 후 반영 확인

#
VERSION=1.26.2

cat > nginx-chart/VERSION <<EOF
$VERSION
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: 2
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

#
git add . && git commit -m "Update nginx version $(cat nginx-chart/VERSION)" && git push -u origin main

SYNC 클릭 → SYNCHRONIZE 클릭

Argo CD 웹에서 App 삭제

16. Argo CD를 통한 배포 2: ArgoCD Declarative Setup

dev-nginx App 생성 및 Auto SYNC

#
echo $MyIP

cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: dev-nginx
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    helm:
      valueFiles:
      - values-dev.yaml
    path: nginx-chart
    repoURL: http://$MyIP:3000/devops/ops-deploy
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
    syncOptions:
    - CreateNamespace=true
  destination:
    namespace: dev-nginx
    server: https://kubernetes.default.svc
EOF


#
kubectl get applications -n argocd dev-nginx
kubectl get applications -n argocd dev-nginx -o yaml | kubectl neat
kubectl describe applications -n argocd dev-nginx
kubectl get pod,svc,ep,cm -n dev-nginx

#
curl http://127.0.0.1:30000
open http://127.0.0.1:30000

# Argo CD App 삭제
kubectl delete applications -n argocd dev-nginx

prd-nginx App 생성 및 Auto SYNC

#
cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prd-nginx
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  destination:
    namespace: prd-nginx
    server: https://kubernetes.default.svc
  project: default
  source:
    helm:
      valueFiles:
      - values-prd.yaml
    path: nginx-chart
    repoURL: http://$MyIP:3000/devops/ops-deploy
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
    syncOptions:
    - CreateNamespace=true
EOF

#
kubectl get applications -n argocd prd-nginx
kubectl describe applications -n argocd prd-nginx
kubectl get pod,svc,ep,cm -n prd-nginx

#
curl http://127.0.0.1:30000
open http://127.0.0.1:30000

# Argo CD App 삭제
kubectl delete applications -n argocd prd-nginx

17. Webhook 설정을 통한 GitOps 실시간 반영

  • Repo(ops-deploy) 에 webhooks 설정 : Gogs 선택
    • Payload URL : http://192.168.254.127:30002/api/webhook
      • Windows (WSL2) 사용자는 자신의 WSL2 Ubuntu eth0 IP
    • 나머지 항목 ‘기본값’ ⇒ Add webhook
    • 이후 생성된 webhook 클릭 후 Test Delivery 클릭 후 정상 응답 확인
  • dev-nginx App 생성 및 Auto SYNC
#
echo $MyIP

cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: dev-nginx
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    helm:
      valueFiles:
      - values-dev.yaml
    path: nginx-chart
    repoURL: http://$MyIP:3000/devops/ops-deploy
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
    syncOptions:
    - CreateNamespace=true
  destination:
    namespace: dev-nginx
    server: https://kubernetes.default.svc
EOF


#
kubectl get applications -n argocd dev-nginx
kubectl get applications -n argocd dev-nginx -o yaml | kubectl neat
kubectl describe applications -n argocd dev-nginx
kubectl get pod,svc,ep,cm -n dev-nginx

#
curl http://127.0.0.1:30000
open http://127.0.0.1:30000

  • Git(Gogs) 수정 후 ArgoCD 즉시 반영 확인
#
cd cicd-labs/ops-deploy/nginx-chart

#
sed -i -e "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
watch -d kubectl get all -n dev-nginx -o wide

#
sed -i -e "s|replicaCount: 3|replicaCount: 4|g" values-dev.yaml
git add values-dev.yaml && git commit -m "Modify nginx-chart : values-dev.yaml" && git push -u origin main
watch -d kubectl get all -n dev-nginx -o wide

#
sed -i -e "s|replicaCount: 4|replicaCount: 2|g" values-dev.yaml
git add values-dev.yaml && git commit -m "Modify nginx-chart : values-dev.yaml" && git push -u origin main
watch -d kubectl get all -n dev-nginx -o wide

  • Argo CD App 삭제
kubectl delete applications -n argocd dev-nginx

18. Jenkins CI + Argo CD

Repo(ops-deploy) 기본 코드 작업

#
cd ops-deploy

#
mkdir dev-app

# 도커 계정 정보
DHUSER=<도커 허브 계정>
DHUSER=gasida

# 버전 정보 
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

Repo(ops-deploy) 를 바라보는 ArgoCD App 생성

#
echo $MyIP

cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: timeserver
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    path: dev-app
    repoURL: http://$MyIP:3000/devops/ops-deploy
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
    syncOptions:
    - CreateNamespace=true
  destination:
    namespace: default
    server: https://kubernetes.default.svc
EOF

#
kubectl get applications -n argocd timeserver
kubectl get applications -n argocd timeserver -o yaml | kubectl neat
kubectl describe applications -n argocd timeserver
kubectl get deploy,rs,pod
kubectl get svc,ep timeserver

#
curl http://127.0.0.1:30000
curl http://127.0.0.1:30000/healthz
open http://127.0.0.1:30000

19. Argo Image Updater

ArgoCD 기존 방식

https://kmaster.tistory.com/85

ArgoCD Image Updater

https://kmaster.tistory.com/85

[출처] 책 GitOps Cookbook - https://product.kyobobook.co.kr/detail/S000214781090

20. Argo CD App-of-apps

기존 방식

https://beer1.tistory.com/68

App-of-apps

https://beer1.tistory.com/68

https://malwareanalysis.tistory.com/478

#
cd cicd-labs
git clone https://github.com/argoproj/argocd-example-apps.git

#
tree argocd-example-apps/apps
argocd-example-apps/apps
├── Chart.yaml
├── templates
│   ├── helm-guestbook.yaml
│   ├── helm-hooks.yaml
│   ├── kustomize-guestbook.yaml
│   ├── namespaces.yaml
│   └── sync-waves.yaml
└── values.yaml

#
helm template -f argocd-example-apps/apps/values.yaml argocd-example-apps/apps


# you need to create and sync your parent app
cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: apps
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    path: apps
    repoURL: https://github.com/argoproj/argocd-example-apps.git
    targetRevision: HEAD
  destination:
    namespace: argocd
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: true
EOF

#
kubectl get applications -n argocd --show-labels 
NAME                  SYNC STATUS   HEALTH STATUS   LABELS
apps                  Synced        Healthy         <none>
helm-guestbook        OutOfSync     Missing         argocd.argoproj.io/instance=apps
helm-hooks            OutOfSync     Missing         argocd.argoproj.io/instance=apps
kustomize-guestbook   OutOfSync     Missing         argocd.argoproj.io/instance=apps
sync-waves            OutOfSync     Missing         argocd.argoproj.io/instance=apps

# 상태 모니터링
kubectl get applications -n argocd -w

확인

# 확인
kubectl get pod -n helm-guestbook -l app=helm-guestbook
NAME                              READY   STATUS    RESTARTS     AGE
helm-guestbook-57c97698c4-hsnxn   0/1     Running   1 (2s ago)   64s

# Readiness Probe 실패 : CPU Arch 가 AMD64 로 mac M에서 동작 불가
kubectl describe pod -n helm-guestbook -l app=helm-guestbook
Containers:
  helm-guestbook:
    Container ID:   containerd://2e53557351150a119825820c3891915e1dd811ed4311e78caebebcac47aabd37
    Image:          gcr.io/heptio-images/ks-guestbook-demo:0.1
...
  Warning  Unhealthy  4s (x10 over 76s)  kubelet            Readiness probe failed: Get "http://10.244.2.24:80/": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
  
#
for i in worker worker2; do echo ">> node $i <<"; docker exec -it myk8s-$i ctr -n k8s.io image list --quiet | grep -i guestbook; echo; done

#
docker pull gcr.io/heptio-images/ks-guestbook-demo:0.1
gcr.io/heptio-images/ks-guestbook-demo:0.1
gcr.io/heptio-images/ks-guestbook-demo@sha256:fe18e00a6aeece16b5b2f77a32ee60929e8a60e27c71df8df66bf804f5677f47

#
docker pull gcr.io/heptio-images/ks-guestbook-demo:0.1
docker images
docker inspect gcr.io/heptio-images/ks-guestbook-demo:0.1 | grep -i arch
"Architecture": "amd64",

21. Argo Rollout

Argo Rollouts는 Kubernetes의 기본 롤링 업데이트 전략보다 더 정교하고 유연한 배포 방식을 제공합니다. Ingress Controller나 Service Mesh와의 통합을 통해 트래픽을 점진적으로 새로운 버전으로 전환할 수 있으며, 다양한 외부 메트릭을 기반으로 KPI를 확인하여 자동으로 배포를 승격(promote)하거나 롤백할 수 있도록 지원합니다.

Why Argo Rollouts?

Kubernetes의 기본 Deployment 오브젝트는 RollingUpdate 전략을 제공하지만 다음과 같은 한계가 존재합니다:

  • 출시 속도 제어 기능 부족: 배포 속도를 세밀하게 제어할 수 없습니다.

  • 트래픽 제어 불가: 새 버전에 대한 트래픽을 점진적으로 유도하는 Canary 배포를 직접 제어할 수 없습니다.

  • Readiness Probe 한계: 복잡하거나 부하 테스트(Stress Test), 일회성 검증에는 적합하지 않습니다.

  • 외부 메트릭 확인 불가: Prometheus 등의 외부 모니터링 시스템을 쿼리하여 배포 성공 여부를 판단할 수 없습니다.

  • 자동 롤백 기능 없음: 문제가 발생하면 수동으로 중단은 가능하나, 자동으로 배포를 중지하거나 롤백할 수 없습니다.

아키텍처

https://argoproj.github.io/argo-rollouts/architecture/

22. GitOps Bridge

Kubernetes 클러스터를 만드는 과정부터 GitOps를 통해 모든 것을 관리 - Github

0개의 댓글