Post

최종 프로젝트 인프라 구축⑥-젠킨스, argocd, helm 구성

최종 프로젝트 인프라 구축⑥-젠킨스, argocd, helm 구성

완벽한 CI/CD 환경을 위하여

개요

일주일 가까이 로컬 환경만 삽질하다 보니까 가끔씩 개발이 그리워질 때도 있다.

적어도 개발은 주는 대로 만들기만 하면 되니까

하지만 인프라 엔지니어링이 더 재밌는데 어떡해?~

어쨌든 이번에는 구축한 쿠버네티스 위에 다시 CI/CD 파이프라인을 올려 보겠다.

재구성한 CI/CD구조

멘토링을 통해 Helm Chart와 ArgoCD라는 것을 배워서 이걸 같이 써먹기로 했다.

기존에는 젠킨스로 CI(컨테이너 이미지 빌드)와 CD(쿠버 배포)를 모두 했다면, 이번에는

  • Jenkins로 이미지 빌드
  • ArgoCD로 쿠버 배포
  • Helm Chart로 쿠버 배포할 yaml들 관리

이렇게 역할이 분담된다. 즉 빌드 단계가

do8-img1

  1. 깃허브 웹훅이 브랜취 푸쉬를 읽어 젠킨스에 전달(scm)
  2. 젠킨스가 그걸 읽어다 서비스(자바) 및 직접 컨테이너 빌드하여 로컬 레지스트리 업로드
  3. 배포할 yaml들은 Helm Chart 구조로 별도의 레포지토리에서 관리
  4. 젠킨스가 3.의 레포를 가져와 버전 등을 수정하여 커밋&푸쉬
  5. 푸쉬된 변화물들을 ArgoCD가 읽어 배포(kubectl apply) 수행

볼드체 친 게 기존 프로세스와의 차이인데, 글로 쓰면 이해하기 힘드니 직접 해보자!

젠킨스 컨테이너 재설치

드라이버 오류를 해결하다 젠킨스 볼륨을 날려먹었다..

그래서 다시 설치하는 삽질을 하는데 발견한 것이,

굳이 여기 나와있는 플러그인을 다 깔려 하지 말고

  • Authentication Tokens API
  • Github
  • Generic Webhook Trigger
  • Pipeline: Stage View

이것만 깔면 된다. 이제는 쿠버노드로 원격 접속하지 않을 거니까

그리고 젠킨스 컨테이너 파일도 아래와 같이 수정야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
FROM jenkins/jenkins:lts-jdk17

USER root

# 기본 패키지 설치
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        ca-certificates \
        gnupg \
        lsb-release \
        git \
        software-properties-common \
        sudo \
        uidmap \
        fuse-overlayfs \
        slirp4netns && \
    rm -rf /var/lib/apt/lists/*

# jenkins 계정에 sudo 권한 부여 (비밀번호 없이 가능하게)
RUN echo "jenkins ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/jenkins && \
    chmod 440 /etc/sudoers.d/jenkins

# Podman 설치
RUN . /etc/os-release && \
    echo "deb http://deb.debian.org/debian $VERSION_CODENAME-backports main" >> /etc/apt/sources.list && \
    apt-get update && \
    apt-get -t $VERSION_CODENAME-backports install -y podman podman-docker && \
    rm -rf /var/lib/apt/lists/*

# Podman 기본 설정 (docker.io 검색 허용, 192.168.56.200:5000 로컬레포 보안연결 비활성화)
RUN mkdir -p /etc/containers && \
    echo 'unqualified-search-registries = ["docker.io"]\n\n[[registry]]\nlocation = "192.168.56.200:5000"\ninsecure = true' > /etc/containers/registries.conf

# Helm CLI 설치
RUN curl https://baltocdn.com/helm/signing.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/helm.gpg && \
    apt-get install -y apt-transport-https && \
    echo "deb https://baltocdn.com/helm/stable/debian/ all main" > /etc/apt/sources.list.d/helm-stable-debian.list && \
    apt-get update && \
    apt-get install -y helm && \
    rm -rf /var/lib/apt/lists/*

# yq 설치: yaml 파일 수정을 위한 팩키지
RUN curl -L https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -o /usr/local/bin/yq && \
    chmod +x /usr/local/bin/yq

# Podman 루트리스(rootless) 실행을 위한 디렉토리 및 환경 변수 설정
# 컨테이너 환경에서 못 하는 리눅스의 /run/user/(유저번호) 디렉토리 대체
ENV XDG_RUNTIME_DIR=/tmp/run
RUN mkdir -p /tmp/run && chmod 700 /tmp/run && chown jenkins:jenkins /tmp/run

# Jenkins 홈 내부 Podman 설정 디렉토리 생성
RUN mkdir -p /home/jenkins/.config/containers && chown -R jenkins:jenkins /home/jenkins/.config

USER jenkins

잼민이(gemini)의 힘을 빌리긴 했는데, 요약하자면

  • 컨테이너 안에서 배포하기 위한 패키지(podman, fuse-overlayfs등) 설치
    • 사실 Docker Tools로 호스트(VM)에 깔린 podman을 사용해도 되겠지만, 루트리스 관련 오류 땜에 컨테이너가 무거워지더라도 그냥 내부에서 설치 하였다.
  • 외부 이미지 로드에 필요한 docker.io 레지스트리와 로컬 레지스트리 설정(/etc/containers/registries.conf)
  • podman이 컨테이너 안에서도 sudo 없이 돌아가도록 설정(루트리스rootless 모드)
    • uidmap, fusr-overlayfs, slirp4netns 등 팩키지 설치(각각 루트리스용 사용자, 스토리지, 네트워크)
    • 컨테이너는 /run/user/(유저번호)가 없어서 순정으론 루트리스를 못 해, 이걸 /tmp/run으로 대체(XDG_RUNTIME_DIR 환경변수)
  • helm chart 수정에 필요한 패키지(helm, yp 등) 설치

이렇게 수정한 다음 빌드하고(본인은 sudo podman build -t my-jenkins2 . 이렇게 my-jenkins2로 했다)

(젠킨스파일까지 다 만들고 다시 보니까, 컨테이너에 너무 많이 넣긴 했다. 솔직히 helm cli는 필요 없는데)

1
2
3
4
5
6
7
8
sudo podman run -d --privileged \
  -p 8000:8080 \
  -p 50000:50000 \
  -v jenkins_data:/var/jenkins_home \
  -v /run/podman/podman.sock:/var/run/docker.sock \
  --name jenkins-server \
  --restart=always \
  my-jenkins2

전과 비슷하게 올리되, –privileged 옵션을 주고 올려야 podman 빌드시 에러 확률이 낮다.
(컨테이너 사용자 권한을 호스트 사용자 권한과 동일하게 맞추는 옵숀)

로컬 레지스트리 설치

이제 이미지를 올릴 로컬 레지스트리를 설치해주자.

전작과 달리 로컬 레지스트리를 까는 이유는 계속 말했듯이 빌드와 배포를 서로 다른 서버에서 하기 때문이다.

1
2
3
4
5
6
7
8
sudo mkdir -p /opt/registry

sudo podman run -d \
  -p 5000:5000 \
  --name local-registry \
  --restart=always \
  -v /opt/registry:/var/lib/registry:z \
  registry:2

do8-img2

이렇게 레지스트리가 들어갈 볼륨(/opt/registry)을 만들어 주고
시중에 있는 registry:2 이미지를 받아 5000포트로 실행시킨다.

do8-img3

그 다음 이 레지스트리를 쿠버노드와 젠킨스 컨테이너에서 쓸 수 있도록 해줘야 하는데

이게 무슨 말이냐면 podman push 192.168.56.200:5000/… 이렇게 해 버리면 얘가 기본적으로 HTTPS 연결을 시도해 버린다.

당연히 이건 로컬이니까 HTTPS 따윈 없기 때문에, 마스터/슬레이브 모든 노드와 젠킨스 컨테이너 안에서 아래 명령어를 수행해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
sudo mkdir -p /etc/containers/registries.conf.d

cat <<EOF | sudo tee /etc/containers/registries.conf.d/99-local-registry.conf
[[registry]]
location = "192.168.56.200:5000"
insecure = true
EOF

# 노드만
sudo systemctl restart crio
# 컨테이너만
sudo systemctl restart podman

192.168.56.200:5000 레지스트리는 secure, 즉 HTTPS 연결을 하지 말라는 설정을 추가해 준 것이다.

당연히 설정이 추가되었으니까 컨테이너 런타임(노드)/플랫폼(젠킨스)를 재시작 해주고

이렇게 추가된 로컬 레지스트리는 주소가 192.168.56.200:5000이라 하면

1
2
3
# 로컬호스트에 빌드된 이미지이므로 원격레지 태그 씌워주기
podman tag localhost/(이미지명+버젼) 192.168.56.200:5000/(이미지명+버젼)
podman push 192.168.56.200:5000/(이미지명+버젼)

이렇게 올릴 수 있다.

argocd 설치

이제 yaml파일을 알아서 쿠버에 배포해 주는 argocd를 설치할 시간이다.

마스터 노드에서

1
2
3
kubectl create namespace argocd

kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

그냥 이렇게 해 주면 끝난다…

그 다음

1
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'

이렇게 argocd-server 서비스를 노드포트로 바꿔 포트개방 시켜주고 kubectl get service -n argocd를 하면

do8-img4

아래처럼 나오는데, argocd-server라는 서비스를 찾아 마스터 노드주소:색칠한 포트로 접속하면 된다.
(색칠한 포트는 각각 HTTP, HTTPS 포트를 개방한 노드포트로 로컬이니까 아무거나 들어가도 무방)

do8-img5

이렇게 페이지가 뜨면 정상~
아이디는 admin
초기 비밀번호는
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath="{.data.password}" | base64 -d; echo
이렇게 알아낼 수 있다.

포드에 들어가지 않고도 작업할 수 있도록 아르고 cli도 설치해 주자.

1
2
3
4
5
curl -sSL -o argocd "https://github.com/argoproj/argo-cd/releases/download/${VERSION}/argocd-linux-amd64"
chmod +x argocd
sudo mv argocd /usr/local/bin/argocd

argocd login (마스타노드주소):(노드포트주소) --username admin --password (초기비밀번호)

이렇게 cli를 설치하고 pod와 연결까지 시켜주면 끝!

젠킨스 재설정

지난번에 말 못했던 깃허브 토큰 발급 방법

do8-img6

깃허브 사용자 로고→settingsdeveloperpersonal access tokenstokens(classic)에 들어가(classic-중요)

우측 위 Generate new token 누르고 Generate new token (classic)-꼭 classic으로!-을 선택한 다음

위 사진처럼 repo, admin:org, admin:repo_hook에만 체크하여 발급한 다음 나온 토큰은 어딘가에 백업해 둔다.
(해당 화면을 넘어가면 다시는 못 봐요)

그 다음 지난번처럼 젠킨스의 Credential에 들어가

do8-img7

위 사진처럼 저 password에 아까 발급받은 토큰을 넣으면 된다.

헬름차트

헬름 차트는 config 변경이나 버전 업그레이드 등이 있을 때 일일이 쿠버 배포용 yaml파일을 수정할 필요 없이,
변수 설정딸깍만으로 yaml을 만들어주는 좋은 기능이다.

이걸 활용하면 서비스마다 배포 yaml을 만들 필요가 없을 뿐더러,
개발/운영환경 yaml도 나누지 않아도 되는 등 활용 가치가 무궁무진하다.

yaml 관리만을 위한 깃허브 레포지토리를 하나 만들어 주자.
그 레포지토리는 아래와 같이 생겼는데…

do8-img8

본인은 이런 구조로 만들었다.
helm_chart 내의 asset은 msa 서비스 이름이고(앞으로 더 늘어날 예정),
그 안에 쿠버에 배포할 yaml이 있는 templates 폴더,
메타데이타가 들어 있는 chart, 변수를 지정하는 values- yaml이 있다.

이 service가 어떻게 들어 있는지 예로 들면

1
2
3
4
5
6
7
8
9
10
11
12
kind: Service
metadata:
  name: -service
spec:
  type: 
  selector:
    app: 
  ports:
    - protocol: TCP
      port: 
      targetPort: 
      nodePort: 

이렇게 되어 있는데, 보시다시피 Chart와 Values yaml파일로부터 변수로 가져오는 것을 알 수 있다.

본인의 Chart.yaml은

1
2
3
4
5
6
apiVersion: v1
name: backend-asset
description: Backend asset microservice
type: application
version: 0.1.0
appVersion: "1.0.0"

values-dev.yaml은

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
replicaCount: 1

image:
  repository: 192.168.56.200:5000/backend_asset
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: NodePort
  port: 8080
  nodePort: 30008

containerPort: 8080

resources: {}

deployment.yaml은

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apps/v1
kind: Deployment
metadata:
  name: -deployment
spec:
  replicas: 
  selector:
    matchLabels:
      app: 
  template:
    metadata:
      labels:
        app: 
    spec:
      containers:
        - name: 
          image: ":"
          imagePullPolicy: 
          ports:
            - containerPort: 

이렇게 구성되어 있다.
그냥 말 그대로 value만 건들어 주는 것으로 개발/운영환경에 따라 yaml을 자동 설정할 수 있는 것이다!
(왜 values-dev가 아닌 Values인지는 조금 있다 argocd 부분에서)

아무튼 본인은 이 구조를 Backend_Manifests이라는 이름으로 레포지토리화했다.

젠킨스파일 만들기

당연히 배포 구조가 바뀌었으니 젠킨스파일도 수정해 준다.

#!/usr/bin/env groovy
def APP_NAME
def APP_VERSION
def DOCKER_IMAGE_NAME

pipeline {
    agent any

    environment {
        REGISTRY_HOST = "아까 만든 레지스트리 주소(포트 포함)"
        USER_EMAIL = '내 깃허브 이메일'
        USER_ID = '내 깃허브 사용자명'
        SERVICE_NAME = 'msa 서비스명'

    }

    tools {
        gradle 'Gradle 8.14.2' // 젠킨스 Tools의 Gradle 이름
        jdk 'OpenJDK 17' // 젠킨스 Tools의 JDK 이름
    }

    stages {
        stage('Set Version') {
            steps {
                script {
                    APP_NAME = sh (
                            script: "gradle -q getAppName",
                            returnStdout: true
                    ).trim()
                    APP_VERSION = sh (
                            script: "gradle -q getAppVersion",
                            returnStdout: true
                    ).trim()

                    DOCKER_IMAGE_NAME = "${REGISTRY_HOST}/${APP_NAME}:${APP_VERSION}"

                    sh "echo IMAGE_NAME is ${APP_NAME}"
                    sh "echo IMAGE_VERSION is ${APP_VERSION}"
                    sh "echo DOCKER_IMAGE_NAME is ${DOCKER_IMAGE_NAME}"
                }
            }
        }

        stage('Checkout Dev Branch') {
            steps {
                // Git에서 dev 브랜치의 코드를 가져옵니다.
                checkout scm
            }
        }

        stage('Build Spring Boot App') {
            steps {
                // gradlew 권한부여
                sh 'chmod +x gradlew'
                // 디버그
                // sh 'ls -al src/main/resources/'
                // sh 'cat src/main/resources/application.yaml'
                // Gradlew로 빌드
                sh './gradlew clean build'
            }
        }

        stage('Image Build and Push to Registry') {
            steps {
                // 이미지 빌드
                sh "echo Image building..."
                sh "podman build -t ${DOCKER_IMAGE_NAME} ."
                // 레지스트리 푸쉬
                sh "echo Image pushing to local registry..."
                sh "podman push ${DOCKER_IMAGE_NAME}"
                // 로컬 이미지 제거
                sh "podman rmi -f ${DOCKER_IMAGE_NAME} || true" 
            }
        }

        stage('Update Helm Values') {
            steps{
                script{
                    withCredentials([usernamePassword(
                        credentialsId:'젠킨스에서 만든 크레덴셜 아이디',
                        usernameVariable: 'GIT_USERNAME',
                        passwordVariable: 'GIT_PASSWORD'
                    )]) {
                        def imageRepo = "${REGISTRY_HOST}/${APP_NAME}"
                        def imageTag = "${APP_VERSION}"
                        def MANIFEST_REPO = "https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/헬름 레포 clone 주소에서 https://github.com/ 떼고.git"

                        sh """
                             # Git 사용자 정보 설정(커밋 사용자 명시땜에)
                            git config --global user.email "${USER_EMAIL}"
                            git config --global user.name "${USER_ID}"
                            
                            # 헬름 차트 레포 클론
                            git clone ${MANIFEST_REPO}
                            cd Backend_Manifests

                            # yq를 사용하여 개발 환경의 values 파일 업데이트
                            yq -i '.image.repository = "${imageRepo}"' helm_chart/${SERVICE_NAME}/values-dev.yaml
                            yq -i '.image.tag = "${imageTag}"' helm_chart/${SERVICE_NAME}/values-dev.yaml
                            
                            # 변경 사항 커밋 및 푸시
                            if ! git diff --quiet; then
                              git add helm_chart/${SERVICE_NAME}/values-dev.yaml
                              git commit -m "Update image tag for dev to ${DOCKER_IMAGE_NAME} [skip ci]"
                              git push origin master
                            else
                              echo "No changes to commit."
                            fi
                        """
                    }
                }
            }
        }

        stage('Clean Workspace') {
            steps {
                deleteDir() // workspace 전체 정리
            }
        }
    }
    // 빌드 완료 후
    post {
        // 성공이든, 실패든 항상 수행
        always {
            echo "Cleaning up workspace..."
            deleteDir() // workspace 전체 정리
        }
    }
}

Build Spring Boot App까지는 똑같은데, environment 변수설정이 약간 바뀌었고

Image Build and Push to Registry 여기서 컨테이너에서 직접 이미지를 빌드해 로컬 레지스트리에 올리는 것을 볼 수 있다.

Update Helm Values 이제 여기가 맛도리인데,

1
2
3
4
5
withCredentials([usernamePassword(
                        credentialsId:'젠킨스에서 만든 크레덴셜 아이디',
                        usernameVariable: 'GIT_USERNAME',
                        passwordVariable: 'GIT_PASSWORD'
                    )])

이렇게 깃허브와 연결시킨 후,

1
2
3
def imageRepo = "${REGISTRY_HOST}/${APP_NAME}"
def imageTag = "${APP_VERSION}"
def MANIFEST_REPO = "https://${GIT_USERNAME}:${GIT_PASSWORD}@github.com/헬름 레포 clone 주소에서 https://github.com/ 떼고.git"

원격 레포지토리에 있는 이미지명과 태그(버젼), 헬름레포 주소를 변수로 지정한 다음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 # Git 사용자 정보 설정(커밋 사용자 명시땜에)
git config --global user.email "${USER_EMAIL}"
git config --global user.name "${USER_ID}"

# 헬름 차트 레포 클론
git clone ${MANIFEST_REPO}
cd Backend_Manifests

# yq를 사용하여 개발 환경의 values 파일 업데이트
yq -i '.image.repository = "${imageRepo}"' helm_chart/${SERVICE_NAME}/values-dev.yaml
yq -i '.image.tag = "${imageTag}"' helm_chart/${SERVICE_NAME}/values-dev.yaml

# 변경 사항 커밋 및 푸시
if ! git diff --quiet; then
  git add helm_chart/${SERVICE_NAME}/values-dev.yaml
  git commit -m "Update image tag for dev to ${DOCKER_IMAGE_NAME} [skip ci]"
  git push origin master
else
  echo "No changes to commit."
fi

위의 git config는 해줘야 하는 것이, 커밋을 해야 함으로 누가 커밋했는지를 알려야 하기 때문이다.

아무튼 레포지토리를 클론하여, 들어간 다음

헬름차트의 변수값을 변경하는 yq를 사용하여 values-devimage: repository값과 image: tag값을 바꾼다.

do8-img9 (여기)

그 다음 변경 사항 커밋하고 푸쉬한 다음 (if문으로 커밋할 게 없으면 넘어가)
deleteDir()로 워크스페이스를 초기화시켜 주면 끝!
(레포가 남아있으면 git clone이 안되기 때문, 이미지도 마찬가지로 레지스트리 업로드까지 끝나면 podman rmi -f로 로컬거 날리는 것을 볼 수 있다)

argocd 어플리케이션 생성

젠킨스 파일도 변경했으면, 다시 마스터 노드로 넘어와 아르고cd를 적용시켜야 하는데…

우선 아까 설치한 아르고 CLI를 사용하여

1
2
3
argocd repo add (헬름차트가 들어있는 레포주소).git \
  --username (내 깃허브 사용자명) \
  --password (내 깃허브 토큰)

이렇게 argocd와 github를 연결시켜야 한다.

연결이 되면(Repository '(헬름차트 레포주소).git' added 이렇게 뜬다)
다음과 같이 yaml파일을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: backend-asset-dev
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/헬름 레포 clone 주소에서 https://github.com/ 떼고.git
    targetRevision: master
    path: helm_chart/asset
    helm:
      valueFiles:
        - values-dev.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

여기서

1
2
3
helm:
  valueFiles:
    - values-dev.yaml

여기를 주목하면 되는데 아까 헬름 차트 deployment,service의 .Values가 바로 valueFiles로 설정하는 것이다.

지금은 values-dev.yaml로 설정했기 때문에 .Values.service.type이면 values-dev.service.type이 되겠지?

요점은 헬름 차트 레포의 master 브랜취에 있는 helm_chart/asset 폴더를 찾아,
.Valuesvalues-dev 기준으로 하여 거기 있는 template 폴더 내 yaml들을 빌드하여,
default 네임스페이스에 배포한다는 것이다.

즉 헬름 차트는 꼭 path를 기준으로 Chartvalue yaml 파일, template 폴더와 그 안의 deploymet, service 등 구조로 이루어져야 하는 것이다.

아무튼 이렇게 저장 하고 kubectl apply -f 만든 야믈 파일.yaml -n argocd 이렇게 적용 하면…

do8-img10

아르고cd 웹 콘솔에서 이렇게 추가된 것을 볼 수 있다!
deployment와 service가 잘 돌아가고 있다!

자 이제 테스트 api를 저 노드포트 주소로 날려 보면…

do8-img11

잘 날아간다!!!

마치며

일주일 간의 삽질 끝에 드디어 로컬 쿠버 배포 환경을 완성하게 되었다.

인프라 업무는 삽질이 매우매우 많지만, 이렇게 하면서 하나 하나 알아가게 되고, 서비스가 작동될 때의 성취감이란 말할 것도 없다.

그래서인지 이렇게 기록해 놓는 것이 매우 중요하다 생각한다…

다음 시간에는 db, kafka 등을 설치하여 개발 환경을 마무리지어보고, 그것도 끝나면 클라우드 운영 환경 설정으로 넘어가 보자!

This post is licensed under CC BY 4.0 by the author.