728x90

 

 


 

Jenkins&Springboot CI/CD 정리(3)

 

 

 

 

지난 2편에 이어 3편에서는 SonarQube Quality Gate 가 통과되었다는 가정하에 그 이후의 Stage들에 대한 설명을 해볼까 합니다.

 

QualityGate까지의 Stage가 끝나게 되면 분석 결과를 Sonar-bot을 통해 Gitea의 PR comment에 남기게 되지만 이부분은 4편에서 설명드리도록 하겠습니다.

 

 

 


1. Jenkins Server에 Docker 설치

 

먼저 Jenkins server가 돌고 있는 EC2 인스턴스에 docker를 설치해주도록 하겠습니다.

이 부분은 이전에 작성된 글이 있으므로 해당 글을 참고해주세요. =)

 

도커(Docker) 설치 & 도커(Docker) 명령어 사용방법 총정리

지난 글에 이어 도커의 명령어와 사용방법을 정리해볼까 합니다 =) 우선 도커를 사용하려면 설치를 해주어야 겠죠? 필자의 경우 AWS EC2 인스턴스로 Ubuntu 환경에서 Docker를 설치하였습니다. (Ubuntu

0andwild.tistory.com

 

 


 

 

2. Dockerfile 생성

 

 

 

SonarQube Quality Gate가 끝난 직후 Docker image build 를 통해 Docker image를 생성한 후 Docker hubpush를 진행하게 됩니다.

 

필자의 경우 Docker hub를 사용해보고자 하는 목적으로 Docker hub를 사용하였습니다.

 

 

(Flow)

1) Jenkins 서버에서 프로젝트 image 생성 & Docker hub로 image push

2) Nginx 서버에서 Docker image pull

 

 

Docker hub는 Github과 같은 개념으로 생각을 하시면 쉽게 이해하실 수 있습니다. =)

Docker hub를 사용하면 이미지 버전관리를 할 수 있다는 장점이 있습니다.

(이번 연습에서는 따로 Docker image 생성 시 버전을 명시하지 않았습니다)

 

 

Docker hub를 사용하지 않을 시에는 gradle build 를 통해 생성된 jar 파일을 Publish over SSH 라는 Jenkins에서 설치가능한 플러그인을 이용해 Nginx 서버로 보낸 후 여기에서 image build를 하는 방법도 있습니다. =)

 

 

필자와 같은 방식을 사용한다면 다음과 같이 진행해주시면 됩니다.

 

 

우선 프로젝트의 root 디렉토리에 dockerfile 을 하나 만들어주도록 하겠습니다.

 

FROM openjdk:11
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

 

FROM 명령문은 base 이미지를 지정해주는 것이며 Dockerfile에서 최상단에 위치시켜줍니다.

base 이미지는 일반적으로 Docker hub 같은 공식 Docker repository에 있는 공개 이미지를 주로 사용합니다. =)

필자는 openjdk11 을 명시해주었습니다.

 

ARG 명령문은  docker build 시 --build-arg 옵션으로 인자값을 넘길 수 있습니다. ex)  --build-arg -port=8080

또한 설정된 인자 값은  ${설정된 인자값} 으로 사용될 수 있습니다.

 

COPY 명령문은 로컬환경 또는 프로젝트내에 파일경로를 입력하고 Docker 이미지 파일 시스템으로 복사를 할 수 있게 해줍니다.

 

위의 COPY 명령문을 해석해보면 필자는 현재 Spirngboot & Gradle을 사용하고 있고 프로젝트 내에서 build를 하게되면

아래와 같이 프로젝트의 root 경로의 build/libs/ 에 .jar 파일이 생성되는 것을 확인할 수 있습니다.

 

 

필자는 ARG 에서 JAR_FILE 이라는 변수안에 .jar 파일의 위치를 지정해주었고 다음과 같이

COPY {JAR_FILE=.jar 파일 경로} app.jar 를 지정해주면 Docker build 시 필자가 지정해준 경로의 .jar파일을 이미지 시스템의 app.jar 파일에 복사 해줍니다. =)

 

마지막으로 ENTRYPOINT 명령문은 이미지를 컨테이너로 띄울 때 항상 실행되어야 하는 커맨드를 지정해줄 수 있습니다.

컨테이너가 뜰 때 ENTRYPOINT 명령문으로 지정되어진 커맨드가 실행되고, 해당 커맨드로 실해된 프로세스가 죽을 때 컨테이너도 함께 종료되어 집니다.

 

java -jar {파일명.jar}

필자가 ENTRYPOINT 안에 지정된 명령문은 우리가 일반적으로 spring project를 터미널 또는 powerShell 등을 이용해 서버를 띄울 때 입력하는 명령어라고 생각하시면 됩니다. 맨뒤에 app.jar 에는 COPY 명령어를 통해 복사된 프로젝트의 jar 파일이 담겨 있기 때문에 app.jar 파일을 실행하라 라고 명령을 하게 되면 프로젝트에서 build 되어진 .jar 파일이 실행되는 것이죠. =)

 

 

이제 Jenkins 서버에서 Docker Plugin을 설치와 Docker hub 가입을 해주도록 하겠습니다.

 

 


 

 

3. Jenkins Docker Plugin 설치 및 Docker hub 가입 및 셋팅.

 

 

 

Jenkins 관리 - Plugin Manager 로 들어가 설치가능에서 Docker 를 검색하신 후 Docker PipelineDocker plugin 두 가지를 설치해주도록 하겠습니다.

 

 

설치가 진행되는 동안 Docker hub 가입을 진행해보도록 하죠. =)

 

 

Docker hub 홈페이지에 들어가 가입을 진행해주고 로그인을 해주도록 하겠습니다.

 

필자의 경우 다음과 같이 image를 관리할 private repository를 하나 생성해주었습니다.

 

docker hub의 셋팅은 끝났습니다.

 

이제 Jenkins에 Docker hub에 접근할 수 있게 Credential 을 등록해주도록 하죠. =)

 

위처럼 Username with password를 이용해 등록해보도록 하겠습니다.

 

Username = Docker hub 아이디

Password = Docker hub 비밀번호

ID = Jenkinsfile에서 사용할 ID 값

Description = 해당 credential 에 대한 설명

 

 

이제 Jenkins Server 에서 docker hub에 로그인을 해보도록 하겠습니다.

 

분명 올바른 아이디와 비밀번호를 입력했는데 permission denied가 떴네요.

 

 

이 문제는 사용자가 /var/run/docker.sock 을 접근하려 했지만 권한이 없어 발생하는 문제입니다. 사용자가 root:docker 권한을 가지고 있어야 합니다.

 

확인을 해보면 docker에 대한 권한이 부여되어 있지 않으므로 권한을 부여해주도록 하겠습니다.

 

sudo usermod -a -G docker $USER

사용자를 docker group에 포함시켜주도록 하겠습니다. $USER 환경 변수는 로그인한 사용자 아이디를 나타내므로 그대로 입력해줍시다.

 

sudo chmod 666 /var/run/docker.sock

/var/run/docker.sock 파일의 권한을 666으로 변경하여 그룹 내 다른 사용자도 접근가능하게 진행해주었습니다.

 

마지막으로 해당 권한 변경사항을 적용시켜주기 위해 Docker 를 재시작 해주도록 하겠습니다.


 

4. Jenkins 파일 작성 (Docker Image build)

 

 

작성을 하다 플로우가 이해가 가지 않는 부분은 풀 소스코드를 필자의 깃헙에서 확인해주시기 바랍니다. =)

 

GitHub - 0AndWild/Jenkins-CICD: Jenkins Ci tool을 활용한 CI/CD 구축

Jenkins Ci tool을 활용한 CI/CD 구축. Contribute to 0AndWild/Jenkins-CICD development by creating an account on GitHub.

github.com

 

stages {
        //공통사용 항목 변수 지정 및 build 유발자와 commit 내역을 함께 Slack 알림으로 전송
        stage("Set Variable") {
            steps {
                script {
                    DOCKER_IMAGE = ''
                    //생성할 Docker Image 이름 지정
                    DOCKER_IMAGE_NAME = "gunyoung/dev"
                    //Container Registry 경로
                    IMAGE_STORAGE = "https://registry.hub.docker.com/"
                    //Container Registry 접근 Credential id
                    IMAGE_STORAGE_CREDENTIAL = "Docker-id"
                    //알림받을 채널
                    SLACK_CHANNEL = "jenkins"
                    SLACK_START_AND_FINISH_COLOR = "#778899";
                    SLACK_SUCCESS_COLOR = "#2C953C";
                    SLACK_FAIL_COLOR = "#FF3232";
                    // Git Commit 계정
                    GIT_COMMIT_AUTHOR = sh(script: "git --no-pager show -s --format=%an ${env.GIT_COMMIT}", returnStdout: true).trim();
                    // Git Commit 메시지
                    GIT_COMMIT_MESSAGE = sh(script: "git --no-pager show -s --format=%B ${env.GIT_COMMIT}", returnStdout: true).trim();
                    //PR_ID
                    PR_ID = "${GIT_COMMIT_MESSAGE}".substring("${GIT_COMMIT_MESSAGE}".indexOf('#')+1,"${GIT_COMMIT_MESSAGE}".indexOf(')')).trim();
                    //PR_BRANCH
                    PR_BRANCH = "${GIT_COMMIT_MESSAGE}".split("from")[1].split("into")[0].trim();
                }
            }
            post {
                success {
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_START_AND_FINISH_COLOR,
                        message:
                        "==================================================================\n" +
                        "\n" +
                        "배포 파이프라인이 시작되었습니다.\n" +
                        "${env.JOB_NAME}(${env.BUILD_NUMBER})\n" +
                        "\n" +
                        "-GIT_PR_ID-\n" +
                        ":  ${PR_ID}\n" +
                        "\n" +
                        "-GIT_PR_BRANCH-\n" +
                        ":  ${PR_BRANCH}\n" +
                        "\n" +
                        "-GIT_COMMIT_AUTHOR-\n" +
                        ":  ${GIT_COMMIT_AUTHOR}\n" +
                        "\n" +
                        "-GIT_COMMIT_MESSAGE-\n" +
                        ":  ${GIT_COMMIT_MESSAGE}\n" +
                        "\n" +
                        "<-More info->\n" +
                        "${env.BUILD_URL}"
                    )
                }
            }
        }

 

먼저 필자는 Stages{} 안의 첫번째 Stage{}로 Set Variable 이라는 자주 사용되는 값 또는 Jenkins에 등록한 credential 등을 이 Stage에서 모두 변수 값에 담아두었습니다. 이렇게 설정을 해두면 다른 Stage들에서도 ${설정한 변수} 로 그 값을 가져올 수 있기 때문에 편리합니다. =)

 

DOCKER IMAGE = ' '  는  docker image 빌드를 한 값을 담아주는 변수이기에 빈 String 으로 지정해주었습니다.

DOCKER_IMAGE_NAME = "gunyoung/dev"  이부분은 Docker hub에서 생성한 registry의 이름과 동일하게 지정해주었습니다.
//Container Registry 경로
IMAGE_STORAGE = "https://registry.hub.docker.com/"
//Container Registry 접근 Credential id로 Jenkins에서 등록한 Docker hub의 credentail ID 값 입니다.
IMAGE_STORAGE_CREDENTIAL = "Docker-id"

 

 

//Docker Image 생성
        stage('Build Docker') {
            when {
                branch "develop"
            }
            steps {
                sh 'echo "Image Build Start"'
                script {
                    DOCKER_IMAGE = docker.build DOCKER_IMAGE_NAME
                }
            }
            post {
                success {
                    sh 'echo "Successfully Build Docker"'
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_SUCCESS_COLOR,
                        message: "Docker Image Build 를 성공하였습니다."
                    )
                }
                failure {
                    sh 'echo "Build Docker Fail"'
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_FAIL_COLOR,
                        message: "Docker Image Build 를 실패하였습니다.\n" +
                        "\n" +
                        "<-More info->\n" +
                        "${env.BUILD_URL}console\n" +
                        "=================================================================="
                    )
                }
            }
        }

이제 SonarQube Quality Gate Stage 밑에 Docker Image build Stage를 추가 해주도록 하겠습니다. =)

 

DOCKER_IMAGE = docker.build DOCKER_IMAGE_NAME

 

맨 처음 Stage에서 Docker_IMAGE 라는 변수에  ' ' 을 지정해주었는데 이번 Stage에서 Docker Image build 를 한 값이 담기게 됩니다.

 

Docker_IMAGE_NAME 에는 gunyoung/dev 로 지정해주었기 때문에 해당 이름으로 Image가 빌드 됩니다.

 

//Docker Image를 Docker-hub 에 push
        stage('Push Docker') {
            when {
                branch "develop"
            }
            steps {
                sh 'echo "Docker Image Push Start"'
                script {
                    docker.withRegistry(IMAGE_STORAGE, IMAGE_STORAGE_CREDENTIAL){
                    DOCKER_IMAGE.push("latest")
                    }
                }
            }
            post {
                success {
                    sh 'docker rmi $(docker images -q -f dangling=true)'
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_SUCCESS_COLOR,
                        message: "Docker registry 에 Image 를 성공적으로 push 하였습니다."
                    )
                    echo "Push Docker Success"
                }
                failure {
                    error 'This Image Push Fail'
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_FAIL_COLOR,
                        message: "Docker registry 에 Image push 를 실패하였습니다.\n" +
                        "\n" +
                        "<-More info->\n" +
                        "${env.BUILD_URL}console\n" +
                        "=================================================================="
                    )
                    echo "Push Docker Fail"
                }
            }
        }

docker.withRegistry(IMAGE_STORAGE, IMAGE_STORAGE_CREDENTIAL){
DOCKER_IMAGE.push("latest")

}

해당 코드를 통해 build 한 Docker Image를 Docker hub 에 Push 하게 됩니다.

withRegistry() 안에 담겨있는 IMAGE_STORAGE IMAGE_STORAGE_CREDENTIAL 은 맨 첫 번째 Stage인 Set Variable 에서 설정한 값이 담기게 됩니다.

 

다음으로 살펴봐야 할 것은 steps 가 끝나고 Post{} 에서 해당 Step이 성공하였을 때 에 대한 실행 명령인데 이부분에서

sh 'docker rmi $(docker images -q -f dangling=true)' 는 Jenkins Server 에서 프로젝트의 Docker Image를 생성하기 때문에 계속해서 이미지가 쌓이는 것을 방지 하기 위해 dangling=true 명령어를 이용해 image tag가 없는 이미지를 삭제하도록 하였습니다.

 

현재 코드에서 Jenkins build를 실행하고 맨처음 Image 빌드를 하였다면 아마 Post의 Success에서  error가 날 것 입니다.

 

그 이유는 현재 이미지를 처음 생성하였기 때문에 docker image 중 tag가 없는 image가 없으므로

sh 'docker rmi $(docker images -q -f dangling=true)'  명령어 에서 삭제할 이미지가 없기 때문에 에러가 납니다.

 

Jenkins 빌드를 다시 한 번 돌리게 된다면 정상적으로 docker hub로 생성한 Imgae를 Push 하는 Stage까지 성공을 하실 수 있을 겁니다.

 

이부분은 안에 조건문을 감싸 tag가 없는 이미지가 존재할 때만 위 명령어를 실행하도록 하는 방법을 추가하면 될 것 같다는 생각이 드네요... 

 

조만간 수정을 하여 다시 Github에 올리도록 하겠습니다. =)

 

만약 해당 Stage에서 빌드시 docker permission denied 나 docker.sock과 관련된 에러가 난다면 Jenkins 가 있는 EC2 인스턴스에 SSH 접속을 하여 docker login을 다시 해보시고,  위에서 진행하였던 docker 에대한 사용자 권한을 다시 확인해보시길 바랍니다. =) 

 

 

이제 Docker image를 생성하고 Docker hub로 image를 Push 하는 것 까지 모두 완료하였습니다. =)

다음 스텝은 Nginx와 spring project 가 띄워질 서버를 만들고 설정을 해보도록 하겠습니다.

 

 


 

 

5. Nginx와 spring project 컨테이너가 띄워질 EC2 인스턴스 생성 및 Docker 설치

 

Nginx와 Spring project 컨테이너가 띄워질 서버가 필요하기 때문에 AWS EC2 인스턴스를 하나 더 생성해주도록 하겠습니다.

 

만약 EC2 인스턴스 생성 방법이 헷갈리시는 분들은 해당 시리즈 1편의 글을 확인해주세요. 

 

Jenkins&Springboot CI/CD 정리(1)

Jenkins&Springboot CI/CD 정리(1) 필자가 경험한 Springboot 프로젝트와 Jenkins CI 를 활용한 CI/CD 구축에 대해 정리를 해보고자 합니다. =) 이 시리즈에서는 이미 작성된 내용에 대해선 해당 글로 대체 할 예

0andwild.tistory.com

필자의 경우 750 시간으로 제한된 EC2 freetier를 효율적으로 사용하기 위해 AWS 계정을 하나 더 만들어 작업을 진행하였습니다. =) 

 

인스턴스 생성을 완료하였다면 해당 서버에도  Docker를 설치해주도록 합시다.

 

Docker 설치의 경우 1번에서 필자가 작성한 글의 링크를 걸어두었으니 참고를하여 설치를 진행해주세요.=)

 

설치가 완료되었다면 Jenkins 서버에서 했던것과 동일하게 Docker 에대한 사용자 권한을 설정해주시면 됩니다.

 


 

 

6. Docker Compose 작성

 

 

5번의 작업이 모두 끝났다는 가정하에 Blue & Green 방식으로 띄워질 Spring project에 대한 Docker-compose 파일 작성과 Nginx 설정파일 작성을 진행해주도록 하겠습니다.

 

version: '3.1'

services:

  api:
    image: gunyoung/dev:latest

    container_name: springboot-blue-a

    environment:
      - "SPRING_PROFILES_ACTIVE=dev1"
      - LANG=ko_KR.UTF-8

    ports:
      - '8080:8081'

필자의 경우 dev환경과  prod 환경을 분리하여 서버를 띄우기 위해 docker-dev 라는 패키지와 docker-operation(=prod) 라는 패키지를 생성해주고 그안에 deploy.sh(실행 명령어 파일)과 docker compose 파일을 담아주었습니다. 해당 패키지들은 Jenkinsfile에서 작성한데로 when{develop} 또는 when{main} 이라는 branch 에 따라 파일이 해당 서버로 Publish Over SSH plugin을 통해 전송이 될 것 입니다.

 

docker-compose.yml 파일은 컨테이너를 compose 파일로 띄울 때 Compose 파일에 명시된 대로 컨테이너가 띄워지므로 내가 이 컨테이너에 어떠한 것들을 담을 것인가라는 장바구니 개념으로 생각하시면 될 것 같습니다. =)

 

Compose 파일 내에 image는 실행할 이미지이고 필자의 경우 gunyoung/dev 라는 이미지를 Jenkins 서버에서 생성하고 nginx가 있는 서버에서 해당 이미지를 필자의 docker hub로 부터 pull 하여 컨테이너를 띄울 것이기 때문에 생성한 이미지 이름을 명시해주었습니다.

 

container_name은 해당 Compose 파일로 컨테이너를 띄울 때 컨테이너 이름을 무엇으로 할 지 정하는 부분입니다.

 

여기서 조금 주의깊게 봐야할 것은 environment 설정에서 필자의 경우 application.yml 파일에서 local, dev, operation(=prod) 로 환경 분리를 해두었기 때문에 여기에서 dev1 이라는 프로필 설정을 가져오겠다고 명시를 하면 해당 컨테이너는 dev1이라는 프로필 설정값으로 컨테이너가 띄워지게 됩니다. =)

 

ports 의 경우 좌측은 외부 포트 번호로 즉, 해당 url 과 포트번호를 입력하여 서버로 붙을 port 번호 입니다. 우측의 포트 번호는 필자의 경우 application.yml 파일에서 spring profile을 local = 8080, dev1= 8081, operation(=prod)=8180 으로 주었기 때문에 해당 Compose의 environment에서 지정해준 dev1 프로필에 맞추어 8081포트를 입력해주었습니다.

 

만약 profile을 operation(=pord) 환경으로 맞추어 놓고 우측 포트번호를  8080 또는 8081 등 내부 프로필 포트번호와 다르게 맞추게되면 정상적으로 서버가 띄워지지 않으니 주의깊게 살펴보시길 바랍니다. =) 

 

필자의 깃헙에 들어가 docker compose 파일을 보시면 아시겠지만 필자는 blue compose 2개 green compose 2개 를 만들었습니다.

 

이 이유는 우선 Blue & Green 방식은 Blue 가 띄워져 있다면 Green 컨테이너에 새로운 버전을 띄운 후 스위칭하고 구버전 컨테이너인 Blue 컨테이너를 종료하는 방식입니다.

 

필자는 이번 연습에서 Nginx를 이용하여 로드밸런싱 설정을 함께 해보고 싶어 각 컨테이너를 2개 씩 띄워지도록 작업을 해보았습니다.

 

(docker compose -dev)

 

Operation(=prod) 환경의 경우 스프링 프로필 설정과 포트번호만 다르게 설정을 해주었기 때문에 따로 코드를 올리지 않도록 하곘습니다. 만약 헷갈리시거나 해당 코드를 확인해보고 싶으시다면 필자의 깃헙 코드를 참고해주세요.

 

GitHub - 0AndWild/Jenkins-CICD: Jenkins Ci tool을 활용한 CI/CD 구축

Jenkins Ci tool을 활용한 CI/CD 구축. Contribute to 0AndWild/Jenkins-CICD development by creating an account on GitHub.

github.com

 

 


 

7. Nginx config 파일 작성

 

 

 

필자의 경우 프로젝트의 root 디렉토리에 nginx/conf.d 라는 패키지를 생성해주었고 이 안에 nginx 컨테이너를 띄울 compose 파일과 nginx의 blue & green 설정파일들을 넣어주었습니다.

 

version: '3.3'
services:
  nginx:
    image: nginx
    ports:
      - '80:80'
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - /home/ec2-user/nginx/conf.d/nginx.conf:/etc/nginx/nginx.conf
    container_name: nginx-webserver

(docker-compose-nginx.yml)

 

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server {spring project가 띄워질 서버의 ip}:8080; # blue
        server {spring project가 띄워질 서버의 ip}:8081; # blue
    }

    access_log /var/log/nginx/access.log;

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
        }

    }
}

(nginx.blue.conf)

 

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server {spring project가 띄워질 서버의 ip}:8082; # green
        server {spring project가 띄워질 서버의 ip}:8083; # green
    }

    access_log /var/log/nginx/access.log;

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
        }

    }
}

(nginx.green.conf)

 

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server {spring project가 띄워질 서버의 ip}:8080; # blue
        server {spring project가 띄워질 서버의 ip}:8081; # blue
    }

    access_log /var/log/nginx/access.log;

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
        }

    }
}

(nginx.conf) - 필자의 경우 초기 nginx.conf의 설정값을 blue로 지정해주었습니다. 이후 deploy.sh에 명시된 docker cp 명령어에 따라 새로 띄워지는 컨테이너의 color의 설정값으로 변경이 됩니다. =)

 

여기서 location 의  proxy pass 를 통해 nginx 서버로 접속을 하면 http upstream backend 에서 설정해준 ip:port 로 프록시가 이루어집니다.

 

이때 필자의 경우 같은 색상의 컨테이너를 두개씩 띄워주었고 upstream에도 해당 컨테이너들이 띄워지는 ip와 외부 포트번호를 명시해주었는데 이렇게 따로 설정없이 두개를 모두 입력하면 라운드로빈 방식(요청 순서대로: 기본값) 으로 로드밸런싱이 적용이 됩니다.

 

nginx를 활용한 무중단 배포 방식은 Blue & Green 방식 외에도 롤링, 카나리 등의 방식이 존재합니다. 

로드밸런싱 또한 라운드로빈 방식 외에 least_connection, ip_hash, least_time 등 의 방식이 존재하지만 이 부분에 대해서는 따로 글을 정리하여 업로드 하도록 하겠습니다. =)

 

 

이제 Deploy.sh 파일을 작성해보도록 하겠습니다.

 

 


8. Deploy.sh 작성

 

 

 

deploy.sh 파일은 배포를 하는데 있어 필요한 sh 스크립트 명령어의 집합이라고 생각하시면 됩니다. =)

 

필자는 docker compose들이 담겨 있는 패키지 안에 deploy.sh 을 위치 시켜주었고 해당 패키지를 한 번에 jenkins 서버에서 nginx 서버로 전송해주었습니다. 이 부분은 다음 스텝에서 Jenkinsfile을 작성하면서 설명드리도록 하겠습니다. 

 

deploy.sh 파일은 주석을 통해 설명 하도록 하겠습니다. =)

#!/bin/bash    <- 해당 파일을 bash 쉘로 실행시키겠다는 의미

#필자의 경우 nginx와 spring project 컨테이너가 띄워지는 두번째 인스턴스를 ubuntu가 아닌
#AWS Linux를 사용해보았고 초기 사용자 이름이 ec2-user 로 잡혀 있습니다. 이부분은 ubuntu 로 생성하였을 경우
#ubuntu로 바꾸어주시길 바랍니다.=)
#아래의 변수에 담긴 파일 경로는 Jenkins에서 publish Over SSH 로 Nginx 서버로 보낸 파일들의 경로 입니다.
#이 경로도 Jenkins 설정에서 경로를 다르게 지정해주면 해당 경로로 파일들을 보낼 수 있습니다. 

#Nginx.conf 파일경로
NGINX_DIR=/home/ec2-user/nginx/conf.d
#Docker-compose 파일경로
DOCKER_DIR=/home/ec2-user/docker-dev
#docker 컨테이너 이름
DOCKER_APP_NAME=springboot

#nginx 컨테이너가 떠있는지 확인
EXIST_NGINX=$(docker ps | grep nginx-webserver)
#떠있지 않으면 nginx 서버를 구동한다 이미 구동중이라면 skip
if [ -z "$EXIST_NGINX" ]; then
    echo "nginx container start"
    docker-compose -p nginx-webserver -f ${NGINX_DIR}/docker-compose.nginx.yml up -d
else
    echo "nginx is already running"
fi

sleep 5

# Blue 를 기준으로 현재 떠있는 컨테이너를 체크한다.
EXIST_BLUE_A=$(docker-compose -p ${DOCKER_APP_NAME}-blue-a -f ${DOCKER_DIR}/docker-compose.blue1.yml ps --status=running | grep ${DOCKER_APP_NAME}-blue-a)
EXIST_BLUE_B=$(docker-compose -p ${DOCKER_APP_NAME}-blue-b -f ${DOCKER_DIR}/docker-compose.blue2.yml ps --status=running | grep ${DOCKER_APP_NAME}-blue-b)

# 컨테이너 스위칭
if [ -z "$EXIST_BLUE_A" ] && [ -z "$EXIST_BLUE_B" ]; then
    echo "blue up"
    docker-compose -p ${DOCKER_APP_NAME}-blue-a -f ${DOCKER_DIR}/docker-compose.blue1.yml up -d
    docker-compose -p ${DOCKER_APP_NAME}-blue-b -f ${DOCKER_DIR}/docker-compose.blue2.yml up -d
    IDLE_PORT=8080
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
else
    echo "green up"
    docker-compose -p ${DOCKER_APP_NAME}-green-a -f ${DOCKER_DIR}/docker-compose.green1.yml up -d
    docker-compose -p ${DOCKER_APP_NAME}-green-b -f ${DOCKER_DIR}/docker-compose.green2.yml up -d
    IDLE_PORT=8082
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
fi

sleep 5

# 새로운 컨테이너가 제대로 떴는지 확인
EXIST_AFTER_A=$(docker-compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR}-a -f ${DOCKER_DIR}/docker-compose.${AFTER_COMPOSE_COLOR}"1".yml ps --status=running | grep ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR}-a)
EXIST_AFTER_B=$(docker-compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR}-b -f ${DOCKER_DIR}/docker-compose.${AFTER_COMPOSE_COLOR}"2".yml ps --status=running | grep ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR}-b)
if [ -n "$EXIST_AFTER_A" ] && [ -n "$EXIST_AFTER_B" ]; then
  #새로운 컨테이너가 뜬 것을 확인 후 Health Check를 통해 해당 spring poject 컨테이너가 잘 실행 되었는지 체크를 진행합니다.
  # health check
  echo "> Health Check Start!"
  echo "> IDLE_PORT: $IDLE_PORT"
  echo "> curl -s http://{Spring project 컨테이너가 띄어지는 서버 ip}:$IDLE_PORT "
  sleep 5

  for RETRY_COUNT in {1..10}
  do
    RESPONSE=$(curl -s http://3.36.66.225:${IDLE_PORT})
    UP_COUNT=$(echo ${RESPONSE} | grep "timestamp" | wc -l)

    if [ ${UP_COUNT} -ge 1 ]
    then # $up_count >= 1
        echo "> Health check 성공"
        #Health check가 정상적으로 성공 된 후 nginx 설정값을 변경해주어야 서버가 다운타임 없이 배포가 진행됩니다.
        # nginx.config를 컨테이너에 맞게 변경해주고 reload 한다
        cp ${NGINX_DIR}/nginx.${AFTER_COMPOSE_COLOR}.conf ${NGINX_DIR}/nginx.conf
        #서버를중단하지 않고 변경된 사항을 적용시켜줌
        docker exec nginx-webserver nginx -s reload

        sleep 5

        # 이전 컨테이너 종료
        docker-compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}-a -f ${DOCKER_DIR}/docker-compose.${BEFORE_COMPOSE_COLOR}"1".yml down
        docker-compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}-b -f ${DOCKER_DIR}/docker-compose.${BEFORE_COMPOSE_COLOR}"2".yml down
        echo "$BEFORE_COMPOSE_COLOR down"
    break

    else
          echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
          echo "> Health check: ${RESPONSE}"
    fi

    if [ ${RETRY_COUNT} -eq 10 ]
      then
        echo "> Health check 실패. "
        echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
        exit 1
      fi

      echo "> Health check 연결 실패. 재시도..."
      sleep 7
    done

else
  echo "> 새로운 ${AFTER_COMPOSE_COLOR} 컨테이너가 정상적으로 띄워지지 않았습니다."
fi

 

프로젝트 내에서의 모든 설정은 끝이 났습니다. =)

이제 Jenkins 서버로 돌아가 Publish Over SSH plugin 을 설치해주도록 하겠습니다.

 

 


 

 

9. Jenkins 서버 Publish Over SSH plugin 설치 및 설정

 

 

Jenkins 관리 - Plugin Manager에 들어가 Publish Over SSH 라는 플러그인을 설치해주도록 하겠습니다.

 

해당 이미지는 필자가 이전에 ssh-keygen 명령어를 사용할 때 캡쳐한 이미지 이므로 ubuntu로 되어 있습니다.

$ ssh-keygen -t rsa

그 다음 Jenkins Server 에서 위 명령어를 통해 RSA 방식으로 개인키와 공개키 한 쌍을 생성하도록 하겠습니다.

cd ~/.ssh
sudo cat id_rsa.pub

해당 경로로 들어가서 ls 를 통해 파일을 확인해보면 개인키와 .pub이 붙은 공개키가 생성된 것을 확인할 수 있습니다.

이제 해당 공개키를 복사하여 nginx 서버에 등록해주도록 하겠습니다. =)

 

sudo vim authorized_keys

필자는 vim 편집기를 사용하여 Nginx 서버의 authoraized_key 파일의 기존 공개키 아래에 Jenkins에서 생성한 공개키를 추가해주었습니다. =)

 

vim 편집기 사용시 알파벳 O 를 누르면 텍스트가 끝난 지점 다음 줄에서 입력을 시작할 수 있습니다. 복사한 공개키를 붙여넣어 주시고 esc 를 누른 후 :w 를 입력하여 저장한 후 :q 를 입력하여 빠져나와 주도록 합니다.

 

 

이제 Jenkins 서버 설정에 들어와 Publish Over SSH 설정에서 Jenkins 서버에서 생성한 RSA 키 중 개인키를 복사하여 붙여넣어주도록 하겠습니다. =)

 

 

이제 아래 Name 은 Jenkins file에서 사용할 ID 값을 입력한 후 HostName은 Nginx 서버의 IP 주소를 넣어주도록 합니다.

Username 은 Nginx 서버의 Username을 입력하면 되는데 Ubuntu 인스턴스의 경우는 ubuntu를 입력하여 주시면 됩니다.

(만약 이름을 변경하셨다면 변경하신 이름을 입력해주세요)

 

Remote Directory는 이제 Jenkins서버에서 보낼 파일을 Nginx 서버의 어느 경로에 위치시킬거냐 라고 생각하시면 됩니다. =)

 

모든 설정이 끝났다면 이제 Test Configuration 을 진행해주시고 Success가 뜨는지 꼭 확인해 주세요!

 

 


 

10. Jenkinsfile 작성 (Publish Over SSH 를 이용한 Nginx 서버의 Docker pull & deploy.sh 실행)

 

 

//nginx 패키지와 docker 패키지를 배포할서버로 보낸 후 deploy.sh을 실행
        stage('Remote Server Docker Pull') {
            steps([$class: 'BapSshPromotionPublisherPlugin']) {
                sh 'echo "Remote Server Docker Pull Start"'
                 sshPublisher(
                    continueOnError: false, failOnError: true,
                    publishers: [
                        sshPublisherDesc(
                            configName: "springboot-remote-server",
                            verbose: true,
                            transfers: [
                                sshTransfer(
                                    execCommand: "docker pull gunyoung/dev:latest"
                                )
                            ]
                        )
                    ]
                 )
            }
            post {
                success {
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_SUCCESS_COLOR,
                        message: "Docker pull 을 성공하였습니다."
                    )
                    echo "Completed Remote Server Docker pull"
                }
                failure {
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_FAIL_COLOR,
                        message: "Docker pull 을 실패하였습니다.\n" +
                        "\n" +
                        "<-More info->\n" +
                        "${env.BUILD_URL}console\n" +
                        "=================================================================="
                    )
                    echo "Fail Remote Server Docker Pull"
                }
            }
        }

 sshPublisherDesc(
                            configName: "springboot-remote-server",
                            verbose: true,
                            transfers: [
                                sshTransfer(
                                    execCommand: "docker pull gunyoung/dev:latest"
                                )

 

위 코드에서 configName은 Jenkins 서버에서 설정해준 Publish Over SSH의 Name입니다.

 

execCommand 에서는 원격 접속한 서버에서 실행할 명령어를 입력해주는 곳입니다. =)

docker pull  명령어를 통해 docker hub의 개인 repositopry로부터 push 해준 이미지를 받아오도록 하겠습니다.

 

//(dev 환경)nginx 패키지와 docker 패키지를 배포할서버로 보낸 후 deploy.sh을 실행
        stage('Dev-Remote Server Exec deploy.sh') {
            when {
                branch "develop"
            }
            steps([$class: 'BapSshPromotionPublisherPlugin']) {
                sh 'echo "Remote Server Deploy start"'
                 sshPublisher(
                    continueOnError: false, failOnError: true,
                    publishers: [
                        sshPublisherDesc(
                            configName: "springboot-remote-server",
                            verbose: true,
                            transfers: [
                                sshTransfer(
                                    sourceFiles:"nginx/**",
                                ),
                                sshTransfer(
                                    sourceFiles:"docker-dev/**",
                                    execCommand: "chmod +x /home/ec2-user/docker-dev/deploy.sh"
                                ),
                                sshTransfer(
                                    execCommand: "/home/ec2-user/docker-dev/deploy.sh"
                                ),
                                sshTransfer(
                                    execCommand: 'docker image prune -f --filter="dangling=true"'
                                )
                            ]
                        )
                    ]
                 )
            }
            post {
                success {
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_SUCCESS_COLOR,
                        message: "dev 환경 배포를를 성공하였습니다."
                    )
                    echo "Completed Remote Server Deploy"
                }
                failure {
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_FAIL_COLOR,
                        message: "dev 환경 배포를 실패하였습니다.\n" +
                        "\n" +
                        "<-More info->\n" +
                        "${env.BUILD_URL}console\n" +
                        "=================================================================="
                    )
                    echo "Fail Remote Server Deploy"
                }
            }
        }

 

다음  Stage는 nginx 설정파일이 담긴 nginx 패키지와 deploy.sh과 docker compose 파일이 담긴 docker 패키지를 원격 서버로 보내줍니다.

 

execCommand: "chmod +x /home/ec2-user/docker-dev/deploy.sh"

 

그다음 위 명령어를 수행하여 deploy.sh파일을 실행시킬 수 있게 x권한(실행권한)을 위임해 줍니다.

 

execCommand: "/home/ec2-user/docker-dev/deploy.sh"

 

실행권한을 위임한 후 deploy.sh을 실행합니다.

execCommand: 'docker image prune -f --filter="dangling=true"'

 

그다음 Nginx 서버에서도 tag가 사라진 불필요한 이미지가 메모리 공간을 차지하지 않도록 제거해줍니다. =)

 

 

이렇게 모든 Jenkins CI/CD 작업이 모두 완료 되었습니다!

 

 

 


 

 

 

이제 git push 또는 merge를 통해 Jenkins job을 trigger 하면 다음과 같이 정상적으로 빌드가 되고 Slack으로 Stage별 알림이 오는 것을 확인 할 수 있습니다. =)

 

 

정상적으로 Container 가 뜨는 것을 확인할 수 있습니다. =)

 

 

다음 마지막 4편에서는 오픈소스인 Sonar-bot을 활용하여 Gitea의 PR comment에 자동으로 SonarQube 분석 결과를 남기는 작업을 진행해보도록 하겠습니다. 

 

 

4편의 내용은 Gitea를 사용할 경우에만 해당이 되니 만약 Github을 사용할 경우에는 아래 참고 블로그 링크를 통해 작업을 진행해보시길 바랍니다! =)

 

 

[Server] 소나큐브(SonarQube) 커뮤니티 무료 버전에서 PR 데코레이션(Pull Request Decoration) 설정 적용하

SonarQube 유료 버전의 기능으로 Pull Request에 대해 정적 분석 코멘트를 남겨주는 Pull Request Decoration이 있습니다. 하지만 무료 플러그인을 사용하면 유료 버전이 아니여도 해당 기능을 사용할 수 있

mangkyu.tistory.com

 

 

 

 

 

728x90
반응형
복사했습니다!