상세 컨텐츠

본문 제목

8주차 - K8S CI/CD

K8S - EKS

by 존자르징어 2025. 3. 29. 22:37

본문

본 글은 CloudNet@ 팀 가시다님의 'AWS EKS Hands-on' 스터디를 내용을 기반으로 정리하였습니다.
Kubernetes(이하 k8s)에 대한 지식(아키텍처, 사용법 등)이 있다고 가정하고 작성했습니다.
잘못된 정보가 있으면 언제든지 알려주시기 바랍니다.

목차

0. 실습 환경 구성

1. Jenkins CI + K8S(Kind)

2. Jenkins CI/CD + K8S(Kind)

3. Argo CD + K8S(Kind)

4. Jenkins CI + Argo CD + K8S(Kind)

5. Argo Image Updater

6. Argo CD App-of-apps

7. Argo Rollout

8. GitOps Bridge

 

0. 실습 환경 구성

- 8주차 실습 환경은 '개인 PC'에서 진행함

 

> 컨테이너 2대 (Jenkins, gogs) : Host OS 포트 노출 로 접속 및 사용 (Mac)

# 작업 디렉토리 생성 후 이동
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"
    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


# 배포
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 컨테이너 초기 설정

# Jenkins 초기 암호 확인
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
09a21116f3ce4f27a0ede79372febfb1

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

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

 

> Jenkins URL 설정 : 각자 자신의 PC의 IP를 입력

 

> Jenkins

  . 소프트웨어 개발의 자동화 도구로, 지속적 통합(CI)지속적 배포(CD) 워크플로를 지원함

- CI/CD 워크플로 예시

  1. 최신 코드 가져오기: 중앙 리포지터리에서 최신 코드를 로컬로 가져오기.
  2. 단위 테스트 실행: 코드 작성 전에 단위 테스트 케이스 작성 후 실행.
  3. 코드 개발: 실패한 테스트를 통과하도록 코드 개발.
  4. 단위 테스트 재실행: 수정된 코드로 단위 테스트 재실행.
  5. 코드 푸시 및 병합: 중앙 리포지터리로 푸시하고 병합.
  6. 코드 컴파일: 병합된 코드 전체 애플리케이션을 컴파일.
  7. 전체 테스트 실행: 개별 테스트 및 통합 테스트 실행.
  8. 배포: 빌드된 애플리케이션을 프로덕션 환경에 배포.
  9. E2E 테스트: 셀레늄(Selenium) 같은 도구로 전체 워크플로 테스트.

  - 주요 기능

    . 자동화: 코드 가져오기, 컴파일, 테스트 실행, 배포 등 소프트웨어 개발 과정을 자동화

    . 서블릿 컨테이너: 자바로 작성된 Jenkins는 Apache Tomcat과 같은 서블릿 컨테이너에서 실행됨

    . DSL(Jenkinsfile): 빌드 단계를 정의하는 스크립트 작성, 파이프라인을 통해 각 단계의 순서를 설정

    . 플러그인 지원: 다양한 플러그인을 통해 빌드 도구(Maven, Gradle 등), 버전 관리 도구(Git, SVN 등), 프로그래밍 언어(Java, Python, Node.js 등)와 연동 가능

 

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

# Jenkins 컨테이너 내부에 도커 실행 파일 설치
docker compose exec --privileged -u root jenkins bash
-----------------------------------------------------
id

curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update && apt install docker-ce-cli curl tree jq yq -y

docker info
docker ps
which docker

# Jenkins 컨테이너 내부에서 root가 아닌 jenkins 유저도 docker를 실행할 수 있도록 권한을 부여
groupadd -g 2000 -f docker  # macOS(Container)

chgrp docker /var/run/docker.sock
ls -l /var/run/docker.sock
usermod -aG docker jenkins
cat /etc/group | grep docker

exit
--------------------------------------------

# 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 (mac)

# 초기 설정 웹 접속
open "http://127.0.0.1:3000/install" # macOS

 

- 초기 설정

  . 데이터베이스 유형 : SQLite3

  . 애플리케이션 URL : http://<각자 자신의 PC IP>:3000/

  . 기본 브랜치 : main

  . 관리자 계정 설정 클릭 : 이름(계정명 - 닉네임 사용 devops), 비밀번호(계정암호 qwe123), 이메일 입력

 

> [Token 생성] 로그인 후 → Your Settings → Applications : Generate New Token 클릭 - Token Name(devops) ⇒ Generate Token 클릭 : 메모해두기!

 

- 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 주소 확인

 

> Gogs 실습을 위한 저장소 설정 : 호스트에서 직접 git 작업

# (옵션) 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'...
...

#
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

# server.py 파일 작성
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
import socket

class RequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        match self.path:
            case '/':
                now = datetime.now()
                hostname = socket.gethostname()
                response_string = now.strftime("The time is %-I:%M:%S %p, VERSION 0.0.1\n")
                response_string += f"Server hostname: {hostname}\n"                
                self.respond_with(200, response_string)
            case '/healthz':
                self.respond_with(200, "Healthy")
            case _:
                self.respond_with(404, "Not Found")

    def respond_with(self, status_code: int, content: str) -> None:
        self.send_response(status_code)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(bytes(content, "utf-8")) 

def startServer():
    try:
        server = ThreadingHTTPServer(('', 80), RequestHandler)
        print("Listening on " + ":".join(map(str, server.server_address)))
        server.serve_forever()
    except KeyboardInterrupt:
        server.shutdown()

if __name__== "__main__":
    startServer()
EOF


# (참고) python 실행 확인
python3 server.py
curl localhost
curl localhost/healthz
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

#
tree
git status
git add .
git commit -m "Add dev-app"
git push -u origin main
...

 

> App Version 예시

 

> 도커 허브 프라이빗 저장소 만들기 및 토큰 생성

   . 프라이빗 리포 만들기

 

  . 토큰 생성
1. 계정 → Account settings

2. Security → Personal access tokens

3. Generate new token : 만료일(편한대로), Access permissions(Read, Write, Delete)

4. 발급된 token 메모


1. Jenkins CI + K8S(Kind)

> kind 설치

  1. Docker Desktop 설치

  - 참고 : https://docs.docker.com/desktop/install/mac-install/

  2. kind 및 출 설치

  - 필수 툴 설치

# Install Kind
brew install kind
kind --version

# Install kubectl
brew install kubernetes-cli
kubectl version --client=true

## kubectl -> k 단축키 설정
echo "alias kubectl=kubecolor" >> ~/.zshrc

# Install Helm
brew install helm
helm version

  - (권장) 유용한 툴 설치

# 툴 설치
brew install krew
brew install kube-ps1
brew install kubectx

# kubectl 출력 시 하이라이트 처리
brew install kubecolor
echo "alias kubectl=kubecolor" >> ~/.zshrc
echo "compdef kubecolor=kubectl" >> ~/.zshrc

# krew 플러그인 설치
kubectl krew install neat stren

 

> Kind로 k8s 배포

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

# Create a cluster with kind
MyIP=<각자 자신의 PC IP>
MyIP=192.168.254.127

# cicd-labs 디렉터리에서 아래 파일 작성
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

# 확인
kind get nodes --name myk8s
kubens default

# kind 는 별도 도커 네트워크 생성 후 사용 : 기본값 172.18.0.0/16
docker network ls
docker inspect kind | jq

# 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 파일 확인
cat ~/.kube/config

- 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 배율)
open "http://127.0.0.1:30001/#scale=1.5"
open "http://127.0.0.1:30001/#scale=2"

- (참고) 클러스터 삭제

# 클러스터 삭제
kind delete cluster --name myk8s
docker ps
cat ~/.kube/config

 

> 작업 소개 (프로젝트, Job, Item) :

1. Trigger : 작업을 수행하는 시점

2. Built step : 작업을 구성하는 단계별 태스크 

. 특정 목표를 수행하기 위한 태스크를 단계별 step

3. Post-build action : 태스크가 완료 후 수행할 명령 

. 작업의 결과(성공 or 실패)를 사용자에게 알려주는 후속 동작이나, 자바 코드를 컴파일한 후 생성된 클래스 파일을 특정 위치로 복사 등

 

(참고) 젠킨스의 빌드 : 젠킨스 작업의 특정 실행 버전

  . 실행될 때마다 고유 빌드 번호가 부여된다.

  . 작업 실행 중에 생성된 아티팩트, 콘솔 로드 등 특정 실행 버전과 관련된 모든 세부 정보가 해당 빌드 번호로 저장

 

> Jenkins 설정 : Plugin 설치 및 자격 증명 설정

- Jenkins Plugin 설치

  . Pipeline Stage View

  . Docker Pipeline : building, testing, and using Docker images from Jenkins Pipeline

  . Gogs : Webhook Plugin

    -  http(s)://<< jenkins-server >>/gogs-webhook/?job=<<jobname>>

 

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

  1. Gogs Repo 자격증명 설정 : gogs-crd

      . Kind : Username with password

      . Username : devops

      . Password : <Gogs 토큰>

      . ID : gogs-crd

 

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

    . Kind : Username with password

    . Username : <도커 계정명>

    . Password : <도커 계정 암호 혹은 토큰>

    . ID : dockerhub-crd

3. k8s(kind) 자격증명 설정 : k8s-crd

    . Kind : Secret file

    . File : <kubeconfig 파일 업로드>

    . ID : k8s-crd

⇒ macOS 사용자 경우, cp ~/.kube/config ./kube-config 복사 후 해당 파일 업로드

 

> Jenkins Item 생성 (Pipeline) : item name(pipeline-ci)

. Pipeline script : '자신의 도커 허브 계정', '자신의 집 IP'는 자신의 환경에 맞게 수정

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

 

- 지금 빌드

- 도커 허브 확인

 

=> 파이프라인에 맞게 version(0.0.1), latest tag로 업로드

 

> k8s Deploying an application with Jenkins(pipeline-ci)

# 디플로이먼트 오브젝트 배포 : 리플리카(파드 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​

=> Private 저장소 이미지이기 때문에 자격 증명 필요

# 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

- 접속 테스트

# 서비스 생성
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

 

> 어플리케이션 업데이트 하기

- 샘플 앱 server.py 코드 변경젠킨스(지금 빌드 실행) : 0.0.2 버전 태그로 컨테이너 이미지 빌드 → 컨테이너 저장소 Push k8s deployment 업데이트 배포

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

# 파드 복제복 증가
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

=> 코드는 변경되었으나, 이미지 레지스트리에 0.0.2 tag 이미지가 없기 때문에 에러 발생

=> 젠킨스 지금 빌드!

 

> Gogs Webhoosk 설정 : 소스 코드 변경시 자동으로 이미지 빌드 후 푸시 (Job Trigger)

  - gogs 에 /data/gogs/conf/app.ini 파일 수정 후 컨테이너 재기동

[security]
INSTALL_LOCK = true
SECRET_KEY   = j2xaUPQcbAEwpIu
LOCAL_NETWORK_ALLOWLIST = 192.168.254.127 # 각자 자신의 PC IP , Windows (WSL2) 사용자는 자신의 WSL2 Ubuntu eth0 IP

 

> gogs 에 Webhooks 설정 : Jenkins job Trigger - Setting → Webhooks → 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

⇒ Add webhook

 

> Jenkins Item 생성(Pipeline) : item name(SCM-Pipeline)

  . 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 => jenkins pipeline 파일을 gogs repo에서 가져온다.

    . SCM : Git

      - Repo URL(http://***<mac IP>***:3000/***<Gogs 계정명>***/dev-app)

      - Credentials(devops/***)

      - Branch(*/main)

     . Script Path : Jenkinsfile

 

> Jenkinsfile 작성 후 Git push

# Jenkinsfile 빈 파일 작성
touch Jenkinsfile

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

 

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

=> Trigger 빌드 수행

 

2. Jenkins CI/CD + K8S(Kind)

=> 젠킨스가 k8s에 배포까지 하도록해보자!

 

> Jenkins 컨테이너 내부에 툴 설치 : kubectl(v1.32), 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

 

> Jenkins Item 생성(Pipeline) : item name(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"
                '''
            }
        }
    }
}

 

> [K8S CD 실습] Jenkins 를 이용한 blue-green 배포 준비

  - 디플로이먼트 / 서비스 yaml 파일 작성 - http-echo 및 코드 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 add . && git commit -m "Add echo server yaml" && git push -u origin main

 

> Jenkins Item 생성(Pipeline) : item name(k8s-bluegreen) - Jenkins 통한 k8s 기본 배포 

  - 이전 실습 자원 삭제

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

 

- script 작성

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

 

=> 지금빌드


3. Argo CD + K8S(Kind)

 - ArgoCD : GitOps 방식의 지속적 배포(CD) 도구로, Kubernetes 환경에서 애플리케이션을 선언적 방식으로 관리함

    . 선언적(Declarative) 방식: 애플리케이션 정의, 구성, 환경 설정은 선언적

    . 자동화 및 감사 가능: 애플리케이션 배포와 라이프사이클 관리가 자동화, 모든 작업은 감사 가능

    . Git 저장소를 애플리케이션의 원하는 상태를 정의

       - Helm 차트

       - YAML/JSON 매니페스트 디렉터리

       - 커스텀 구성 관리 툴

       - kustomize

  1. API Server : Web UI, CLI, CI/CD 시스템과 통신하며 애플리케이션 관리, 상태 보고, 인증 등을 처리 수행
  2. Repository Server : Git 연결, 배포 대상 매니페스트를 생성합니다.
  3. Application Controller : 애플리케이션 상태를 지속적으로 모니터링, Git에서 정의된 원하는 상태와 비교

> 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

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 접속

 

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

- ops-deploy Repo 등록 : Settings → Repositories → CONNECT REPO 클릭

  • connection method : VIA HTTPS
  • Type : git
  • Project : default
  • Repo URL : http://***<자신의 집 IP>***:3000/devops/ops-deploy
  • Username : devops
  • Password : <Gogs 토큰>

⇒ 입력 후 CONNECT 클릭

 

> 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

 

> Repo(ops-deploy) 에 nginx helm chart 를 Argo CD를 통한 배포 1

  - git 작업

#
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에 App 등록 : Application → NEW APP

  . GENERAL

    - App Name : dev-nginx

    - Project Name : default

    - SYNC POLICY : Manual

       . AUTO-CREATE NAMESPACE : 클러스터에 네임스페이스가 없을 시 argocd에 입력한 이름으로 자동 생성

       . APPLY OUT OF SYNC ONLY : 현재 동기화 상태가 아닌 리소스만 배포

    - SYNC OPTIONS : AUTO-CREATE NAMESPACE(Check)

    - PRUNE PROPAGATION POLICY

      . foreground : 부모(소유자, ex. deployment) 자원을 먼저 삭제함

      . background : 자식(종속자, ex. pod) 자원을 먼저 삭제함

      . orphan : 고아(소유자는 삭제됐지만, 종속자가 삭제되지 않은 경우) 자원을 삭제함

    - Source

      . Repo URL : 설정되어 있는 것 선택

      . Revision : HEAD

      . PATH : nginx-chart

    - DESTINATION

      . Cluster URL : <기본값>

      . NAMESPACE : dev-nginx

    - HELM

      . Values files : values-dev.yaml

⇒ 작성 후 상단 CREATE 클릭

    - PRUNE : GIt에서 자원 삭제 후 배포시 K8S에서는 삭제되지 않으나, 해당 옵션을 선택하면 삭제시킴

    - FORCE : --force 옵션으로 리소스 삭제

    - APPLY ONLY : ArgoCD의 Pre/Post Hook은 사용 안함 (리소스만 배포)

    - DRY RUN : 테스트 배포 (배포에 에러가 있는지 한번 확인해 볼때 사용)

=> 코드 형상이 라이브 형상에 반영이 안 된 상태!

=> Sync!

 

> GitOps 방식을 무시하고 K8S(Live) 수정 시도하기

- 동작 확인용 반복 접속

while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; date ; echo "------------" ; sleep 1 ; done

- LIVE MANIFESTEDIT 클릭 후 index.html 내용 추가 → SAVE

- K8S(Live) 상태 확인

#
kubectl get cm -n dev-nginx dev-nginx -o yaml
apiVersion: v1
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head>
      <title>Welcome to Nginx!</title>
    </head>
    <body>
      <h1>Hello, Kubernetes!</h1>
      <p>DEV testtest : Nginx version 1.26.1</p>
    </body>
    </html>
...
  labels:
    argocd.argoproj.io/instance: dev-nginx
    myname: tester
...

# (추가) kubectl 로 직접 k8s 추가 시 >> 이후 ArgoCD LIVE 에서 확인!
kubectl get cm -n dev-nginx dev-nginx --show-labels
kubectl label cm dev-nginx -n dev-nginx study=aews
kubectl get cm -n dev-nginx dev-nginx --show-labels

# 변경된 CM 적용을 위해서 롤아웃
kubectl rollout restart deployment -n dev-nginx dev-nginx

#
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; date ; echo "------------" ; sleep 1 ; done

=> 없는 필드는 인식을 못함

=> Desired Manifest를 기준으로 비교하므로 반드시 코드를 수정해야함

⇒ 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

 

> reconciliation config

kubectl get cm -n argocd argocd-cm -o yaml | grep timeout
  timeout.hard.reconciliation: 0s
  timeout.reconciliation: 180s

> SYNC 클릭 → SYNCHRONIZE 클릭

# 배포 확인
kubectl get all -n dev-nginx -o wide

- Argo CD 웹에서 App 삭제

 

> Repo(ops-deploy) 에 nginx helm chart 를 Argo CD를 통한 배포 2 : ArgoCD Declarative Setup

  - ArgoCD Declarative Setup : Project, applications(ArgoCD App 자체를 yaml로 생성), ArgoCD Settings 

    . finalyzer : 리소스 삭제 요청을 받았을 때 바로 제거되지 않고, 지정된 작업이 완료될 때까지 "종료 중"(Terminating) 상태로 유지

    . ArgoCD는 이 메커니즘을 활용해 애플리케이션 삭제 시 관리 대상 리소스의 정리(cleanup)를 제어

    . resources-finalizer.argocd.argoproj.io : 애플리케이션이 삭제될 때 해당 애플리케이션이 관리하는 모든 리소스(예: Pod, Service, ConfigMap 등)를 함께 삭제하도록 보장

 

  - ArgoCD Finalizers의 목적

  1. 리소스 정리 보장
  2. 의도치 않은 삭제 방지
  3. App of Apps 패턴 지원

- 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

 

> Repo(ops-deploy) 에 Webhook 를 통해 Argo CD 에 즉시 반영 trigger하여 k8s 배포 할 수 있게 설정

  - Repo(ops-deploy) 에 webhooks 설정 : Gogs 선택

    . Payload URL : http://argocd주소:30002/api/webhook

    . 나머지 항목 ‘기본값’ ⇒ 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

replicas 변경(2->3)
replicas 변경(3->4)
replicas 변경(4->2)

 

- Argo CD App 삭제

kubectl delete applications -n argocd dev-nginx


4. Jenkins CI + Argo CD + K8S(Kind)

* Full CI/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

 

 

> Repo(dev-app) 코드 작업

  - dev-app Repo에 VERSION 업데이트 시 → ops-deploy Repo 에 dev-app 에 파일에 버전 정보 업데이트 작업 추가

  1. 기존 버전 정보는 VERSION 파일 내에 정보를 가져와서 변수 지정 : OLDVER=$(cat dev-app/VERSION)
  2. 신규 버전 정보는 environment 도커 태그 정보를 가져와서 변수 지정 : NEWVER=$(echo ${DOCKER_TAG})
  3. 이후 sed 로 ops-deploy Repo 에 dev-app/VERSION, timeserver.yaml 2개 파일에 ‘기존 버전’ → ‘신규 버전’으로 값 변경
  4. 이후 ops-deploy Repo 에 git push ⇒ Argo CD App Trigger 후 AutoSync 로 신규 버전 업데이트 진행

  - dev-app 에 위치한 Jenkinsfile 로 젠킨스에 SCM-Pipeline(SCM:git) 으로 사용되고 있는 파일을 수정해서 실습에 사용

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

 

=> dev-app repo에 있는 Jenkins 파일 수정

 

- dev-app (Repo) 에서 git push 수행

# [터미널] 동작 확인 모니터링
while true; do curl -s --connect-timeout 1 http://127.0.0.1:30000 ; echo ; kubectl get deploy timeserver -owide; echo "------------" ; sleep 1 ; done

#
cd cicd-labs/dev-app

# 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

 

 

=> dev-app Repo 에서 코드 업데이트

=> jenkins pipeline 에서 ops-deploy Repo 에 버전 정보 업데이트를 하고, 이후 Argo CD가 자동으로 신규 버전 정보로 배포


5. Argo Image Updater

- ArgoCD 기존 방식

 

 

- ArgoCD Image Updater

=> ArgoCd가 이미지 저장소를 모니터링하고 코드 변경


6. Argo CD App-of-apps

- 기존 방식 : Argo App이 많아지면 관리 어려움

=> Argo App을 App으로 묶어서 관리 (App of apps : Root, Child)


> [ArgoCD Docs] Cluster Boostrapping : app of apps pattern

  - 클러스터 부트스트랩핑 할 때 필수 설치 대상은 한 번에 묶어서 배포

#
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",

 

- 삭제

kubectl delete applications -n argocd apps

 

=> Non cascade를 선택하면 app of app application만 삭제되고 다른 application은 유지 Ignoring differences in child applications


7. Argo Rollout

- 효율적인 배포를 위해 필요한 고급 기능들을 제공하며, 트래픽 제어, 메트릭 분석, 자동화된 롤백/프로모션 기능 제공

  . 점진적 트래픽 전환: 세부적으로 조정 가능

  . 자동 롤백 및 프로모션: 배포 중 문제가 발생하면 자동으로 롤백하고, 정상적으로 완료되면 자동으로 프로모션

  . 메트릭 쿼리: 사용자 맞춤형 메트릭 쿼리 지원.

  . Ingress 컨트롤러 통합: NGINX, ALB, Apache APISIX 등과 통합

  . 서비스 메시 통합: Istio, Linkerd, SMI와 통합

  . 여러 메트릭 프로바이더 지원: Prometheus, Datadog, New Relic 등 다양한 메트릭 제공업체와 통합.

 

> Argo Rollouts 설치 및 Sample 테스트

 

 

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

kubectl create ns argo-rollouts
cat <<EOT > argorollouts-values.yaml
dashboard:
  enabled: true
  service:
    type: NodePort
    nodePort: 30003
EOT

# 설치: 2.35.1
helm install argo-rollouts argo/argo-rollouts --version 2.39.2 -f argorollouts-values.yaml --namespace argo-rollouts

# 확인
kubectl get all -n argo-rollouts
kubectl get crds

# Argo rollouts 대시보드 접속 주소 확인
echo "http://127.0.0.1:30003"
open "http://127.0.0.1:30003"

 

> Deploying a Rollout

  - 롤아웃 리소스와 해당 롤아웃을 타겟으로 하는 Kubernetes 서비스를 배포

  - 이 가이드의 예제 롤아웃은 카나리 배포 전략을 사용하여 트래픽의 20%를 카나리아로 보내고 수동 프로모션 수행 마지막으로 업그레이드의 나머지 기간 동안 점진적으로 자동 트래픽이 증가

  - 이 동작은 롤아웃 동작 정의:

spec:
  replicas: 5
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: {}
      - setWeight: 40
      - pause: {duration: 10}
      - setWeight: 60
      - pause: {duration: 10}
      - setWeight: 80
      - pause: {duration: 10}
# 다음 명령을 실행하여 초기 롤아웃 및 서비스를 배포합니다:
kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/rollout.yaml
kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/service.yaml

# 확인
kubectl get rollout --watch
kubectl get rollout
kubectl describe rollout

kubectl get pod -l app=rollouts-demo
kubectl get svc,ep rollouts-demo
kubectl get rollouts rollouts-demo -o json | grep rollouts-demo
...
   "image": "argoproj/rollouts-demo:blue"
...​



- Updating a Rollout

  - Pod 템플릿(spec.template)을 변경하여 새로운 버전의 ReplicaSet을 배포함.

  - 롤아웃 사양을 수정하고 컨테이너 이미지 버전을 변경한 후 클러스터에 적용

  - 롤아웃 플러그인은 실시간 롤아웃 객체에 대해 설정 이미지 명령을 제공, 예를 들어 "yellow" 버전으로 업데이트 가능

# Run the following command to update the rollouts-demo Rollout with the "yellow" version of the container:
KUBE_EDITOR="nano" kubectl edit rollouts rollouts-demo
..
     - image: argoproj/rollouts-demo:yellow
...

#
kubectl get rollout --watch

# 파드 label 정보 확인
watch -d kubectl get pod -l app=rollouts-demo -owide --show-labels

 

- 자원 전체 삭제

docker compose down --volumes --remove-orphans && kind delete cluster --name myk8s

 

8. GitOps Bridge

- Kubernetes 클러스터를 만드는 과정부터 GitOps를 통해 모든 것을 관리하는 과정까지 연결하는 모범 사례와 패턴을 선보이는 커뮤니티 프로젝트 (https://github.com/gitops-bridge-dev/gitops-bridge/tree/main/argocd/iac/terraform/examples/eks/complete)

 

 

 

 

'K8S - EKS' 카테고리의 다른 글

9주차 - EKS Blue/Green Upgrade 실습  (0) 2025.04.05
7주차 - EKS Mode/Nodes  (0) 2025.03.22
6주차 - EKS Security  (0) 2025.03.15
5주차 - EKS Autoscaling  (0) 2025.03.07
4주차 - EKS Observability  (0) 2025.02.28

관련글 더보기