[Project] [3] CI/CD Jenkins 배포 시나리오 / Hashicorp Vault 사용하기

Hayoon·2024년 4월 1일
0

토이 프로젝트를 진행하면서 발생했던 문제에 대한 본인의 생각과 고민을 기록한 글입니다.
기술한 내용이 공식 문서 내용과 상이할 수 있음을 밝힙니다.

현재까지의 CI/CD의 파이프라인은 완성되었다. 하지만 하나의 문제가 있다. 만약 톰캣 애플리케이션이 실행 중에 배포가 될 경우에는 어떻게 될 것인가? 이에 대한 시나리오에 대해 고민하였다.

새로운 버전 배포 시 기존 앱의 동작 여부

  1. 배포 중 기존 앱의 동작:

    기본적으로, 새로운 버전의 애플리케이션을 배포하더라도 기존에 실행 중인 애플리케이션(V1)은 새로운 버전의 애플리케이션으로 대체되기 전까지 계속 실행된다. 즉, 새로운 버전(V2)의 배포 파일을 톰캣의 webapps 디렉토리에 넣거나 배포하는 동안에도 기존 버전(V1)은 서비스 중단 없이 계속해서 서비스를 제공한다.

  2. 새로운 버전의 적용:
    새로운 버전(V2)이 적용되려면, 기존의 애플리케이션을 중단하고 새로운 버전으로 애플리케이션을 재시작해야 한다. 단순히 배포 파일을 교체하는 것만으로는 실행 중인 애플리케이션의 코드가 업데이트되지 않는다.

시나리오

git webhook → Jenkins(Build, Test, Deploy) → Tomcat App Restart → 빌드 후 조치

Tomcat App Restart

해당 과정에서 새로운 버전 배포 시 조치를 해야한다.
파이프라인 과정은 다음과 같다.

stage('restart tomcat') {
    stage('restart tomcat') {
    steps {
        script {
            sshagent(credentials: ['tomcat']) {
                sh '''
                    ssh ec2-user@172.31.41.40 '
                    TOMCAT_PID=$(ps -ef | grep tomcat | grep -v grep | awk '"'"'{print $2}'"'"')
                    if [[ -n $TOMCAT_PID ]]; then
                        echo "Tomcat is running with PID $TOMCAT_PID, stopping..."
                        sudo kill -15 $TOMCAT_PID
                        while ps -p $TOMCAT_PID > /dev/null; do sleep 1; done
                        echo "Tomcat stopped."
                    else
                        echo "Tomcat is not running."
                    fi
                    echo "Starting Tomcat..."
                    cd /opt/apache-tomcat-9.0.86/
                    sudo ./bin/startup.sh
                    echo "Tomcat started."
                '
                '''
            }
        }
    }
}

Script

  1. SSH Agent Plugin 사용: sshagent 플러그인은 Jenkins에서 지정된 SSH 키(credentials ID: 'tomcat')를 사용하여 원격 서버에 안전하게 접속할 수 있다.
    'tomcat'이라는 ID의 SSH 키를 사용하여 ec2-user@172.31.41.40 주소의 EC2 인스턴스에 접속한다.
  2. Tomcat 프로세스 확인 및 종료: 원격 서버에 접속한 후, 현재 실행 중인 Tomcat의 프로세스 ID(PID)를 찾는다. ps -ef 명령어를 사용하여 현재 실행 중인 프로세스 중에서 'tomcat'을 포함하는 프로세스의 PID를 찾는다.
    만약 Tomcat 프로세스가 실행 중이라면(if [[ -n $TOMCAT_PID ]]), 해당 프로세스를 안전하게 종료하기 위해 kill -15 명령어를 사용한다. 그 후, while ps -p $TOMCAT_PID > /dev/null; do sleep 1; done을 통해 실제로 프로세스가 종료될 때까지 기다린다.
  3. Tomcat 재시작: Tomcat 프로세스의 종료가 확인되면, cd /opt/apache-tomcat-9.0.86/ 디렉토리로 이동하여 ./bin/startup.sh 스크립트를 실행하여 Tomcat을 재시작한다.
  • [[ -n $TOMCAT_PID ]]는 TOMCAT_PID 변수가 비어 있지 않은 경우를 검사하는 조건이다.
    [[ ... ]]는 조건 표현식을 나타낸다.
    -n은 문자열의 길이가 0보다 큰지(즉, 비어 있지 않은지)를 검사하는 조건으로 -n $TOMCAT_PID는 $TOMCAT_PID 변수에 값이 할당되어 있고 그 값이 빈 문자열이 아닐 때 true이다.

  • ps -p $TOMCAT_PID는 PID가 $TOMCAT_PID인 프로세스의 정보를 출력하는 명령이다. 이 명령을 사용해서 해당 프로세스가 실행 중인지를 확인한다. > /dev/null은 명령의 출력을 버린다.
    while ...; do sleep 1; done은 조건이 참인 동안 계속해서 반복 실행하는 루프이다. sleep 1은 1초 동안 대기하라는 명령으로 이 루프는 PID가 $TOMCAT_PID인 프로세스가 더 이상 실행 중이지 않을 때까지(즉, ps -p $TOMCAT_PID의 결과가 없을 때까지) 매 1초마다 반복된다.

  • awk '"'"'{print $2}'"'"'는 awk를 사용하여 print $1, print $2, print $3 등과 같이 특정 필드를 선택하여 출력할 수 있다. ps -ef 명령의 출력에서 각 필드는 $1: 사용자 이름, $2: PID, $3: PPID를 나타낸다. 나는 톰캣의 프로세스 ID만 필요하기에 $2를 사용했다.

    추가로, '"'"'{print $2}'"'"' 상당히 난잡하다. 쉘 스크립트에서는 따옴표 내부에서 따옴표를 사용할 때 별도 처리가 필요하다. 쉘은 따옴표 내에서 동일한 종류의 따옴표를 직접 사용할 수 없다. 작은따옴표 안에서 작은따옴표를 사용하려면, 바깥쪽 작은따옴표를 잠시 닫고, 이스케이프하려는 작은따옴표를 삽입한 후, 다시 작은따옴표를 열어야 한다. 즉, 작은따옴표 '를 문자로 사용하기 위해 '"'"'를 사용한다. 첫 번째와 마지막 작은따옴표는 문자열의 시작과 끝을 나타내고, 중간의 '"'는 작은따옴표를 문자열로 포함시킨다.

Pipeline 빌드 결과

Trouble Shooting

Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).[Pipeline]

ssh-agent 단계로 넘어가면 위와 같은 에러가 발생했다. AWS 인바운드 규칙 설정 및 방화벽도 확인하고 Jenkins에 Tomcat CredentialID도 설정했지만 해결되지 않았다.
결론부터 말하면 공캐기 인증방식이 문제였다.

클라이언트(Jenkins)가 서버(Tomcat)에 SSH로 접속하기 위한 절차는 다음과 같다:

  1. 클라이언트 측(Jenkins)에서 공개키/개인키 쌍을 생성한다.
    생성된 개인키(private key)는 클라이언트 측에서 보관한다. 개인키는 절대로 외부에 공개되어서는 안 되며, 클라이언트의 인증에 사용된다.
  2. 생성된 공개키(public key)는 서버 측(Tomcat)에 등록된다. 이 공개키는 서버 내 해당 사용자 계정의 ~/.ssh/authorized_keys 파일에 추가하여 등록한다. 이 파일에 등록된 공개키를 가진 클라이언트만이 서버에 접속할 수 있도록 허용된다.
  3. Jenkins에서 Tomcat 서버에 SSH 접속을 시도할 때, Jenkins는 자신의 개인키를 사용하여 서버에 자신을 인증한다. 서버 측에서는 authorized_keys 파일에 등록된 공개키를 이용해 클라이언트의 접속 요청을 검증하고, 개인키와 짝이 맞는 공개키가 있을 경우 접속을 허용한다.

Server ~/.ssh/authorized_keys

Vault

datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/yunni_bucks_traffic?rewriteBatchedStatements=true
      username: root
      password: 1234
      auto-commit: false
      connection-test-query: SELECT 1
      maximum-pool-size: 40
      pool-name: mysqlM-example-cp
      hibernate:
        ddl-auto: validate
    slaves:
      - name: slave-1
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3307/yunni_bucks_traffic?rewriteBatchedStatements=true
        username: root
        password: 1234
        hibernate:
          ddl-auto: validate

내 SpringBoot yml 파일이다. git에 push하면 그대로 git repository에 공개된다. 어느 누구나 내 DB에 접근해서 데이터를 조작할 수 있다. (이미 당했을 수도 있다.)
Secret Key, Datasource, Token 등 민감한 정보는 암호화해서 별도의 서버에 저장하면 어떨까? 그럼 외부에 노출될 일돌 없고, 권한에 따라 값을 조회할 수도 있어 보다 안전한 환경을 만드는 것이다.

hashicorp 사의 Vault에서 개발된 크로스플랫폼 패스워드 및 인증 관리 시스템이다. 비공개를 원하는 비밀번호, API 키, 토큰 등을 저장하고 관리한다.

설치

$ brew install vault

설정

vi /opt/vault/config/config.hcl
ui = true
api_addr = "http://{Vault IP}:8200"

listener "tcp" {
    address = "0.0.0.0:8200"
    tls_disable = "true"
}

storage "file" {
    path = "/opt/vault/file"
}

Vault 접속을 위한 config 파일 설정이다. AWS EC2 서버에 Vault를 설치했기에 나는 {Vault IP}에 공인 IP를 기입했다.

실행

vault server -config=/opt/vault/config/config.hcl

Download keys를 눌러 json파일을 다운받는다.

Unseal Key Portion에 keys의 값을 넣는다.
Token에 root_token의 값을 넣는다.
접속을 하여 GUI 또는 CLI에서 <K,V>를 저장할 수 있다.

CLI

vault kv put secret/application host=101.101.xxx.xxx port=1024 ssh_user=root ssh_pw=password

GUI

이렇게 json으로 저장된 값은 이제 Spring Cloud를 사용하여 SpringBoot에서 암호값들을 외부에서 주입받아 사용할 수 있다.

Spring에 값 가져오기

Spring 환경 : Gradle, Java version 17 이상
build.gradle에 추가Vault에 <K, V>로 <ssh.host, root>로 <ssh.port, 1024>로 저장되어있다. Vault와 연동 시 바로 Key의 경로를 통해 Value를 가져와 사용할 수 있다.

결과

datasource:
    master:
      auto-commit: false
      connection-test-query: SELECT 1
      maximum-pool-size: 40
      pool-name: mysqlM-example-cp
      hibernate:
        ddl-auto: validate
    slaves:
      - name: slave-1
        hibernate:
          ddl-auto: validate

yml이 매우 간소화되었다.


다음은 Nginx를 두어 톰캣 2대를 리버스 프록시를 활용하여 로드밸런싱을 할 것이다. 단일 톰캣으로 서비스 트래픽을 감당하기에는 벅차다고 생각하여 서버의 scale-out, scale-up을 점진적으로 성능 확인을 통해 서버의 최적화된 환경을 찾아보자.

profile
Junior Developer

0개의 댓글

관련 채용 정보