CS + 인프라

[인프라] CI/CD 구축하기 6탄 : 실전에서는 이렇게 하면 안 된다 - 1~4탄 개선하기(문제점3개 수정)

공대키메라 2026. 10. 25. 15:43

1~4탄에서 미니 PC에 Jenkins를 띄우고, push하면 빌드와 테스트를 거쳐 이미지를 만들고,

HAProxy 뒤의 was1, was2를 순서대로 교체하는 파이프라인을 만들었다.

 

dev는 자동, prod는 버튼으로 나눠서 Continuous Delivery까지 구현했다.

 

그런데 해당 글들을 Claude에게 리뷰받았더니 실전에서 문제가 될 부분을 몇 가지 짚어 줬다.

 

이 글은 그래서 이를 토대로 개선하는 작업을 할 것이다.

 

이번 글에서는 지금 구성을 다시 뜯어보면서, 실무였다면 문제가 됐을 부분을 찾아 하나씩 개선하려고 한다. (ㅠㅠ... 몰랐다고요)


목표

  1. 지금 파이프라인이 실전에서 어떤 문제를 일으킬 수 있는지 이해한다.
  2. 직접 개선하고, 개선됐는지 검증한다.

0. 이번 글에서 다룰 개선점

솔직히 필자가 예~전에 docker에 대해 아무리 공부하고 CI/CD가 그런가보다 했었지만

 

실제로 이를 돌아가고 문제가 없게끔 하는건 정말 다른 이야기이다.

 

여태 1~4탄 글을 기반으로 분석을 해달라고 했는데 문제가 있는 항목은 총 6개였다.

 

1. 무중단 배포의 구멍: stop을 먼저 하면 안 된다!

2. 배포 실패 시 자동 롤백 누락!

3. 두 번의 빌드와 레이어 비효율 발생!

4. docker.sock 마운트는 root 권한과 같다.

5. 이미지의 누적으로 인한 디스크 공간 차지!

6. Poll SCM과 Webhook 체크하기

 

이를 표로 다시 정리해보았다.

 

번호 지금 방식 실전에서의 문제 개선 방향
1 docker stop 먼저 하고 교체 몇 초간 요청이 실패할 수 있음 HAProxy drain 후 교체, redispatch
2 헬스 체크 실패 시 그냥 멈춤 서버 1대가 죽은 채로 남음 이전 이미지로 자동 롤백
3 Jenkins와 Dockerfile에서 각각 빌드 시간 2배, 테스트한 jar와 배포한 jar가 다를 수 있음, layer 구조의 비효율 테스트 통과한 jar를 그대로 이미지에 담기
4 Jenkins에 docker.sock 마운트 Jenkins가 뚫리면 호스트 root 권한 위험 인지, 실전 대안 정리
5 커밋마다 이미지 생성, 삭제 없음 디스크가 계속 참 최근 N개만 유지, 레지스트리
6 Poll SCM 5분 최대 5분 지연, 불필요한 요청 Webhook과 비교, 지금 선택의 이유 설명

 

 

내가 분석을 했다기 보다는 코치코치 캐물어서 문제점에 대해 진단한 것이고 이를 이번 글에서 개선하려고 한다.

 

그래서 이를 내가 했다는듯이 하기에는 애매하고, 정독 후 작업하는 것을 정리하고자 한다.

 

내용에 대해서는 링크로 올려놓겠다. 관심이 있는 사람은 읽어보기를 바라며 이에 대해 필자는 요약을 하도록 하겠다.

 

CI/CD 구축하기 6탄 개선분석 : Claue를 활용해서 분석!

 

CI/CD 구축하기 6탄 개선분석

Editable docs — documents, reports, plans, memos, briefs that people and Claude read and edit together; export to .docx, PDF or Markdown to download or print. Content is written through the Claude Docs connector, not as files.

claude.ai

 

1. 무중단 배포의 구멍 : Stop을 먼저 하면 안 된다.

1.1) 문제점

Spring Boot -> Jenkinsfile.prod 수정 전

// 컨테이너 하나를 새 이미지로 교체하고 헬스체크한다
def deploy(String name, String port) {
    sh """
        docker stop -t 30 ${name} || true
        docker rm ${name} || true
        docker run -d --name ${name} --restart unless-stopped -p 127.0.0.1:${port}:8080 ${env.IMAGE}
        for i in \$(seq 1 30); do
            if docker run --rm --network host curlimages/curl -fs http://127.0.0.1:${port}/actuator/health > /dev/null; then
                sleep 3; exit 0
            fi
            sleep 2
        done
        exit 1
    """
}

pipeline {
    agent any
    triggers { pollSCM('H/5 * * * *') }
    options { disableConcurrentBuilds() }

    stages {
        stage('Build & Test') {
            steps {
                sh 'chmod +x gradlew'
                sh './gradlew clean build'
            }
        }

        // 이미지는 여기서 한 번만 만든다. prod 작업은 이 이미지를 그대로 쓴다
        stage('Docker Build') {
            steps {
                script { env.IMAGE = "myapp:${env.GIT_COMMIT.take(7)}" }
                sh 'docker build -t "$IMAGE" .'
            }
        }

        // was-dev를 만들거나 교체한다. 여기서 끝
        stage('Deploy dev') {
            steps { script { deploy('was-dev', '8181') } }
        }
    }

    post {
        always { junit 'build/test-results/test/*.xml' }
    }
}

 

해당 파일의 핵심을 정리하면 다음과 같다.

 

Spring Boot -> Jenkinsfile.prod 수정 전 요약

// was 하나를 새 이미지로 교체하고 헬스체크한다
def deploy(String name, String port) {
    sh """
        docker stop -t 30 ${name} || true      // (1) 기존 컨테이너를 멈춘다
        docker rm ${name} || true              // (2) 지운다
        docker run -d --name ${name} ...       // (3) 새 버전을 띄운다
        for i in \$(seq 1 30); do              // (4) 헬스 체크 통과까지 기다린다
            ...
        done
        exit 1
    """
}

stage('Deploy was1') { steps { script { deploy('was1', '8081') } } }
stage('Deploy was2') { steps { script { deploy('was2', '8082') } } }

 

문제는 HAProxy가 was1이 죽은 걸 언제 알아채느냐다.

 

HAProxy 공식 문서에 따르면 헬스 체크 간격(inter)의 기본값은 2초이고, 연속 몇 번 실패해야 서버를 빼는지 정하는 fall의 기본값은 3이다. 필자는 inter 1s로 줄여 놨으니, 최악의 경우 약 3초 동안은 HAProxy가 이미 멈춘 was1로 새 요청을 계속 보낸다.

 

실제로 4탄 글에서 무중단 배포 해보겠다고 배포하고 화면을 봤는데 요청이 was1에서 정확하게 2번 실패했었다.

 

결국 무중단 구조는 맞는데, 배포 중 발생하는 요청 실패는 몇 건 발생한 것이다.

 

그러면 이 실패를 겪지않고, 배포중이지 않고 현재 잘 작동하고 있는 서버로 요청을 보내도록 수정해야 한다.

 

1.2) 개선 방안

개선 방안을 요약했다.

 

① HAProxy: was1 DRAIN → 새 요청은 was2만
② was1 처리 중 세션 3 → 1 → 0 확인
③ was1 stop → rm → 새 이미지 run → 헬스 UP
④ HAProxy: was1 READY → 다시 was1, was2 둘 다
→ 다음 stage에서 deploy('was2', '8082')가 같은 순서로 반복

 

결국 was1을 변경한다면 여기로 요청이 오지 않게 완벽하게 정리하고! 나서 was1을 새롭게 올리는 것이다.

 

HAProxy가 load balancing을 해주니 ready, drain, maint 옵션에 대해 이해하고 이를 적용하면 된다.

 

state 트래픽 받음 헬스 체크
ready O O
drain X O
maint X X

 

그러면 Jenkins가 HAProxy에게 명령을 내려줘야 한다.

 

왜? 배포가 언제 되는지 아는건 Jenkins 뿐이기 때문이다.

 

4탄:  Jenkins가 was1 멈춤 → HAProxy가 몇 초 뒤 눈치챔 → 그 사이 요청 실패
6탄:  Jenkins → HAProxy "was1 빼" → HAProxy가 was2로만 보냄 → Jenkins가 was1 멈춤 → 실패 없음

 

그런데 또 읽어보니 HAProxy에 명령을 보내려면 소켓을 추가해서 그 소켓으로만 Jenkins가 명령을 보내도록 해야한단다.

 

연결이 안되면 다른 서버로 보내는것도 추가했다.

 

 

sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy

 

재설정을 했으면 확인하고 다시 켜야한다.

 

실행하니 크게 문제는 없다.

 

 

그리고 Spring Boot에 있는 Jenkinsfile을 다음과 같이 변경하자.

 

Spring Boot -> Jenkinsfile.prod 수정 후

// [A] HAProxy에 명령 한 줄을 보내는 함수 (①, ④에서 사용)
def haproxy(String cmd) {
    sh "echo '${cmd}' | docker run --rm -i --network host alpine/socat stdio tcp4-connect:127.0.0.1:9999"
}

// ② 남은 요청 기다리기: 처리 중인 세션이 0이 될 때까지 최대 30초
def waitDrained(String name) {
    sh """
        for i in \$(seq 1 30); do                       # ②-1 최대 30번 반복
            cur=\$(echo 'show stat' | docker run --rm -i --network host alpine/socat stdio tcp4-connect:127.0.0.1:9999 | awk -F, '\$1=="be_was" && \$2=="${name}" {print \$5}')   # ②-2 처리 중 세션 수 읽기
            [ "\$cur" = "0" ] && break                  # ②-3 0이면 끝
            sleep 1                                     # ②-4 아니면 1초 뒤 다시
        done
    """
}

// ③ 교체: 4탄 deploy()의 내용을 그대로 함수로 옮겼다 (2장 롤백에서 다시 쓴다)
def replace(String name, String port, String image) {
    sh """
        docker stop -t 30 ${name} || true               # ③-1 옛 컨테이너 종료 (SIGTERM, 최대 30초)
        docker rm ${name} || true                       # ③-2 삭제
        docker run -d --name ${name} --restart unless-stopped -p 127.0.0.1:${port}:8080 ${image}   # ③-3 새 버전 실행
        for i in \$(seq 1 30); do                       # ③-4 헬스 체크 (2초 간격, 최대 60초)
            if docker run --rm --network host curlimages/curl -fs http://127.0.0.1:${port}/actuator/health > /dev/null; then
                exit 0
            fi
            sleep 2
        done
        exit 1                                          # ③-5 끝까지 UP이 안 되면 실패
    """
}

// was 하나를 무중단으로 교체한다. 호출 예: deploy('was1', '8081')
def deploy(String name, String port) {
    haproxy("set server be_was/${name} state drain")    // ① drain: 새 요청은 안 받고, 헬스 체크는 계속
    waitDrained(name)                                   // ② 남은 요청 기다리기
    replace(name, port, env.IMAGE)                      // ③ 교체 (4탄과 동일)
    haproxy("set server be_was/${name} state ready")    // ④ ready: 다시 요청 받기
}

 

2. 배포 실패 시 자동 롤백이 필요

2.1) 문제점

문제점을 다음과 같이 요약했다.

이전 버전 was1을 이미 stop, rm 했음
→ 새 버전 was1은 헬스 체크 실패
→ 파이프라인 중단
→ 결과: was1은 고장 난 새 버전, 서비스는 was2 혼자 버팀

 

그렇다면 배포 실패 시 stop, rm을 해도 이전으로 되돌려야 한다.

 

2.2) 개선: 교체 전 버전을 기억해 두고 되돌린다

 

추가할 부분 정리

// haproxy(), waitDrained(), replace()는 1.4와 같다. deploy()만 바뀐다.
def deploy(String name, String port) {
    // 개선2 - 교체 전 이미지를 기억해 둔다 (컨테이너가 없으면 빈 값)
    def prev = sh(script: "docker inspect ${name} --format '{{.Config.Image}}' 2>/dev/null || true",
                  returnStdout: true).trim()

    haproxy("set server be_was/${name} state drain")    // ① drain
    waitDrained(name)                                   // ② 남은 요청 기다리기

    // 개선2 - ③ 교체를 try로 감싸서 실패를 잡는다
    try {
        replace(name, port, env.IMAGE)                  // ③ 교체
    } catch (e) {
        // 개선2 - 새 버전이 실패하면 이전 이미지로 같은 교체를 한 번 더 한다
        if (prev) {
            echo "${name} 새 버전 실패. ${prev} 로 롤백한다"
            replace(name, port, prev)
        }
        haproxy("set server be_was/${name} state ready")
        // 개선2 - 롤백해도 파이프라인은 실패로 표시하고, 다음 서버(was2)로 넘어가지 않는다
        error("${name} 배포 실패 (이전 버전으로 복구함)")
    }

    haproxy("set server be_was/${name} state ready")    // ④ ready
}

 

Jenkinsfile.prod 완성본 (1번 개선 + 2번 개선)

 

1번 내용에 더해서 2가 추가 되었으니 이를 전체적으로 보고자 다 복붙했다.

// =====================================================================
// Jenkinsfile.prod : prod(was1, was2) 무중단 배포
//   개선1 - drain으로 무중단 구멍 막기 (1장, haproxy.cfg의 9999 입구가 전제)
//   개선2 - 배포 실패 시 자동 롤백     (2장)
// =====================================================================


// [A] 개선1 - HAProxy에 명령 한 줄을 보내는 함수 (①, ④에서 사용)
//   Jenkins 컨테이너의 127.0.0.1은 미니 PC가 아니어서 --network host로 socat을 띄운다
def haproxy(String cmd) {
    sh "echo '${cmd}' | docker run --rm -i --network host alpine/socat stdio tcp4-connect:127.0.0.1:9999"
}


// ② 개선1 - 남은 요청 기다리기
//   show stat의 5번째 칸(처리 중 세션 수)을 1초마다 읽어 0이 되면 끝, 최대 30초
def waitDrained(String name) {
    sh """
        for i in \$(seq 1 30); do
            cur=\$(echo 'show stat' | docker run --rm -i --network host alpine/socat stdio tcp4-connect:127.0.0.1:9999 | awk -F, '\$1=="be_was" && \$2=="${name}" {print \$5}')
            [ "\$cur" = "0" ] && break
            sleep 1
        done
    """
}


// ③ 교체: 4탄 deploy()의 내용을 그대로 함수로 옮겼다 (개선2 롤백에서 다시 쓴다)
//   stop → rm → 새 이미지 run → 헬스 체크(2초 간격, 최대 60초), UP이 안 되면 실패
def replace(String name, String port, String image) {
    sh """
        docker stop -t 30 ${name} || true
        docker rm ${name} || true
        docker run -d --name ${name} --restart unless-stopped -p 127.0.0.1:${port}:8080 ${image}
        for i in \$(seq 1 30); do
            if docker run --rm --network host curlimages/curl -fs http://127.0.0.1:${port}/actuator/health > /dev/null; then
                exit 0
            fi
            sleep 2
        done
        exit 1
    """
}


// was 하나를 무중단으로 교체한다. 호출 예: deploy('was1', '8081')
def deploy(String name, String port) {
    // 개선2 - 교체 전 이미지를 기억해 둔다 (컨테이너가 없으면 빈 값)
    def prev = sh(script: "docker inspect ${name} --format '{{.Config.Image}}' 2>/dev/null || true",
                  returnStdout: true).trim()

    haproxy("set server be_was/${name} state drain")    // ① drain
    waitDrained(name)                                   // ② 남은 요청 기다리기

    // 개선2 - ③ 교체를 try로 감싸서 실패를 잡는다
    try {
        replace(name, port, env.IMAGE)                  // ③ 교체
    } catch (e) {
        // 개선2 - 새 버전이 실패하면 이전 이미지로 같은 교체를 한 번 더 한다
        //   (롤백까지 실패하면 여기서 예외로 끝나고 서버는 DRAIN으로 남는다 → 수동 확인)
        if (prev) {
            echo "${name} 새 버전 실패. ${prev} 로 롤백한다"
            replace(name, port, prev)
        }
        haproxy("set server be_was/${name} state ready")
        // 개선2 - 롤백해도 파이프라인은 실패로 표시하고, 다음 서버(was2)로 넘어가지 않는다
        error("${name} 배포 실패 (이전 버전으로 복구함)")
    }

    haproxy("set server be_was/${name} state ready")    // ④ ready
}


// ===== 여기부터는 4탄과 동일 =====
pipeline {
    agent any
    // triggers 없음: push로는 실행되지 않는다. 버튼으로만 실행
    options { disableConcurrentBuilds() }

    parameters {
        string(name: 'IMAGE_TAG', defaultValue: '',
               description: '비워두면 지금 dev(was-dev)에 떠 있는 이미지를 배포. 롤백할 때는 이전 태그를 넣는다')
    }

    stages {
        stage('Resolve image') {
            steps {
                script {
                    if (params.IMAGE_TAG?.trim()) {
                        env.IMAGE = "myapp:${params.IMAGE_TAG.trim()}"
                    } else {
                        env.IMAGE = sh(script: "docker inspect was-dev --format '{{.Config.Image}}'", returnStdout: true).trim()
                    }
                    echo "prod에 배포할 이미지: ${env.IMAGE}"
                }
                sh 'docker image inspect "$IMAGE" > /dev/null'
            }
        }

        stage('Deploy prod was1') {
            steps { script { deploy('was1', '8081') } }
        }
        stage('Deploy prod was2') {
            steps { script { deploy('was2', '8082') } }
        }
    }
}

 

그렇다네... 솔직히 문법 잘 모르겠다. 하지만 뭐 내부에 함수를 만들어서 이미지를 삭제하기 전에 다른데다가 넣어놓고

 

성공하면 교체하는데, 실패하면 이전걸 그대로 불러온다. 

 

이렇게만 큰 틀을 기억하고 자세한 명령어는 AI에게 요청하는게 맞는 것 같다.

 

2장 개선점 요약!

  • 롤백해도 파이프라인은 실패로 표시한다. 조용히 복구만 하면 아무도 새 버전이 깨졌다는 걸 모른다.
  • was1에서 실패하면 was2 단계로 넘어가지 않는다. error()가 파이프라인을 멈추니 was2는 아예 건드리지 않는다.

 

3. 빌드를 2번이나 하며 레이어가 비효율적임

3.1) 문제점 파악

이게 무슨 말인가?

 

다시 용어를 짚고 넘어가자.

 

빌드는 소스 코드를 실행할 수 있는 산출물(jar)로 만드는 행위다. Jenkins에서는 Build & Test 단계의 ./gradlew build가 이 일을 한다. 그런데 3탄 Dockerfile은 이미지를 만들면서 ./gradlew bootJar로 한 번 더 빌드하고 있었다. 

 

레이어는 도커 이미지를 이루는 층으로, Dockerfile 명령어 하나가 만든 파일 변경 묶음이다.

한 번 만들어지면 바뀌지 않아서, 내용이 같은 레이어는 다음 이미지 빌드 때 다시 만들지 않고 재사용된다.

 

그런데 필자는 어느 레이어가 비효율적인지는 솔직히 잘 모르겠어서 정확히 어디인지를 확인해보았다.

 

이를 정리하면 다음과 같다.

 

문제점 요약 정리

Jenkins  'Build & Test' : ./gradlew clean build   → 컴파일 + 테스트 + jar 생성
Docker   1단계(build)    : ./gradlew bootJar       → 컴파일 + jar 생성 (한 번 더)

 

필자는 Jenkins가 만든 jar를 도커 이미지 생성 단계에서 바로 담도록 하려한다.

 

현재 시간이 두배가 걸리고, 테스트한 jar와 배포한 jar가 다른 파일이다. 

마지막으로 코드 1줄만 고쳐서 수십 MB레이어가 새로 생긴다.

 

3.2) 개선하기 - Jenkins가 만든 jar를 그대로 담고, 레이어로 나누기

수정 전의 Dockerfile을 확인하자.

 

Dockerfile - 수정 전

# ===== 1단계: 빌드 =====
# JDK와 Gradle로 jar를 만든다. 이 단계의 내용은 최종 이미지에 남지 않는다
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app

# 의존성 정의 파일만 먼저 복사한다.
# 소스만 바뀌고 build.gradle이 그대로면 이 레이어는 캐시를 재사용해 빌드가 빨라진다
COPY gradlew settings.gradle build.gradle ./
COPY gradle gradle
RUN chmod +x gradlew && ./gradlew dependencies --no-daemon

COPY src src
RUN ./gradlew bootJar --no-daemon

# ===== 2단계: 실행 =====
# JRE만 있는 작은 베이스 이미지
FROM eclipse-temurin:21-jre
WORKDIR /app

# root가 아닌 전용 사용자로 실행한다
RUN useradd -r app

# 1단계 결과물에서 실행용 jar만 가져온다.
# build/libs에는 실행용 jar와 -plain.jar가 생기므로 *-SNAPSHOT.jar로 실행용만 고른다
COPY --from=build /app/build/libs/*-SNAPSHOT.jar app.jar

USER app
EXPOSE 8080

# 배열 형식이어야 java가 PID 1이 되어 종료 신호(SIGTERM)를 직접 받는다
ENTRYPOINT ["java", "-jar", "app.jar"]

 

Dockerfile을 수정하자.

Dockerfile - 수정 후

# ===== 1단계: jar를 레이어별로 풀기 =====
# 개선3 - 기존의 "JDK + Gradle 빌드" 단계를 없앴다. (3.1의 문제 1, 2)
#   Jenkins ① Build & Test에서 이미 빌드와 테스트를 끝낸 jar가 workspace의 build/libs에 있다.
#   그래서 JDK 대신 JRE만 쓰고, 그 jar를 가져와 레이어별로 푸는 일만 한다.
FROM eclipse-temurin:21-jre AS extract
WORKDIR /builder

# 개선3 - Jenkins가 만든 실행용 jar를 그대로 가져온다
#   (-plain.jar는 이름이 -SNAPSHOT-plain.jar라서 이 패턴에 걸리지 않는다)
COPY build/libs/*-SNAPSHOT.jar application.jar

# 개선3 - jar를 4개 레이어로 푼다 (3.1의 문제 3)
#   jar 안의 layers.idx(Spring Boot가 기본으로 넣어 줌)를 보고
#   dependencies / spring-boot-loader / snapshot-dependencies / application 폴더로 나눈다
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted


# ===== 2단계: 실행 =====
# JRE만 있는 작은 베이스 이미지 (기존과 동일)
FROM eclipse-temurin:21-jre
WORKDIR /application

# root가 아닌 전용 사용자로 실행한다 (기존과 동일)
RUN useradd -r app

# 개선3 - 안 바뀌는 것(라이브러리)부터 자주 바뀌는 것(내 코드) 순서로 나눠 COPY한다
#   COPY 하나가 레이어 하나라서, 코드만 고치면 마지막 application 레이어만 새로 생긴다
COPY --from=extract /builder/extracted/dependencies/ ./
COPY --from=extract /builder/extracted/spring-boot-loader/ ./
COPY --from=extract /builder/extracted/snapshot-dependencies/ ./
COPY --from=extract /builder/extracted/application/ ./

USER app
EXPOSE 8080

# 배열 형식이어야 java가 PID 1이 되어 종료 신호(SIGTERM)를 직접 받는다 (기존과 동일)
# 개선3 - 실행 파일 이름만 app.jar → application.jar (extract가 만든 이름)
ENTRYPOINT ["java", "-jar", "application.jar"]

 

.dockerignore 추가

# .dockerignore
.git
.gradle
src
# build/libs의 jar만 필요하므로 나머지 build 산출물은 제외
build/*
!build/libs

 

 

먼저 확인할 점이 있다.

 

내 PC에서 .\gradlew bootJar 후 java -Djarmode=tools -jar build\libs\cicd-lab-0.0.1-SNAPSHOT.jar list-layers를 쳐서 4개 레이어 이름이 나오는지 본다. 안 나오면 이 Dockerfile의 extract 단계가 실패한다.

 

또한, 이제 Dockerfile 혼자서는 이미지를 못 만든다. jar가 이미 있어야 하므로, 내 PC에서 docker build를 직접 할 때도 .\gradlew bootJar를 먼저 해야 한다.

 

Jenkins에서는 ① Build & Test가 항상 ② Docker Build보다 먼저라 문제없다.

 

 


우선 3개의 개선 사항에 대해서 확인했다.

 

이를 표로 정리해보았다.

 

개선 무엇이 문제였나 어디를 고쳤나 어떻게 바뀌었나
1. 무중단 배포의 구멍 docker stop 순간 HAProxy가 몇 초 늦게 알아채서 그 사이 요청이 실패할 수 있었다 haproxy.cfg (관리 입구 9999, redispatch),
Jenkinsfile.prod의 deploy()
멈추기 전에 HAProxy에서 빼고(drain), 교체하고, 다시 넣는다(ready)
2. 배포 실패 시 자동 롤백 새 버전이 헬스 체크에 실패하면 서버 1대가 고장 난 채로 남았다 Jenkinsfile.prod의 deploy()
→ replace() 분리
실패하면 이전 이미지로 되돌리고, 파이프라인은 실패로 표시한다
3. 빌드 2번 + 레이어 비효율 Jenkins와 Dockerfile에서 각각 빌드했고, 코드 1줄만 바꿔도 수십 MB 레이어가 새로 생겼다 Dockerfile, .dockerignore Jenkins가 만든 jar를 그대로 담고, 4개 레이어로 나눈다

 

결국 필자가 HAProxy가 서버 상태를 언제 알게 되는지, 배포가 실패했을 때 어떻게 되돌릴지, Docker 이미지가 레이어로 쌓인다는 것을 제대로 이해하지 못했기 때문에 이런 문제들이 생겼다.

 

사실 이번에는 원하는 작업도, 개선점도 전부 AI에게 요청했다. 1~4에서 만든 걸 검증해 달라고 했을 뿐인데 이렇게 많은 문제가 나올 줄은 몰랐다. 원래라면 공식 문서를 읽고 여러 블로그를 찾아보면서 직접 정리했어야 하는 과정이고, AI가 없었다면 얼마나 걸렸을지 가늠이 안 된다.

 

덕분에 그 과정이 엄청나게 짧아졌지만, 그만큼 기대지 않으려고 한다. 큰 그림을 머릿속에 넣어 두고 "아, 이 부분은 이런 게 문제가 될 수 있겠다" 하고 먼저 의심할 수 있는 엔지니어로서의 감각을 기르는 게 맞는 것 같다.

 

그리고 이번 글은 개선점을 적용해 본 것까지다. 실제로 잘 동작하는지는 아직 확인하지 않았다. 다음 글에서는 먼저 이 개선이 정말 효과가 있는지 검증하고, 그다음에 남은 4, 5, 6번 문제를 처리하도록 하겠다.

 

AI시대에서 가장 필요한 덕목은 이렇게 주는것을 그대로 맹신하지않고 계속 의심하고 deep 하게 명령을 내리고 구조화하는게 필요하지 않을까 생각하며... 이번 주말의 글을 마친다.