글 작성자: beaniejoy

Jenkins와 Ansible을 이용해 Spring Boot application을 빌드하고 서버에 배포하는 모든 과정(CI/CD)을 자동화해보는 내용을 정리해보려고 합니다. 이 글을 통해 Jenkins와 Ansible을 통해 빌드, 배포 과정을 처음 만들어보려는 분들에게 작게나마 도움이 되셨으면 좋겠습니다.

본론으로 바로 들어가보겠습니다.

 

📌 1. 준비물

CI/CD 프로세르를 만들어보기 전에 준비물이 있는데요. Spring Boot 프로젝트, Jenkins가 설치된 서버(로컬, 클라우드 서버 상관없습니다), Ansible이 필요한데요. 지난 저의 게시글 중에 Jenkins, Ansible 간단하게 구성해보는 글이 있으니 참고하셔도 좋을 것 같습니다.

또한 배포할 대상이 되는 간단한 Spring Boot 프로젝트도 하나 있으면 좋을 것 같습니다.

추가로 저는 Spring Boot에 vault secret을 연동을 한 상태입니다. 그래서 Jenins pipeline에 vault secret 정보를 가져오는 내용도 있는데요. vault 서버도 하나 있으면 좋을 것 같지만 선택사항이라 프로젝트에 vault를 연동하고 있지 않다면 넘어가셔도 좋습니다.

- Ansible 구성
https://beaniejoy.tistory.com/101

 

[Jenkins] Ansible plugin 사용해보기(ansible 설치부터 pipeline까지 작업)

지난 게시글 중에 Jenkins 서버를 설치하고 Spring boot project 대상으로 간단하게 테스트, 빌드까지 해보는 Jenkins pipeline을 적용해보는 글이 있었습니다. 이번 게시글을 읽기 전에 먼저 읽어보시는 것

beaniejoy.tistory.com

- Jenkins 구성
https://beaniejoy.tistory.com/95

 

[Jenkins] Lightsail에 Jenkins server 구축해보고 Spring project 빌드해보기

목적 - AWS Lightsail을 통해 생성한 instance에 jenkins server 구축한 내용 기록용 목표 - AWS Lightsail에 띄운 instance에 jenkins server 띄우기 - jenkins server 기본적인 설정 - jenkins item 생성 후 spring project build 해

beaniejoy.tistory.com

- Vault 구성
https://beaniejoy.tistory.com/100 

 

Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기)

Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을

beaniejoy.tistory.com

 

📌 2. 배포 전 빌드 순서 Overview

빌드 순서는 다음과 같습니다.

  • DB Validate (flyway)
  • Test
  • Build
  • Deploy (ssh publisher)
  • Run Application (by ansible playbook)

 

2-1. Flyway 사용한 DB Validate

가장 첫번째로 flyway를 사용한 DB Validate를 진행합니다. 이 과정은 제가 빌드 시작단계에 적용한 단계인데요.

과거 실무에서 일하면서 실환경 DB에 테이블 변경(DDL) 내용이 적용이 안되었는데 배포가 진행되었던 경험이 있는데요.이것이 생각이 나서 개인 프로젝트에서 DB Migration 용도로 사용하고 있는 flyway를 통해 DB에 제대로 변경분이 모두 적용된 상태인지 체크하는 validate 과정을 넣게 되었습니다.

이런 식으로 빌드 및 테스트 전에 DB 확인부터 하니 안전성도 챙기고 쓸데 없는 리소스 낭비도 줄이고 꽤 괜찮게 사용하고 있습니다. 이러한 부분도 고려해봐도 좋을 거 같네요.
(리소스 낭비를 줄인다는 것은 실컷 테스트 다 수행하고 빌드, 배포까지 잘 이루어졌는데 애플리케이션 실행단계에서 DB 내용이 달라서 오류가 발생한다면 그 이전의 모든 작업들이 헛수고가 되어버리는 것 같았습니다. 사전에 이러한 일들을 방지한다는 의미에서 리소스 낭비를 줄인다고 표현을 했습니다.)

여기서는 Jenkins의 Flyway Runner 플러그인을 적용해보려 합니다.

제가 쓴 게시글 중에 Jenkins - flyway runner 플러그인 적용해보는 글이 있는데요. 참고하시면 좋을 것 같습니다.
https://beaniejoy.tistory.com/98

 

Jenkins의 flywayrunner plugin을 통해 DB migration 자동화하기(jenkins pipeline)

목표 - DB migration 용도의 Jenkinsfile 작성해보기 - Jenkins pipeline을 통해 flyway migration 자동화 적용 - Jenkins에서 item에 버튼 하나 누르면 알아서 migration 작업 진행하도록 적용 - DB 테이블에 대한 migration

beaniejoy.tistory.com

 

2-2. Test 및 Build

DB까지 잘 적용된 것을 확인했으면 그 다음에 테스트와 빌드를 수행하게 됩니다. 이를 통해 서버에 배포할 jar 파일을 생성하게 되는데요. Spring Boot 프로젝트에 있는 gradle wrapper를 통해 수행하게 됩니다.

 

2-3. Deploy

테스트와 빌드를 성공적으로 마치게 되면 jar 파일이 생성되는데요. 해당 파일은 실행가능한 파일이기 때문에 java 명령어를 통해 바로 애플리케이션을 동작하게 할 수 있습니다. 그 전에 애플리케이션을 돌릴 서버에 해당 jar파일을 건네줘야 하는데요. Deploy 단계에서 서버로 ssh를 이용해 파일 전송을 하게 됩니다.

여기서 Jenkins의 Publish over SSH라는 플러그인을 적용해서 사용해보려 합니다.

 

2-4. Run Application

배포한 애플리케이션 jar 파일을 절차에 따라 실행을 단계입니다.
애플리케이션 실행 단계에서는 Ansible을 적극 활용하려 합니다. Jenkins에서도 Ansible plugin을 제공하는데 해당 플러그인을 적용해서 ansible playbook으로 애플리케이션 실행을 해보겠습니다.

Jenkins에 Ansible plugin 적용해보는 내용은 이전 게시글에서 확인할 수 있는데요. 이것도 역시 위의 준비물에서 Ansible 링크 드렸습니다. 참고하시면 좋을 것 같네요.

 

📌 3. pipeline 구성하기

pipeline은 여러 개의 stage를 기반으로 순차적으로 작업이 이루어지게 됩니다. 위에서 소개한 빌드 순서를 기반으로 jenkins pipeline의 stage들 구성해보려 합니다.
프로젝트 빌드, 배포를 위한 stage를 위에서 간략히 소개했던 것처럼, 개인적으로 CI/CD를 구성해보고자 할 때 먼저 전체적인 stage들을 구상해보고 pipeline script를 작성하면 수월하게 하실 수 있을 거라 생각합니다.

 

3-1. Inititalize 단계(필요한 변수 선언 및 할당)

각 stage 단계에서 필요한 변수들을 선언하고 할당하는 작업을 진행합니다. 예를 들어 Spring Boot 프로젝트를 test, build를 하기 위한 MODULE_NAME, 그리고 flyway를 수행하기 위한 conf 파일이나 ansible을 수행하기 위한 playbook, inventory hosts 파일 위치나 이름 등등 각 stage에서 필요한 변수들을 선언하고 할당해줍니다.

stage('Init') {
    steps {
        script {
            PROJECT_PROFILE = 'prod'
            PROJECT_NAME = 'service-api'
            PARENT_MODULE_NAME = 'app'
            MODULE_NAME = "${PARENT_MODULE_NAME}:${PROJECT_NAME}"

            // flyway migration
            MIGRATION_WORKSPACE = "${WORKSPACE}/flyway"

            // ansible playbook
            ANSIBLE_INVENTORY = "${HOME}/ansible/inventory"
            ANSIBLE_PLAYBOOK = "${WORKSPACE}/scripts/deploy/${PROJECT_NAME}.yml"
        }
    }
}

또한 변수를 위의 내용처럼 직접 할당하는 것들도 있지만 vault secret 혹은 jenkins credentials 같은 곳에서 필요한 정보들을 가져와야 하는 경우도 있을 수 있습니다. 저 같은 경우 vault secret에서 필요한 정보들이 있어서 직접 가져와 값을 할당했습니다.

def secrets = [
    [
        path: "secret/dongne/${PROJECT_PROFILE}",
        engineVersion: 2,
        secretValues: [
            [envVar: 'jdbcUrl', vaultKey: 'spring.datasource.hikari.jdbc-url']
        ]
    ],
    [
        path: "vault/config",
        engineVersion: 2,
        secretValues: [
            [envVar: 'vaultAddr', vaultKey: 'VAULT_ADDR'],
            [envVar: 'vaultToken', vaultKey: 'VAULT_TOKEN']
        ]
    ],
]

withVault([
    vaultSecrets: secrets,
    configuration: [
        engineVersion: 2,
        timeout: 10
    ]
]) {
    JDBC_URL = "${jdbcUrl}"
    VAULT_ADDR = "${vaultAddr}"
    VAULT_TOKEN = "${vaultToken}"
}

Jenkins pipeline에 vault secret 내용을 가져오려면 vault plugin(HashiCorp Vault)이 따로 필요한데요. 연동하는 방법은 아주 간단한데 아직 관련 글을 작성해보진 않았네요. Jenkins - Vault 연동 방법에 대해 간략하게라도 글 작성 완료되면 여기에도 업데이트를 해두겠습니다.
(여기서는 vault가 중요한 내용이 아니라 이런 게 있구나라고만 생각하시고 가볍게 넘어가면 좋을 것 같습니다.)

위의 내용대로 직접 할당하든 vault나 jenkins credentials에서 값을 가져와 할당하든 필요한 정보들을 선언하고 할당하는 내용이 Initialize stage에서 이루어진다고 보면 될 것 같습니다.

 

3-2. DB Validate 단계

flyway migration 내역이 실제 DB에 잘 적용이 되어있는지 체크하는 stage입니다.

stage('DB Validate') {
    steps {
        flywayrunner installationName: 'flyway-jenkins',
                flywayCommand: 'info validate',
                commandLineArgs: "-configFiles=${MIGRATION_WORKSPACE}/flyway-${PROJECT_PROFILE}.conf",
                credentialsId: "VAULT_DB_CONNECTION_${PROJECT_PROFILE.toString().toUpperCase()}",
                url: "${JDBC_URL}",
                locations: "filesystem:${MIGRATION_WORKSPACE}/migration"
    }
}

 

(flywayrunner에 대한 pipeline syntax는 위에 제가 링크드린 flyway runner 관련 글을 참고하시면 됩니다.)

gradle에서도 따로 plugin을 제공해주고 있어서 gradle wrapper를 통해 flyway 관련 작업을 할 수 있는데요. gradle로 flyway 진행하는 것이 flyway를 직접 설치해서 수행하는 것보다 빌드 진행속도가 조금 느리게 느껴졌습니다.

그래서 저 같은 경우 flyway runner라는 jenkins plugin을 이용해 서버에 설치된 flyway 패키지와 연동해서 사용하고 있습니다. 물론 각각에 장단점이 있는 것 같습니다.

gradle plugin을 통한 flyway 작업은 따로 jenkins에 플러그인 설치나 패키지 설치 및 연동 작업을 하지 않고도 실행할 수 있다는 점에서 이식성이 좋다고 느꼈습니다. 즉 jenkins 서버를 다른 곳에 새로 구축하거나 확장하거나 할 때 이러한 방식이 괜찮을 것 같습니다.
Jenkins 서버를 한 번 구축하고 굳이 확장하거나 옮기지 않을 거라면 플러그인 연동해서 사용해도 무방합니다. 각자의 환경에 맞는 방식을 선택하면 될 것 같습니다.

 

3-3. Test, Build

stage('Test') {
    steps {
        sh "./gradlew clean :${MODULE_NAME}:test"
    }
}

stage('Build') {
    steps {
        sh "./gradlew clean :${MODULE_NAME}:build -x test"
    }
}

Test, Build 단계는 따로 언급할 것이 없어 보입니다. spring boot project root 경로에 있는 gradle wrapper(./gradlew)를 통해 대상이 되는 모듈에 대한 test와 build를 각각 실행하면 됩니다.

 

3-4. Deploy

build 단계를 통해 생성된 실행가능한 jar 파일을 서버로 배포하는 단계입니다.
Publish over SSH 플러그인을 설치하면 Jenkins에서 pipeline syntax 페이지를 통해 간편하게 script를 작성할 수 있습니다.

ssh publisher 스크립트를 구성하기 전에 jenkins 설정을 해야하는데요.
Jenkins 관리 > System에 들어가면 Publish over SSH 항목이 있습니다.
(Jenkins 관리 > Plugins 에서 Publish over SSH 설치하셔야 합니다.)

해당 항목에서 SSH Servers 설정할 수 있는 칸들이 나오는데요. SSH Server는 배포 대상이 되는 서버이자 애플리케이션이 실행되는 서버라고 생각하시면 됩니다. 여기에 해당 서버에 대한 내용을 기입하면 됩니다.

  • Hostname: ssh server의 ip 주소를 기입하면 됩니다.
  • Username: ssh server의 사용자 이름(aws ec2는 ec2-user)
  • Remote Directory: ssh server에 전송이 될 파일들이 위치하는 기준이 되는 path입니다. 기본 workspace 경로라고 생각하면 됩니다. (aws ec2는 ec2-user 유저의 user directory로 설정)
  • authentication: ssh를 위한 key 설정을 해야 합니다. 설정방식은 제각각이지만 저는 그냥 jenkins서버에 있는 .ssh 디렉토리 안에 원격 서버에 대한 pem key 파일을 저장해두고 해당 경로를 설정했습니다.
    (Path to key > jenkins 서버의 경로를 기준으로 설정해야 합니다.)
    참고로, 위의 캡쳐본에서 /home/ec2-user/.ssh/app-server.pem 경로에서 /home/ec2-user는 ssh server의 디렉토리가 아니라 jenkins 서버의 디렉토리입니다. (저는 jenkins 서버도 ec2에 올려서 상용하고 있습니다.)

위 내용과 같이 설정하고 저장합니다. 

설정이 완료되면 ssh publisher pipeline script를 작성해야 하는데요. Jenkins에서 제공해주는 Pipeline Syntax 기능을 사용합시다.

Pipeline Syntax에 가시면 sshPublisher 선택하면 아래 관련 설정필드들이 나옵니다.

Source files, Remove prefix, Remote directory 내용만 입력했습니다.

  • Source files: Jenkins 서버에서 빌드했던 jar파일의 절대경로
  • Remove prefix: 원격 서버로 보낼 때 Source files에 지정했던 절대경로가 포함이되어 전송이 되는데요. Jenkins 서버와 원격서버의 파일위치는 독립적으로 가져가야 하기에 절대경로 부분을 제거하고 딱 파일만 지정되도록 remove prefix를 지정합니다.
    (ex. /this/path/to/application.jar ... remove prefix: /this/path/to >> application.jar )
  • Remote directory: 위에 SSH Server 설정할 때 Remote Directory를 /home/ec2-user로 지정했는데요. 여기서 지정한 디렉토리를 기준으로 어느 위치에 source file을 보내겠느냐하는 내용입니다. 저는 deploy 디렉토리를 설정했고 /home/ec2-user/deploy 위치에 배포파일이 보내지게끔 했습니다.

설정 필드들을 채우고 맨아래에 Generate Pipeline Script 버튼을 클릭하면 위와 같이 ssh publisher에 대한 완성된 pipeline script 코드가 나오게 됩니다. 위 내용을 복사해서 Jenkinsfile에 stage 구문 안에 집어넣으면 됩니다.

stage('Deploy') {
    steps {
        echo 'Deploy JAR file'
        sshPublisher(publishers: [
                sshPublisherDesc(
                        configName: 'app-server',
                        transfers: [
                                sshTransfer(
                                        cleanRemote: false,
                                        excludes: '',
                                        execCommand: '',
                                        execTimeout: 120000,
                                        flatten: false,
                                        makeEmptyDirs: false,
                                        noDefaultExcludes: false,
                                        patternSeparator: '[, ]+',
                                        remoteDirectory: 'deploy',
                                        remoteDirectorySDF: false,
                                        removePrefix: "${PARENT_MODULE_NAME}/${PROJECT_NAME}/build/libs",
                                        sourceFiles: "${PARENT_MODULE_NAME}/${PROJECT_NAME}/build/libs/${PROJECT_NAME}.jar"
                                )
                        ],
                        usePromotionTimestamp: false,
                        useWorkspaceInPromotion: false,
                        verbose: false
                )
        ])
    }
}

 

3-5. Run Application

Ansible playbook을 이용해 원격서버에서 application을 실행하는 단계입니다. Jenkins에 Ansible 관련 설정은 맨위에 제가 작성했던 Ansible 관련 글을 첨부해두었는데요. 그것을 참고하시면 될 것 같습니다.

stage('Run Application with Ansible') {
    steps {
        ansiblePlaybook(
                installation: 'ansible',
                playbook: "${ANSIBLE_PLAYBOOK}",
                inventory: "${ANSIBLE_INVENTORY}",
                extraVars: [
                    module_name: "${PROJECT_NAME}",
                    jenkins_user_home: "${HOME}",
                    spring_profile: "${PROJECT_PROFILE}",
                    vault_address: "${VAULT_ADDR}",
                    vault_token: "${VAULT_TOKEN}"
                ]
        )
    }
}

installation, playbook, inventory부분은 이전에 제가 작성했던 글에서 설명하고 있어서 넘어가도록 하겠습니다.
extraVars 부분은 ansible playbook에서 사용할 수 있도록 제공해주는 일종의 환경변수 같은 것들입니다. spring boot를 실행시키기 위한 값들을 위주로 설정했습니다.

Jenkinsfile 전체 내용에 대해서 링크 따로 남겨놓겠습니다. 위의 내용하고 다른 점들이 있긴 한데요. 참고하시면 좋을 것 같습니다.
https://github.com/beaniejoy/dongne-cafe-api/blob/01a9f61f44f3fa0f6a918727178c2dcaeae1da33/Jenkinsfile-service

 

📌 4. ansible playbook 작성하기

Jenkins pipeline이 작성이 완료되었으면 다음으로 ansible을 실행할 playbook을 작성해야합니다. 여기서는 playbook을 아주 간단하게 작성해보려고 합니다. 또한 shell script를 통해 무중단 배포 전략 없이 간단하게 중단(?) 배포를 통해 application을 실행시키는 것까지 정리해보겠습니다.
(ansible playbook에 대한 사용법, 문법같은 경우 너무나 방대한 내용을 담고 있기 때문에 document를 참고하면서 아래 내용을 살펴보시는 걸 추천드립니다. ansible playbook doc)

 

Ansible playbooks — Ansible Documentation

A playbook runs in order from top to bottom. Within each play, tasks also run in order from top to bottom. Playbooks with multiple ‘plays’ can orchestrate multi-machine deployments, running one play on your webservers, then another play on your databas

docs.ansible.com

 

4-1. 변수 설정하기

# run-app.yml (ansible playbook)
---
- name: Run java application
  hosts: ec2

  vars_files:
    - vars/all.yml
    - "{{ jenkins_user_home }}/ansible/vars/app_config.yml"

  tasks:
    ...

 

hosts는 inventory/hosts 파일에 지정한 대상 서버를 설정하게 됩니다. ec2 이름으로 설정된 내용이 inventory 안에 있는데 그것을 가리키도록 작성한 것입니다.

var_files는 해당 playbook에서 사용할 변수들을 어떤 파일에서 가져올 것인지 지정하는 것입니다. 해당 playbook을 기준으로 상대 경로로 하거나 절대경로로 설정할 수 있습니다. 그럼 변수들을 담은 파일에는 어떤 것들이 있는지 간단하게 살펴보도록 하겠습니다.

# ./vars/all.yml

remote_server_user: ec2-user
remote_server_group: ec2-user
remote_user_workspace: "/home/{{ remote_server_user }}"

remote_deploy_path: "{{ remote_user_workspace }}/deploy"
remote_app_path: "{{ remote_user_workspace }}/app"
remote_env_path: "{{ remote_user_workspace }}/app/env"
remote_script_path: "{{ remote_user_workspace }}/app/scripts"

주로 remote server에 대한 내용들입니다. 사용자명, 그룹명을 작성했고 application을 실행하기 위해 필요한 directory들을 미리 변수로 만들어두었습니다.

 

4-2. 배포된 jar 파일 체크

# run-app.yml (ansible playbook)
tasks:
    - name: Register JAR file existence status
      ansible.builtin.stat:
        path: "{{ remote_deploy_path }}/{{ module_name }}.jar"
      register: file_status

    - name: Check if JAR file exists
      block:
        - name: Check 'file_status'
          ansible.builtin.fail:
            msg: "{{ module_name }}.jar does not exist"
          when: not file_status.stat.exists

        - name: Print success message
          ansible.builtin.debug:
            msg: "{{ module_name }}.jar exists!!"

다시 playbook으로 돌아와 이제 ansible을 통해 어떤 작업을 지시할 것인지 tasks에 설정해야 합니다.

처음에는 Jenkins Deploy 단계에서 배포한 jar 파일이 잘 있는지부터 체크합니다. file_status를 매개로 해서 jar 파일 존재유무 체크를 하게 되는데요. (file_status.stat.exists)
만약 존재하지 않으면 fail 처리해서 다음 단계 진행이 안되도록 하면 됩니다.

 

4-3. 파일 이동 및 필요한 template 파일 전송

    - name: Copy JAR file for running application
      ansible.builtin.copy:
        src: "{{ remote_deploy_path }}/{{ module_name }}.jar"
        remote_src: yes
        dest: "{{ remote_app_path }}/{{ module_name }}.jar"
        owner: "{{ remote_server_user }}"
        group: "{{ remote_server_group }}"
        mode: 0644

    - name: Remove deployed JAR file
      ansible.builtin.file:
        state: absent
        path: "{{ remote_deploy_path }}/{{ module_name }}.jar"

    - name: Copy env file to remote server
      ansible.builtin.template:
        src: .env.j2
        dest: "{{ remote_env_path }}/.env"
        owner: "{{ remote_server_user }}"
        group: "{{ remote_server_group }}"
        mode: 0644

    - name: Copy service script file to remote server
      ansible.builtin.template:
        src: service.sh.j2
        dest: "{{ remote_script_path }}/service.sh"
        owner: "{{ remote_server_user }}"
        group: "{{ remote_server_group }}"
        mode: 0755

ansible은 원격 서버 내부에 있는 파일을 다른 디렉토리로 이동시키는 것도 가능하고 ansible을 실행하는 서버에서 원격서버로 파일을 전송하는 것도 가능합니다.

먼저 원격 서버에 배포된 jar 파일을 원하는 위치로 이동을 시킵니다. (copy 후 제거 처리) 그 다음 java 애플리케이션 실행시 필요한 환경변수를 담은 환경변수 파일(.env)과 애플리케이션 실행을 위한 실행 스크립트 파일(service.sh)을 원격서버로 보냅니다.

기본적인 ansible playbook 작성법에 대해서는 링크드린 ansible playbook doc을 참고하는 걸 추천드렸는데요. 그 중 j2 template에 대해서는 간단하게 언급하고 넘어가는 것이 좋을 것 같습니다. 위의 스크립트를 보면 src에 이상한 j2 확장자로 끝나는 파일이 있습니다.
(service.sh.j2, .env.j2)

j2 template은 ansible playbook에서 제공하는 templating 기능인데요. j2 확장자로 끝나는 파일에 대해서 ansible playbook에 등록한 여러 변수들(extraVars, vars_files 등 여러 방식을 통해 설정된 변수들)을 접근할 수 있게 됩니다. 이게 무슨 소리인지는 다음 예시를 보면 한 번에 이해되실 겁니다.

# .env.js
DATABASE_HOST={{ database_host }}
DATABASE_USERNAME={{ database_username }}
DATABASE_PASSWORD={{ database_password }}

# run-app.yml (ansible playbook)
...
vars_files:
    - vars/all.yml
    - "{{ jenkins_user_home }}/ansible/vars/app_config.yml"
...

.env.j2 파일입니다. spring boot application을 실행하기 위해 필요한 환경변수들을 담은 파일을 원격서버로 보내야 하는데요.
위의 파일 내용 처럼 {{ 변수명 }} 이렇게 지정하면 ansible playbook으로 가져온 변수 내용으로 교체되어 원격서버로 전송이 된다고 생각하시면 됩니다. {{ database_host }}은 그 아래 playbook에서 vars_files로 가져온 app_config.yml 파일에서 가져오게 됩니다.

이렇게 j2 template은 playbook에 가져온 변수들에 접근할 수 있다는 점에서 유용합니다. ansible playbook은 이것을 이용해 .env.j2와 애플리케이션 실행을 위한 shell script인 service.sh.j2 파일을 원격 서버로 보내게 됩니다.

 

4-4. 애플리케이션 실행 및 로그 확인

    - name: Run Application
      ansible.builtin.command: "{{ remote_script_path }}/service.sh"
      register: result
      environment:
        VAULT_DOMAIN_ADDR: "{{ vault_address }}"
        VAULT_AUTH_TOKEN: "{{ vault_token }}"
        
    - name: Show all prints while running service script
      ansible.builtin.debug:
        msg: "{{ result.stdout_lines }}"

    - name: Finish
      ansible.builtin.debug:
        msg: "All tasks completed!!"

원격 서버로 보내진 service.sh 스크립트 파일을 통해 애플리케이션을 실행하게 되고(ansible.builtin.command) 해당 스크립트 파일을 실행하면서 기록된 로그들을 result에 담아 다음 단계에서 출력(ansible.builtin.debug)하게 됩니다. command로 스크립트 파일 실행할 때 environment 통해 vault 관련 domain과 token 값을 환경변수로 등록합니다. (vault 내용은 스킵하셔도 됩니다.)

여기까지 실행이 완료되면 배포를 위한 ansible playbook 완성입니다.

위 내용에 대한 ansible playbook 전체 파일도 링크드리겠습니다. 내용이 조금 다를 순 있어서 참고만 하시면 좋을 것 같습니다.
https://github.com/beaniejoy/dongne-cafe-api/blob/01a9f61f44f3fa0f6a918727178c2dcaeae1da33/scripts/deploy/run-app.yml

 

📌 5. Shell Script를 통한 애플리케이션 실행

마지막으로 애플리케이션 실행을 위한 shell script를 작성해야 하는데요. 이번 게시글에서는 정말 간단하게 애플리케이션 실행만 해보는 것을 목표로 작성을 해보았습니다. 그래서 배포전략도 없이 기존 java process 중단 > 신규 애플리케이션 실행하는 방식으로 작성했습니다. (중단 배포 방식)

#!/bin/bash

ENV_LOCATION={{ remote_env_path }}
APP_WORKSPACE={{ remote_app_path }}
MODULE_NAME={{ module_name }}
PORT=8080

SPRING_PROFILE={{ spring_profile }}

STD_OUT=${APP_WORKSPACE}/logs/stdout_${PORT}.log
STD_ERR=${APP_WORKSPACE}/logs/stderr_${PORT}.log

echo "## move to application workspace"
cd $APP_WORKSPACE

echo "## kill current running application"
CURRENT_PID = $(pgrep -f ${MODULE_NAME})

if [ -z "${CURRENT_PID}" ];
then
  echo "no current running application >> pass killing process"
else
  echo "kill -TERM CURRENT_PID"
  kill -TERM ${CURRENT_PID}
  sleep 2
fi

echo "## run java application"
java --version
if [ $? -ne 0 ];
then
  echo "no JRE setup > JRE is required!!"
  exit 1
fi

nohup java -jar \
  -Dspring.config.import=optional:file:${ENV_LOCATION}/.env[.properties] \
  -Dspring.profiles.active=${SPRING_PROFILE} \
  ${APP_WORKSPACE}/${MODULE_NAME}.jar 1>${STD_OUT} 2>${STD_ERR} &

echo "## run application completed!!"

전체 script 내용입니다. service.sh.j2 파일이름에서 알 수 있듯이 shell script도 ansible의 j2 template을 이용했습니다.
내용이 워낙 간단해서 일부분만 짚고 넘어가도록 하겠습니다.

echo "## kill current running application"
CURRENT_PID = $(pgrep -f ${MODULE_NAME})

if [ -z "${CURRENT_PID}" ];
then
  echo "no current running application >> pass killing process"
else
  echo "kill -TERM CURRENT_PID"
  kill -TERM ${CURRENT_PID}
  sleep 2
fi

- pgrep -f ${MODULE_NAME}

보통 linux 계열 OS에서 process 확인 할 때 "ps -ef | grep [process name]" 이런 식으로 많이들 확인하셨을텐데 pgrep은 저 두 개를 합친 것으로 생각하시면 됩니다. pgrep의 f 옵션(-f)은 실행중인 프로세스가 있는 경우 해당 프로세스 ID를 반환하도록 하는 옵션입니다. 즉, 저기서 하려는 액션은 실행 중인 java application의 PID 값을 받아서 종료해주는 것입니다.

- kill -TERM ${CURRENT_PID}

배포할 때 가장 중요하게 고려해야 하는 것 중에 하나가 바로 프로세스를 올바르게 종료하는 것입니다.

배포때 마다 애플리케이션을 중단해야 하는데 이미 요청이 들어오고 나서 처리되지 않았는데 중간에 애플리케이션을 강제 종료하면 그 요청은 제대로 처리되지 않은 상태로 응답도 못 내려주고 끝나게 됩니다. 요청 처리하는 중간에 중단이 되어버리면 데이터가 꼬일 수 있는 상황도 발생할 수 있는 만큼 프로세스 종료에 대해서 신경써서 스크립트를 작성해야 합니다.

Spring Boot에서는 graceful shutdown 기능을 제공하고 있고 서버단에서는 kill SIGTERM을 사용해서 안전하게 프로세스를 종료할 수 있는데요. 이부분에 대해서 아주 잘 설명하고 있는 글이 있어서 소개를 해드리고자 합니다. 시간 되실 때 읽어보시는 걸 추천드립니다.

> kill 명령어로 안전하게 프로세스 종료 시키는 방법
https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html

 

Unix, Linux 에서 kill 명령어로 안전하게 프로세스 종료 시키는 방법

 여기를 클릭하여 펼치기... 위 쓰레드에서 발췌한 killtree.sh #!/bin/bash killtree() { local _pid=$1 local _sig=${2:-TERM} kill -stop ${_pid} # needed to stop quickly forking parent from producing child between child killing and parent ki

www.lesstif.com

> SpringBoot Graceful Shutdown
https://hudi.blog/springboot-graceful-shutdown/

 

SpringBoot Graceful Shutdown

이전 포스팅인 무중단 배포 중 구버전 프로세스를 그냥 종료해도 괜찮을까? (feat. Kill, Graceful Shutdown) 에서 스프링부트의 Graceful Shutdown에 대해서 간단히 다루어보았다. 이 내용을 조금 더 자세히

hudi.blog

 

echo "## run java application"
java --version
if [ $? -ne 0 ];
then
  echo "no JRE setup > JRE is required!!"
  exit 1
fi

nohup java -jar \
  -Dspring.config.import=optional:file:${ENV_LOCATION}/.env[.properties] \
  -Dspring.profiles.active=${SPRING_PROFILE} \
  ${APP_WORKSPACE}/${MODULE_NAME}.jar 1>${STD_OUT} 2>${STD_ERR} &

echo "## run application completed!!"

프로세스를 안전하게 종료했다면 새로 배포된 빌드파일로 애플리케이션을 실행하면 됩니다. 가장 먼저 jdk가 잘 설치되었는지 체크합니다. 여기서 더 추가해볼 수 있는 것은 Spring Boot 프로젝트에 설정된 jdk 버전과 일치하는 버전이 있는지도 체크할 수 있을 것 같습니다.

다음으로 java application 실행단계입니다. nohup은 뒤에 나오는 명령프로그램을 데몬 형태로 실행시켜줍니다. 로그아웃으로 세션이 끊어져도 해당 명령프로그램은 중지되지 않고 계속 실행이 된다고 보시면 됩니다. java 실행시 ansible playbook에서 가져온 변수들을 토대로 env파일을 import하고 spring profile도 설정해주었습니다.

 

📌 6. 정리

Jenkinsfile과 ansible playbook, shell script까지 작성하고 Jenkins에 item 등록해서 빌드를 하게 되면 아래와 같이 pipeline으로 등록해둔 모든 stage가 나오면서 성공, 실패여부가 나오게 됩니다.

위의 캡쳐 사진에서는 제가 이것저것 테스트하느라 몇몇 stage를 스킵해서 중간에 비어있는 채로 나왔는데요. 처음부터 끝까지 jenkins build를 성공적으로 마치게되면 모두 초록불로 나오게 됩니다.

위에서 Jenkins pipeline의 stage 순서 구성부터 pipeline script, ansible playbook, 실행 script 작성까지 정리해보았는데요. 다시 읽어보니 두서없이 정리한 것 같네요,, 혹시 궁금한 점이 있으면 언제든 댓글 주세요!

이번 게시글에서 정리한 내용들은 참고용입니다. ci, cd 프로세스는 개발하는 팀과 사람마다 다 다르고 인프라 구조도 다르기 때문에 각자의 상황에 맞게 사용하는 것이 중요합니다.
이번 글을 통해 조금이나마 도움을 받으셨으면 하는 바람은 있습니다.

다음 글에서는 무중단 배포전략(Rolling, Blue Green)을 사용해서 배포하는 내용을 정리해보려합니다. 감사합니다.