Post

최종 프로젝트 인프라 구축⑦-최종 로컬 환경 구성, 각종 버그픽스

최종 프로젝트 인프라 구축⑦-최종 로컬 환경 구성, 각종 버그픽스

로컬 구성 마무리

겪은 오류들

부트로더 오류

드디어 로컬 구성을 마무리했다!

그리고 서버를 개인 노트북에서 공용 노트북으로 옮겼는데,
개인 노트북에서는 virtualbox를 사용했는데, 공용 노트북은 hyper-v 기반이여서 고생이 많았다.

단순히 가상하드 확장자를 변환하고 hyper-v에다 이식하면 끝날 줄 알았는데, 계속 패닉 모드에 진입한 거였다.

해결법은 rescue 모드로 들어간 다음..(부트로더 선택할 때 아래거)

1
2
3
4
5
6
7
8
9
lvm vgscan
lvm vgchange -ay
lvs

mkdir /mnt/sysroot
mount /dev/mapper/rl-root /mnt/sysroot

grub2-mkconfig -o /boot/grub2/grub.cfg
dracut -f -v

이렇게 파일 시스템을 다시 초기화해 주고 부트로더에 반영하는 것으로 해결할 수 있었다.

쿠버 통신 오류

do9-img1

서버를 옮긴 후 노드 간 통신이 되지 않아, 확인해 보니 슬레이브 노드에 CNI가 실행되지 않고 있었다.

do9-img2

kubectl logs로 확인해 보니, 이렇게 /etc/cni/net.d에 설정 파일이 없다고만 나왔다.

1
2
3
4
5
6
7
8
9
10
11
# 슬레이브
sudo kubeadm reset -f

sudo rm -rf /etc/cni/net.d
sudo rm -rf /etc/kubernetes/*

# 마스타
sudo kubeadm reset -f

sudo rm -rf /etc/cni/net.d
sudo rm -rf $HOME/.kube/config

이렇게 kubeadm을 초기화한 다음 모든 설정 파일을 다 지우고 다시 클러스터를 만들어 주니까

do9-img3

해결~

DNS 오류

do9-img4

어느 날 클라스터가 뻗어서 다시 실행하니 클러스터의 인터넷 연결이 되지 않았다.

웃긴 것은 ping 8.8.8.8처럼 dns는 가는데, curl www.github.com은 안되는 것이었다.

이걸로 거의 2시간을 싸맸는데, 혹시 네트워크 인터페이스가 초기화된 것 아닐까 해서 열어보니까

do9-img5

DNS란이 원인불명의 이유로 빠져 있었다. DNS 주소를 추가해 주니 해결되었다.

방화벽 문제

do9-img6

가장 기묘한 문제였는데, 마찬가지로 클라스터가 뻗어서 시작했더니 또 통신이 되지 않고 있었다.

do9-img7
do9-img8

골때리는 게 방화벽을 껐고 systemctl에서도 꺼졌다 나왔지만, firewall-cmd에서는 계속 돌아가고 있었다..이래서 통신이 안 됐지

do9-img9

더 골때리는 것은 그냥 VM을 재부팅하니 해결되었던 것이다…
(저렇게 SSL 오류 페이지가 나와야 정상)

보조 프로그램 설치

백엔드 개발에 필요한 프로그램이

  • 마리아db(3306포트)
  • 몽고db(27017포트)
  • 카프카(9092포트)
  • 레디스(6379포트)

이렇게 있었다.
사실 레디스는 카프카로도 갈음할 수 있는데, 다들 각자 자신 있는 기술 스택을 사용하다 보니 이렇게 중복되었다..

아무튼 이것들을 젠킨스 서버(192.168.56.200)에 컨테이너로 설치해주기로 했다.

이번에는 podman-compose로 컨테이너를 올려보기로 했다. 이게 뭐냐면 docker-compose와 똑같은 것인데,
podman run -v ... -p ... 일일이 이렇게 치기 어려우니 yaml 파일로 한 번에 실행 옵션을 지정하는 것이라 할 수 있다.

어떻게 보면 한방에 관리한다는 점에서 컨테이너판 아르고cd라고도 할 수 있겠다.

설치는 그냥 sudo dnf install podman-compose -y 하면 된다.

do9-img10

이렇게 각 프로그램별 디렉토리를 만들어 주고…

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
# podman-compose.yaml
version: "2"
services:
  kafdrop:
    image: obsidiandynamics/kafdrop:3.31.0
    restart: "always"
    ports:
      - "9000:9000"
    environment:
      KAFKA_BROKERCONNECT: "kafka:29092"
      JVM_OPTS: "-Xms16M -Xmx48M -Xss180K -XX:-TieredCompilation -XX:+UseStringDeduplication -noverify"
    depends_on:
      - "kafka"
  kafka:
    image: obsidiandynamics/kafka
    restart: "always"
    ports:
      - "2181:2181"
      - "9092:9092"
    environment:
      KAFKA_LISTENERS: "INTERNAL://:29092,EXTERNAL://:9092"
      KAFKA_ADVERTISED_LISTENERS: "INTERNAL://kafka:29092,EXTERNAL://192.168.35.155:9092"
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT"
      KAFKA_INTER_BROKER_LISTENER_NAME: "INTERNAL"
      KAFKA_ZOOKEEPER_SESSION_TIMEOUT: "6000"
      KAFKA_RESTART_ATTEMPTS: "10"
      KAFKA_RESTART_DELAY: "5"
      ZOOKEEPER_AUTOPURGE_PURGE_INTERVAL: "0"

카프카를 예시로 들면 이렇게 작성하였다.
podman-compose는 service:로 여러 컨테이너를 한 번에 올릴 수도 있다.

자세히 보면 알겠지만, kafka 컨테이너와 이를 시각화하는 kafdrop 컨테이너를 실행하는 것이다.

실행은 yaml이 설치된 디렉토리에서 sudo podman compose up -d, 종료+삭제는 sudo podman compose down으로 하면 된다.

아무튼 이렇게 서비스 별로 실행시키니…

do9-img11

이렇게 프로그램들을 모두 올릴 수 있었다!

몽고db 사용자 설정

몽고db는 타 IP에서 접근할 때 사용자 로그인이 필수인데, 현재 서비스는 192.168.56.100, 몽고는 192.168.56.200에 들어 있으니 로그인 설정을 해 줘야 했다.

몽고db를 실행할 때 만들 수 있는 루트 사용자는 어쩐지 spring boot와의 연동에 사용할 수 없었다..

do9-img12

1
sudo podman exec -it (몽고컨테이너) mongosh -u (루트사용자명) -p (루트비번)

우선 컨테이너에 mongosh로 루트 접속해서…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.createUser(
    {
        user: "사용자",
        pwd: "비번",
        roles: [
            { "role": "readWriteAnyDatabase", "db" : "admin" },
            { "role": "userAdminAnyDatabase", "db" : "admin" },
            { "role": "dbAdminAnydatabase", "db" : "admin" },
            { "role": "clusterAdmin", "db" : "admin" },
            { "role": "restore", "db" : "admin" },
            { "role": "backup", "db" : "admin" }
        ]
    }
)

이렇게 관리자 권한으로 db.createUser()를 실행하면 {ok:1} 응답이 뜬다.
(사진에는 이미 만들어져 있기 때문에 에러가 생긴 것)

만든 사용자는 db.auth("아이디", "비번")으로 확인할 수 있다.

몽고db와 스프링부트 연동

application.yaml에

1
2
3
4
5
6
7
8
9
spring:
  data:
    mongodb:
      host: 몽고 ip
      port: 몽고 포트
      database: 몽고 db명
      authentication-database: test
      username: 아까만든 사용자명
      password: 아까만든 비번

이렇게 지정해 주면 된다.
참고로 authentication-database는 무조건 test로 해 줘야 한다.

서비스 배포

지금 만들고 있는 것은 MSA 서비스니까 지난번에 했던 노가다를 모든 서비스에 해줘야 했다.

본인이 8개에 달하는 서비스를 하기에는 시간과 예산이 부족하여, 개발자 팀원들에게 매뉴얼을 만들어 주었다.

백엔드 개발자 안내문

각자 레포지토리 루트에

1
2
3
4
5
FROM docker.io/amazoncorretto:17
VOLUME /app
EXPOSE (서비스별 포트 번호)
COPY build/libs/*.jar /app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]

이걸 Containerfile 이라는 이름으로, 참고로 포트는

  • 게이트웨이 8080
  • 유저 8081
  • 에셋 8082
  • 프로덕트 8083
  • 에스크로 8084
  • 블록체인 8085
  • 마켓 8086
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/usr/bin/env groovy
def APP_NAME
def APP_VERSION
def DOCKER_IMAGE_NAME

pipeline {
    agent any

    environment {
        REGISTRY_HOST = "192.168.56.200:5000"
        USER_EMAIL = '(검열됨)'
        USER_ID = '(검열됨)'
        SERVICE_NAME = '(서비스 이름: product, market 등...)'

    }

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

    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'
                // 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:'github-credential',
                        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/(검열됨)/Backend_Manifests.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 전체 정리
        }
    }
}

이걸 Jenkinsfile 이라는 이름으로 넣어 주세요.

Containerfile의 EXPOSE 항목, Jenkinsfile의 SERVICE_NAME 항목도 담당 서비스에 맞게 수정하는 것 잊지 말아주세요~ (주의사항: Backend_product → product 이런 식으로 소문자로 적어 주세요)

그리고 루트에 있는 build.gradle 파일에

1
2
3
4
5
6
7
8
tasks.register('getAppName') {
  doLast {
    println "${rootProject.name}" }
}
tasks.register('getAppVersion') {
  doLast {
    println "${project.version}" }
}

맨 아래에 이렇게 추가해 주고

do9-img14

version에 -SNAPSHOT 있으면 없애 주세요.

do9-img15

이렇게 되어 있으면 됩니다

do9-img16

settings.gradle의 rootProject.name 값에 대문자가 있으면 안 됩니다. 소문자로 바꿔주세요

그리고 resources 폴더에 application-dev.yaml 화일을 만들어

1
2
3
4
5
spring:
  config:
    import:
      - file:/etc/config/application-dev.yaml
      - file:/etc/secret/application-secret.yaml

이거 꼭 넣어주세요!

또한 application-local.yaml 화일 내용도 알려주시기 바랍니다

do9-img17

test 폴더에 있는 @Test 어노테이션 붙은 함수도 주석처리 해주세요 (이게 있으면 빌드가 되지 않습니다)

그리고 각자 레포지토리에 dev 브랜취 만들어 주세요.

커밋하고 푸쉬하는 등의 작업을 dev 브랜취에서 하시면 됩니다.

do9-img18

또한 레포지토리 SettingsWebhooksAdd Webhook 하신 다음

do9-img19

이렇게 설정하고 저장해 주세요~

payload url은 서버(노트북)이 꺼질 때마다 바뀌어져 바뀔 때마다 공지하겠습니다.

번거로우시겠지만 그때마다 바꿔 주시길 바랍니다.

application.yaml은 따로 dev, local 구분할 필요 없이 그냥 자기 컴퓨터에서 하실 거 하나만 만드셔도 됩니다.

개발 환경(로컬서버)와 운영 환경(클라우드)의 application.yaml은

do9-img20

여기 레포에서 configmapsecret으로 관리하고 있습니다!

자바스크립트 배포

특이한 점은 서비스 하나는 spring boot가 아닌 자바스크립트로 배포 중이었다.

do9-img21

그래서 젠킨스에 NodeJS 플러그인을 깔아 준 다음

do9-img22

Tools에 노드JS 버전 설정을 해 주고 젠킨스 파일을

tools {
    nodejs 'NodeJS 20.19.2'
}

stages {

    ...

    stage('Set Version') {
        agent any
        steps {
            script {
                echo '도커 이미지 이름과 태그를 동적으로 설정합니다...'
    
                APP_NAME = sh(
                    script: "node -p \"require('./package.json').name\"",
                    returnStdout: true
                ).trim()

                APP_VERSION = sh(
                    script: "node -p \"require('./package.json').version\"",
                    returnStdout: true
                ).trim()
    
                DOCKER_IMAGE_NAME_WITH_VER = "${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_WITH_VER is ${DOCKER_IMAGE_NAME_WITH_VER}"
            }
        }
    }
    ...
}

tools의 gradle 등을 nodejs로 바꿈과 동시에 Set Version을 위와 같이 바꿔 주었다.

테스트

do9-img24

do9-img23

현재 이렇게 서비스들이 올라 왔고, 개발 환경에서 단위테스트 중이다!

개발자들이 로컬에서 코드 변경 시 build.gradle의 버전을 바꿔 dev 브랜취 푸쉬하면 개발 환경에 바로 디플로이가 된다!

do9-img25

한 가지 옥에 티로는 프로퍼티(application.yaml)는 수동으로 관리해 줘야 하고, 아르고cd 특성상 3분 주기로 업데이트가 되기 때문에
바로 반영이 필요하면 일일이 sync를 눌러줘야 한다는 점이다.

그런데 확실히 사양이 사양이라(VM당 4코어 8GB RAM) 많이 힘들어하는 모습을 보인다…

다음 주부터는 위 환경을 AWS 클라우드에 구현해보려고 한다.

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