[우젠구2편] 젠킨스 파이프라인을 활용한 배포 자동화

SungSiHyung·2021년 8월 29일
21

우당탕탕 젠킨스 CD 구현기 2편입니다!

개요

우당탕탕 젠킨스 CD 구현기 2편! 1편은 설치편인데, 없습니다! 아직 못 적어서요! 얼른 적겠습니다!

서버 구성을 직접해야하는 단점이자 장점이 있고, 오랫동안 사랑받아온 만큼 레퍼런스도 다양하고 플러그인도 많이 제공합니다!

오늘은 젠킨스를 통해 자동 배포 파이프라인을 구축한 경험을 공유하고자 합니다~!

서버 배포

상황 : 프로젝트에 멋들어지게 기능을 추가해서 base branch에 merge되었다. 이제 실서버에 배포를 해야겠는데?

제가 최근하고 있는 팀프로젝트에선 다음과 같은 work flow를 가집니다.

  1. 각 개발자가 기능 구현 후 PR
  2. EC2 서버로 접속
  3. github에 merge된 코드를 git pull
  4. gradle을 이용해 bootJar
  5. jar파일을 java -jar 명령어를 통해 80포트에 실행

서버 배포는 매번 기능이 추가 될 때마다 시행되어야 합니다. 이 flow는 새로운 기능마다 바뀌는 것이 아니기에, 자동화 할 수 있는 대상입니다. 이를 자동화하여 최신상태의 코드가 자동으로 배포되도록 하는 것을 Continuos Distribution, 지속적 배포라고 부릅니다.

젠킨스가 해주는 것

아쉽게도 젠킨스가 기능구현을 해주진 않습니다.. copilot 기다립니다..ㅎ_ㅎ

젠킨스가 대신해주는 부분은,

  1. github으로부터 머지되었다는 연락을 받아,
  2. 코드를 git clone하고
  3. 그래들로 프로젝트를 빌드하여,
  4. 빌드한 목적파일을 어플리케이션이 실행되어야 할 서버로 전달하고, 어플리케이션을 구동하기 위한 명령어를 실행합니다.

입니다.

저는 처음 젠킨스 학습을 할 때, 이 설정이 내가 배포를 할때 했던 행동 중 어떤 부분을 자동화 해주는 걸까? 생각해보며 진행하면 젠킨스를 이해하는 데 많은 도움이 되었습니다.


포스팅에선 2→3→4→1 순서로 설정을 해보겠습니다.

처음엔 어플리케이션 서버에서 젠킨스를 구동하였는데, 빌드가 진행될 때 CPU 자원이 모자라 서버가 다운되는 일이 있어서 별도 EC2로 분리하였습니다. 동일한 EC2에서 진행하여도 무방합니다. 서버 자원이 모자라거나 여러개의 WAS를 띄울 일이 생긴다면, 별도 환경으로 분리하는 것을 추천드립니다.

클릭 한 번으로 배포하기

사전작업 - credentials 세팅

컴퓨터가 나 대신 git clone하게 하려면 내가 해당 레포지토리에 대한 권한이 필요합니다.

단순히 공개 레포지토리에 있는 소스코드를 clone하는 목적이라면 권한이 필요하지 않기 때문에 따로 credentials를 설정하지 않아도 됩니다!

파이프라인 상에서 push나 서브모듈 등 권한이 필요한 일이 있다면 credentials이 필요합니다.

현재 팀프로젝트에서 서브모듈을 활용하고 있기 때문에 credentials를 설정해주도록 하겠습니다~! 서브모듈 적용기

액세스 토큰 발급 및 테스트 (테스트는 생략 가능)

  1. 시스템 설정으로 들어갑니다.

  2. github 탭까지 이동합니다. 이후 add를 눌러 access token을 등록해봅시다.

    - 이후 나오는 팝업에서 secret text에서 access token을 등록해주면 됩니다.

  3. 아직 토큰을 발급 안 받았기 때문에, 토큰을 발급해보겠습니다. github 로그인을 한 후 developer settings 으로 이동합니다.

  4. personal access token에서 토큰의 이름, 유효기간, 필요한 권한을 체크해줍니다. 저는 repository의 접근 권한이 필요하므로 첫번째 체크박스와, 레포지토리 훅을 위한 repo_hook 체크박스를 체크했습니다.

  5. 토큰을 생성하면 다신 확인할 수 없는 access Token이 드러납니다. 이후 해당 값을 잊어버리면, 토큰 자체를 새로 발급받아야 합니다~!

  6. test connection을 눌러 성공적으로 access token이 동작하는지 확인합니다.

  7. 잘 되네요! 시스템 설정의 github탭에선 credentials에 Secret text로 등록한 credentials만 인식하지만, 이후 해당 포스트에서는 Username with password 로 생성한 credentials이 필요합니다. add를 한번 더 눌러 미리 생성해 놓겠습니다.

username은 깃헙 아이디, 패스워드는 access token, id는 다른 설정 페이지에서 활용할 이름을 적어주세요.

  1. 이렇게 등록한 credentials는 추후 manage credentials에서 등록 혹은 확인이 가능합니다.

파이프라인 생성하기

간단한 Job을 위해선 freestyle도 좋은 선택입니다. 비교적 세팅이 단순합니다. 좀 더 복잡한 종류의 Job간의 연계나 상세한 세팅, 이쁜 UI (각 job별로 어떻게 진행되고 있는지 이쁘게 보여줍니다^^)를 원한다면 파이프라인을 생성할 수 있습니다. 이번 포스트에선 각 job별로 진행상황을 확인하고 싶고, 정적파일 분석이나 슬랙알림 등 추가적인 job들을 CD과정에 포함할 것을 고려하고 있기 때문에 파이프라인으로 생성하겠습니다.

  1. 새로운 아이템을 클릭합니다.

  2. 파이프라인을 선택하고, 이름을 지어줍니다.

  3. 파이프라인을 생성했다면, 파이프라인에서 활용할 pipeline script에 대해 알아봅시다. pipeline script은 그루비나 젠킨스에서 정의한 pipeline syntax를 통해 해당 item에서 수행할 job을 선언하고, 순서를 조정하고, 환경을 설정하는 역할을 합니다. 자세한 내용은 아래 링크에서 확인할 수 있습니다.

    Pipeline 공식 Document

  • 처음엔 막막할 수 있는데, 젠킨스에선 플러그인 별로 syntax를 만들어주는 pipeline syntax를 지원합니다. 먼저 뼈대를 만들기 위해 파이프라인 스크립트에서 우상단의 샘플 파이프라인 중, Hello World를 클릭합니다.

    1. pipe line은 이 파이프라인 자체를 의미합니다.
    2. agent는 이 파이프라인 스크립트를 실행할 executor를 지정합니다. any로 두면 어떤 executor도 실행할 수 있다는 의미가 됩니다.
    3. stages는 실행할 Job들의 집합입니다.
    4. stage는 각각의 Job을 의미합니다. Job 내부의 단계를 의미하는 steps를 포함해야합니다.
    5. steps에선 실제로 실행할 쉘이나 syntax를 입력해주면 됩니다.

hello world 대신에 github clone을 하기 위해, syntax를 만들어주겠습니다. pipeline syntax를 클릭합니다.

github clone하기

pipeline syntax를 클릭하여 나온 화면에서, snippet generate를 클릭합니다.

  1. sample step을 클릭하고, 목록 중 git을 체크합니다.

  2. repository url을 적어주고, 브랜치를 선택하고, credentials를 넣어줍니다. 아까 추가하지 않았다면 지금 추가해줍시다. 공용 레포지토리를 clone하는 입장이라면 비워놓아도 됩니다.

  3. generate를 누르면 짜잔! 스니펫이 생성되었습니다. 해당 내용을 복사합니다.

  4. stage의 이름을 적절하게 바꾸고, steps에서 아까 복사한 snippet을 붙여넣어 줍시다. 읽어보시면 git 특정 브랜치에서, 특정 credentials로, url의 레포지토리에서 소스를 가져오는 스니펫이구나 짐작이 가능합니다.

  5. 저장을 누르면, github을 clone하는 job을 가진 파이프 라인을 생성한 것입니다! 한번 테스트 해봅시다. 한 번에 전체 job을 작성하면 trouble shooting이 힘들어지기 때문에 각 단계별로 테스트 해보시는 걸 권합니다. 대쉬보드에서 생성한 아이템을 클릭하여 들어갑시다.

  6. build now버튼을 눌러 수동으로 job을 유발합니다.

  7. 실행되어 빌드 히스토리에 잘 들어간걸 볼 수 있습니다. 빌드 히스토리도 클릭해봅시다.

  8. 빌드 과정에서 출력되는 콘솔문은 console output탭에서 확인할 수 있습니다.

  9. pipeline 잡의 진행 경과와, github clone을 진행하며 입력된 명령어와 출력문들을 확인할 수 있습니다.

빌드하기

이제 빌드를 위한 stage를 추가하겠습니다.

  1. 빌드는 clone해 온 소스에 포함되어 있는 gradle wrapper를 활용합니다. 저희 프로젝트는 .gradlew가 소스코드의 /가 아닌 /backend 에 위치하고 있습니다. dir 스니펫을 활용해 cd backend 명령어를 입력한 것과 동일한 효과를 보고, shell문을 직접 이용해 빌드하겠습니다. pipeline syntax에서 sh ''' ${쉘 명령어} ''' 를 통해 쉘 명령어를 실행하도록 설정할 수 있습니다. 저희 프로젝트에서 빌드를 위해 실행해야할 태스트는 clean과 bootJar이므로, 다음과 같이 설정하겠습니다.

현재까지의 pipeline script입니다.

    pipeline {
        agent any

        stages {
            stage('github clone') {
                steps {
                    git branch: 'develop',
                        credentialsId: 'repo-and-hook-access-token-credentials',
                        url: 'https://github.com/woowacourse-teams/2021-jujeol-jujeol'
                }
            }
            
            stage('build'){
                steps{
                    dirs('backend'){
                        sh'''
                            echo build start
                            ./gradlew clean bootJar
                        '''
                    }
                }
            }
        }
    }

한번 테스트를 해볼까요? item의 대쉬보드로 가 build now를 눌러봅니다.

이런... 저는 실패했네요 ㅠ.ㅠ 저희 코드를 빌드할 때 서브모듈이 필요한데, 해당 내용을 고려하지 않았기 때문입니다. (서브 모듈이 없었다면 성공하셨을 겁니다!)

git plugin snippet은 서브모듈의 init과 update까지 지원해주진 않습니다. 이렇게 생성한 syntax가 원하는대로 동작하지 않으면 다른 step을 찾거나, 공식문서를 통해 원하는 설정이 있는지 확인해봐야 합니다.

  1. 서브모듈 까지 받아오기 위해선 다른 steps syntax을 활용해보겠습니다. checkout으로 검색하여 해당 simple step을 선택합니다. private repoistory로 등록한 서브모듈을 받아오기 위한 여정이므로 credentials를 세팅해줍니다.

  2. addtional behaviours의 add 버튼에서 Advanced sub-modules behaviours를 눌러 아래와 같이 설정해줍니다. 부모 레포지토리의 credentials를 그대로 활용한다는 4번 옵션이 중요합니다.

서브모듈이 private인 경우 서브모듈 레포지토리 이름과 메인 프로젝트에서 서브모듈을 포함한 디렉토리 명이 일치하지 않으면 레포지토리를 못 찾는 버그가 있었습니다. 디렉토리명을 수정하여 해결하였습니다.
ex) "jujeol-submodule" 이란 이름의 레포지토리를 /submodule 이란 디렉토리 하위로 init 한 경우 문제가 될 수 있다.
git submodule add -b main [jujeol-submodule url] submodule

  1. 생성한 파이프라인 스크립트를 steps 하위에 붙여넣어서 기존의 git syntax를 대체해줍니다.

현재까지의 파이프라인 입니다.

pipeline {
    agent any

    stages {
        stage('github clone') {
            steps{
                checkout(
                    [$class: 'GitSCM',
                    branches: [[name: '*/develop']],
                    extensions: 
                    [[$class: 'SubmoduleOption',
                        disableSubmodules: false,
                        parentCredentials: true,
                        recursiveSubmodules: false,
                        reference: '',
                        trackingSubmodules: true]],
                    userRemoteConfigs:
                        [[credentialsId: 'repo-and-hook-access-token-credentials',
                            url: 'https://github.com/woowacourse-teams/2021-jujeol-jujeol']]
                    ]
                )
            }
        }
        
        stage('build'){
            steps{
                dir('backend'){
                    sh'''
                        echo build start
                        ./gradlew clean bootJar
                    '''
                }
            }
        }
    }
}


테스트 해보겠습니다.

이후 빌드까지 잘 돌아서 초록불이 떴습니다! (미처 캡처를 못했네요ㅠ)

ssh를 이용하여 서버로 jar파일 전달하기

이제 배포용 젠킨스에서 WAS용 EC2로 빌드 결과 파일을 전달해야합니다. publish over ssh라는 플러그인을 활용해주겠습니다.

  1. 대쉬보드의 플러그인 관리를 눌러줍니다.

  2. 설치 가능 탭에서 publish over ssh를 찾아 설치합니다. 캡처 당시엔 이미 설치되어서 설치된 플러그인 목록에 해당 플러그인이 보이네요.

  3. ssh를 통해 파일을 보내기전, pem키의 정보가 필요합니다. EC2에 접속할 수 있는 pem키의 내용을 텍스트 편집기나 cat명령어로 열어, 내용을 복사합니다. 플러그인이 설치되면 시스템 설정의 플러그인 설정도 생기는데, 해당 설정으로 이동합니다.

  • EC2 접속에 필요한 pem키의 내용을 key에 붙여 넣어줍니다.

  • ssh server를 추가하고,

    1번에는 syntax에서 참조할 수 있는 이름

    2번에는 빌드된 파일을 전송할 서버의 private ip (같은 vpc에 속해있어 private ip로 접근할 수 있습니다.)

    3번에는 username(ubuntu로 만든 aws ec2에서 따로 설정을 바꾸지 않았다면 ubuntu가 default입니다.)

    4번에는 파일이 도착할 베이스 디렉토리를 적어줍니다.

  • 다 잘 추가했다면 Test Configuration 버튼을 눌러 확인해줍니다.

  1. 성공했다면 snippet으로 만들어볼 시간입니다. pipeline syntax 생성으로 와서, sshPublisher simple step을 만듭니다.
  • ssh Server엔 설정에서 선언한 name을 넣어줍니다.
    1. 1번은 소스파일의 위치입니다. gradle wrapper에서 빌드 결과물을 build/libs/ 로 위치하므로, build/libs/*.jar 로 작성하였습니다.
    2. 2번은 소스파일에서 원본파일의 디렉토리를 어디까지 포함할 것인지에 대한 설정입니다. 필요하지 않으므로 디렉토리를 모두 제거합니다.
    3. remote directioy기준으로 배포될 경로를 적어줍니다. 배포 서버의 해당 폴더로 목적파일이 도착하게 됩니다. (디렉토리를 미리 생성해주세요!)
    4. 전송을 마치고 실행할 shell문의 디렉토리 및 파일 위치입니다. 저희 프로젝트에선 다음과 같은 배포 쉘을 활용합니다.
        echo "> jujeol pid 확인"
        CURRENT_PID=$(ps -ef | grep java | grep jujeol | grep -v nohup | awk '{print $2}')
        echo "$CURRENT_PID"
        if [ -z ${CURRENT_PID} ] ;then
        echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
        else
        echo "> sudo kill -9 $CURRENT_PID"
        sudo kill -9 $CURRENT_PID
        sleep 10
        fi
        echo "> jujeol 배포"
        JAR_PATH=$(ls -t /home/ubuntu/2021-jujeol-jujeol/deploy/*.jar | head -1)
        sudo nohup java -jar -DServer.port=80 -Dspring.profiles.active=dev ${JAR_PATH} >> /home/ubuntu/2021-jujeol-jujeol/logs/jujeol.log &
        1. CURRENT_PID에 현재 실행되고 있는 프로젝트의 pid를 받아옵니다. 
        쉘을 작성하기 전 직접 서버를 실행하고 아래 명령어를 입력해서 pid를 잘 잡아오는지 확인합니다.(grep의 내용을 바꾸어가며)
        # 프로세스 중 java, jujeol 이라는 문자열이 있고, nohup이란 문자열은 없는 프로세스의 두번째 열 
        ps -ef | grep java | grep jujeol | grep -v nohup | awk '{print $2}'

        2. CURRENT_PID가 있으면 먼저 종료합니다. 셧다운 시간이 있기 때문에 sleep를 해줍니다.
        3. 종료가 끝나면 deploy폴더에서 마지막 jar을, 80포트에 dev 프로필로 실행합니다.
  1. snippet을 stage로 만들어봅시다. sshPublisher같은 경우 verbose옵션이 있는데, 해당 옵션을 true로 주면 트러플 슈팅시 유용합니다. 빌드의 console output에 해당 내용이 상세하게 찍힙니다.

현재까지의 파이프라인입니다.

    pipeline {
        agent any

        stages {
            stage('github clone') {
                steps{
                    checkout(
                        [$class: 'GitSCM',
                        branches: [[name: '*/develop']],
                        extensions: 
                        [[$class: 'SubmoduleOption',
                            disableSubmodules: false,
                            parentCredentials: true,
                            recursiveSubmodules: false,
                            reference: '',
                            trackingSubmodules: true]],
                        userRemoteConfigs:
                            [[credentialsId: 'repo-and-hook-access-token-credentials',
                                url: 'https://github.com/woowacourse-teams/2021-jujeol-jujeol']]
                        ]
                    )
                }
            }
            
            stage('build'){
                steps{
                    dir('backend'){
                        sh'''
                            echo build start
                            ./gradlew clean bootJar
                        '''
                    }
                }
            }
            
            stage('publish on ssh'){
                steps{
                    dir('backend'){
                        sshPublisher(
                            publishers: 
                                [
                                    sshPublisherDesc(
                                        configName: 'jujeol-dev',
                                        transfers:
                                            [
                                                sshTransfer(
                                                    cleanRemote: false,
                                                    excludes: '',
                                                    execCommand: 'sh /home/ubuntu/myproject/script/init_server.sh',
                                                    execTimeout: 120000,
                                                    flatten: false,
                                                    makeEmptyDirs: false,
                                                    noDefaultExcludes: false,
                                                    patternSeparator: '[, ]+',
                                                    remoteDirectory: '/myproject/deploy',
                                                    remoteDirectorySDF: false,
                                                    removePrefix: 'build/libs',
                                                    sourceFiles: 'build/libs/*.jar')],
                                                    usePromotionTimestamp: false,
                                                    useWorkspaceInPromotion: false,
                                                    verbose: true
                                                )
                                            ]
                                        )
                    }
                }
            }
        }
    }
  1. WAS에선 목적파일이 도착할 디렉토리나, 쉘에서 nohup에 선언한 로그파일 디렉토리가 잘 생성되었는지 확인합니다.
  1. 빌드해봅시다! 초록초록하길 기원합니다. 저는 성공했네용~~! EC2 콘솔에서 ps -ef | grep java 명령어를 활용해 구동이 잘 되었는지 확인해봅시다.

중간 점검

이제 젠킨스 웹 콘솔에 접속해 build now를 누르면 프로그램을 자동 배포할 수 있는 발판을 마련했습니다... 하지만 조금 더 편해지고 싶네요! pr이 머지될 때마다 저절로 build 된다면 어떨까요??

빌드 유발하기

깃헙은 깃헙에서 발생하는 수많은 활동들에 대해서 webhook을 제공합니다. 특정한 이벤트가 발생했을 때 해당 내용을 등록해놓은 api로 보내주는 기능입니다. 해당 기능을 활용하여 github에서 특정한 활동이 일어났을 때 job이 유발되도록 구현할 수 있습니다. 저는 PR이 머지되었을 때 자동으로 빌드가 일어나도록 설정하겠습니다.

freestyle에선 build trigger로 GitHub hook trigger for GITScm polling 옵션을 많이 활용했는데, 파이프라인에선 제대로 동작시키기 어렵고 디테일한 세팅은 할 수 없다는 단점이 있었습니다. 저희 프로젝트는 백엔드 코드와 프론트엔드 코드가 공존하는데, 프론트엔드 배포는 github action을 활용하고 있었으므로 백엔드 PR 머지시에만 젠킨스가 동작하도록 만들고 싶었습니다. 방안을 고민하다 Pull Request의 라벨을 바탕으로 백엔드와 프론트엔드 PR을 구분하도록 하는 동료의 조언을 얻어 해당 방식으로 구현해보겠습니다.

백엔드 라벨이 붙어있는 PR이 머지 되었을 때만 빌드가 일어나기를 희망합니다!

Generic Webhook Trigger

  1. 위 내용을 구현하기 위해 Generic Webhook Trigger플러그인을 설치해줍니다.

  2. 파이프라인의 상세 내용에서 build triggers로 이동하면, generic webhook trigger가 추가된 걸 볼 수 있습니다.

    체크박스를 체크해줍니다.

  3. Post content parameter는 웹훅으로 데이터가 도착했을 때 해당 데이터를 분석하여 변수화하는 옵션입니다. json 데이터를 받을 것인데, json을 파싱하여 해당 데이터를 변수에 담습니다. variable 항목은 변수명입니다.


    모든 PR이벤트가 아닌 특정 상황에만 빌드 유발이 되도록

      1. pr이 머지 된 경우인지 여부
      2. pr의 대상이 되는 브랜치
      3. (Optional)백엔드 라벨의 이름

    변수로 받겠습니다. (저희와 같은 특수한 상황이 아니라면 라벨은 받을 필요 없겠지요!)

    토큰을 주면 토큰을 api에 쿼리 파라미터로 붙여주어야만(헤더에 싣는 방식도 가능) 웹훅 요청을 받을 수 있도록 설정할 수 있습니다. job이 여러개가 되었을 때, 젠킨스 서버가 웹훅을 받았을 때 해당 토큰을 통해 어떤 파이프라인이 작업을 요청받았는지 분별할 근거로 삼을 수 있습니다.

    위의 받아놓았던 변수를 기반으로, 한번 더 필터링을 거는 optional filter항목입니다. 변수의 나열이 Expression 정규식의 결과가 true인 경우에만 빌드가 일어납니다. 이를 통해 IF_MERGED가 true면서 BRANCH가 develop면서 라벨 이름이 backend일 때만 빌드가 일어나도록 설정할 수 있습니다.

  1. 이제 깃헙에서 레포지토리의 변경이 일어날 때 웹훅을 전달하도록 설정해봅시다. 프로젝트 settings로 이동합니다. add webhook을 클릭합니다.

  2. 플러그인을 체크했을 때 나온 안내문처럼, jenkins url에 뒤의 url을 붙여 웹훅을 받을 주소를 지정합니다. 토큰도 추가하였기에 빼먹지 않고 적습니다. Content/type은 JSON으로 받겠습니다. 그리고 정말 다양한 상황에 대해서 webhook이 오기 때문에, 모든 이벤트에 대해 webhook을 받지 않고 PR과 Push에 대해서만 연락을 받도록 하겠습니다.


  3. 그리고 테스트용 PR을 머지시켜보면 웹훅의 recent deliveries탭에서 실제로 발행된 웹훅 데이터를 확인할 수 있습니다.

  • 내가 설정한 변수가 잘 받아와질지 예측해보고 싶다면 payload를 복사해와서
  1. jsonpath.com 에서 jsonpath 표현식과 함께 테스트를 해볼 수 있습니다.

  2. Optional filter는 regExr.com 페이지에서 테스트 해볼 수 있습니다.

    RegExr: Learn, Build, & Test RegEx

    JSONPath Online Evaluator

  3. 해당 설정을 해주면, 이후 백엔드 라벨을 단 PR이 머지되었을 때 자동으로 빌드가 실행되는걸 확인할 수 있습니다!

결론

백엔드 자동 배포를 위한 편리한 도구, 젠킨스 파이프라인의 활용법에 대해 알아보았습니다. 현재는 clone, 전달, 배포를 수행하고 있지만 job과 steps를 추가해 다양한 활동을 할 수 있습니다. 시리즈는 1편인 설치편과 3편인 코드 정적분석 프로그램인 sonarcube와 연동하는 내용으로 이어집니다~~! 긴 글 읽어주셔서 감사합니다!

Special Thanks to 손민성 (a.k.a 우아한 테크코스 3기 손너잘)

profile
블로그 이전하였습니다!!! https://sihyung92.oopy.io/

5개의 댓글

comment-user-thumbnail
2021년 8월 30일

이해하기 쉬운 설명 감사합니다 :)

답글 달기
comment-user-thumbnail
2021년 9월 3일

해보진 않았지만 매번 궁금했던 내용인데 이해하기 쉽고 자세한 설명 감사합니다😊👍

답글 달기
comment-user-thumbnail
2022년 3월 7일

roWjsek

답글 달기
comment-user-thumbnail
2023년 12월 19일

안녕하세요 script부분의 -DServer.port 가 정확히 어떤 걸 말하는지 알 수 있을까요? 그냥 이 서버의 80포트를 말하는 걸까요?

답글 달기
comment-user-thumbnail
2024년 2월 29일

와 한 며칠을 credentialsId로 고생했는데 여기서 답을 찾네요 좋은 글 감사합니다!

답글 달기