Synology + Jenkins 배포

heyhey·2023년 4월 13일
0

infra

목록 보기
2/3

배포에 성공하면 이글을 적어야지 생각이 블로그를 적기까지 정말 오래걸렸습니다.ㅎㅎ 젠킨스 테스트만 120번 이라니..
하루에 두시간, 총 3주가 걸린 저의 배포 노트 입니다


프로젝트 생성

스프링 부트 프로젝트를 생성하기 위해 start.spring.io 를 이용했습니다.

spec

project - gradle Groovy
language - Java
Spring Boot - 2.7.9

Project Metadata
Group - com.dubu
Artifact - party
Name - party

package name - com.dubu.party

Packaging - Jar
Java - 11

Dependencies

  • Spring Web
  • MariaDB
  • Spring Boot DevTools
  • JPA
  • Lombok

스프링부트를 3.1.0 버전을 이용하려고 했더니, JAVA 버전이 17이상이 필요했습니다.

이미 11버전으로 JAVA_HOME 도 설정되어있고 해서 굳이 3.1.0 을 사용하긴 좀 귀찮달까..? 그래서 2.7.9를 이용한 것도 있습니다.
(다음 프로젝트는 자바 버전 올리면서 3.~ 대로 올라가야지)

JAVA 관련 세팅

JAVA 11 버전 다운을 받아줍니다.
https://www.oracle.com/java/technologies/downloads/#jdk11-mac

  1. JAVA_HOME
    vi ~/.bash_profile bash를 이용한다면 JAVA_HOME 을 변경해줘야합니다.
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-11.0.17.jdk/Contents/Home

PATH="$PATH:$JAVA_HOME/bin"
  1. Intellij Java 세팅 변경
    file > project structure > sdk 11 선택
    preference > build > build tools > gradle > gradle jvm 변경

DataBase

Maria DB

DataBase로 Maria DB를 이용했습니다.
원래 MySQL을 사용하고 있어서 사용하려고 했는데, 몇가지 이유로 Maria DB로 변경하게 되었습니다.

  • 시놀로지 환경
    - 시놀로지에서 mySQL을 지원하긴 하지만, Maria를 조금 더 쉽게 사용할 수 있다는 점이 메리트가 있었습니다.

  • Maria DB는 Mysql을 기반으로 만들어진 DB이기 때문에 개선된 성능을 제공합니다.
    - 쿼리 최적화 기능

    • 빠른 쿼리 실행속도
    • 스레드 , 캐시 , 인덱스 등 최적화
      • 더 많은 엔진 제공으로 효율적인 데이터 관리가 가능
        - 기능 추가 (JSON,XML 등.. ) 의 기능이 추가되어 다양한 데이터 관리를 가능하게 합니다.
    • 보안 강화 (보안 관련 문제에 대한 처리속도 빠름)

Maria DB 실행

시놀로지에서 Maria DB를 실행하는 법
1. 패키지 센터에서 "Maria DB" 를 설치합니다.
2. 저는 3307번을 사용하기 때문에, 방화벽과 포트포워딩을 열어둡니다.
3. phpmyadmin을 다운받고 로그인 합니다.
4. 권한 탭에서 사용자를 추가하고 설정합니다. 이 과정이 안되면 아래와 같이 합니다.

마리아 DB에 루트로 로그인을 합니다.

CREATE USER '유저네임'@'%' IDENTIFIED BY '패스워드';
'%'는 어떤 호스트(또는 IP)에서든 접근 가능하도록 하는 것입니다.

GRANT ALL PRIVILEGES ON *.* TO 'user'@'%';
해당 사용자에게 권한을 부여하기 위해서는 다음과 같은 SQL문을 실행합니다.

  • database 이름을 dubu 로 접근하려고 하는데 이 이름의 database가 없으면 에러가 나기 때문에, 새로 database를 만들어 줘야 합니다.
    CREATE DATABASE dubu

이렇게 해서 생겨난 build.gradle 파일 입니다.

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.9' // Spring Boot 2.7.9
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.dubu'
version = '0.0.1-SNAPSHOT' // Spring Boot 2.7.9 requires Java 11
sourceCompatibility = '11' // Java 11

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
	maven { url 'https://repo.spring.io/milestone' }
	maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

이 파일에서 특별한 내용은 없습니다. 이제 새로운 dependency가 필요할 때마다, 추가를 해서 사용하면됩니다.

DockerFile

지금부터 진짜 배포에 관한 내용이 시작됩니다.

FROM openjdk:11-jdk 
COPY build/libs/server-0.0.1-SNAPSHOT.jar app.jar 
ENTRYPOINT ["java","-jar","app.jar"] 
EXPOSE 3333

FROM openjdk:11-jdk
11-jdk 의 이미지를 사용해 도커를 실행시킵니다.

COPY build/libs/server-0.0.1-SNAPSHOT.jar app.jar

build/libs/server-0.0.1-SNAPSHOT.jar 파일을 app.jar 라는 파일로 복사합니다.

EXPOSE 3333
3333 포트를 사용합니다.

ENTRYPOINT ["java","-jar","app.jar"]
docker file을 RUN 하게 되면 부르는 명령어입니다.
방금 만든 app.jar 파일을 java -jar app.jar 로 실행합니다.

⚠️ docker run 오류

docker 이미지를 실행할 때 계속 오류가 나는데 app.jar 파일을 제대로 실행하는지 확인할 수가 없었습니다
또한 생성한 도커 이미지로 컨테이너를 실행하면 자꾸 꺼지게 되어 확인이 어려웠습니다.

해결방법
docker run -it --entrypoint /bin/bash dubu-server
"dubu-server"라는 이미지를 실행하며, 컨테이너 내부로 들어가서 bash 쉘을 실행하는 명령어입니다. 따라서 해당 이미지 내부에서 작업할 수 있게 됩니다

ls 명령어를 이용해 app.jar 파일이 생성된 것을 확인했습니다.

그래서 이 파일을 그냥 java -jar app.jar으로 돌려봤는데 잘 돌아갑니다. (읭??)

원래는 ENTRYPOINT 도 전의 프로젝트의 배포 파일을 가져와 ENTRYPOINT ["java","-jar","app.jar","--spring.config.name=application-prod"] 로 적어놨던 것이 문제였습니다.
application-prod 라는 이름의 설정 파일을 실행시키게 되는 것인데, 저 파일이 없어서 에러가 났던 것이었습니다. (절대 몰랐지..)

그래서 ENTRYPOINT ["java","-jar","app.jar"] 으로 고쳐서 올리게 되니 해결 되었습니다.

application.yml

스프링 부트 프레임워크에서 사용하는 설정파일 입니다.
이 파일을 사용하여 애플리케이션의 설정 정보를 저장하고 로드합니다.
데이터베이스 연결, 서버 포트 설정, 로깅 관련 등의 정보를 포함합니다.

위의 --spring.config.name=application-prod" 처럼 여러 프로파일을 지원하므로
여러개의 환경에 대해 각각 다른 정보를 제공할 수 있습니다.

spring:
  datasource:
    # DB 드라이버
    driver-class-name: org.mariadb.jdbc.Driver
    # DB 접속 주소 jdbc:mysql://{IP}:{PORT}/{DB 이름}
    url: jdbc:mariadb://heyhey.i234.me:3307/dubu?autoReconnect=true&characterEncoding=UTF-8
    # DB 접속 계정
    username: user
    password: ****
  jpa: # JPA 설정
    database-platform: org.hibernate.dialect.MariaDBDialect # DB 방언
    show-sql: true # SQL 쿼리 출력 여부
    hibernate: # 하이버네이트 설정
      ddl-auto:  update  # 스키마 자동 생성 (create, create-drop, update, validate)
      format_sql: true  # SQL pretty print

server: # 서버 설정
  port : 3333 # 서버 포트

DB

DB 와 관련된 내용을 정의합니다.

driver-class-name: org.mariadb.jdbc.Driver
mariadb 드라이버를 사용합니다. 이 값은 jdbc에서 알아서 자동으로 등록도 해준다고 하기 때문에 생략도 가능합니다.

url: jdbc:mariadb://heyhey.i234.me:3307/dubu?autoReconnect=true&characterEncoding=UTF-8
DB 접속 주소 입니다. {IP} : {PORT} / {DB이름} 순으로 작성합니다.

username: user password: ****
db에 접속하기 위한 username,password 를 입력합니다.
참고로 기본적으로 외부 접속은 root로 접근이 불가하기 때문에 유저를 만들어줘야 합니다.


Jenkins

대망의 젠킨스입니다.
Jenkins는 CI/CD를 위한 배포 자동화 도구 입니다.
자동화 빌드, 테스트, 배포가 가능하기 때문에 사용합니다.

젠킨스 실행

  1. 시놀로지 패키지 센터에서 도커를 설치합니다.
  2. 레지스트리에서 jenkins 를 다운받습니다.
  3. 다운 받으면 이미지가 만들어지는데 그 이미지를 실행시켜 젠킨스가 돌아가는 컨테이너를 만들어줍니다. 저는 로컬 40000, 컨테이너 8080으로 만들어줬습니다.
  4. 정상적으로 실행이 된다면 host:40000로 접근이 가능해지게됩니다.

젠킨스 설정

새로운 Item을 생성해줍니다. 세팅 내용은 아래와 같습니다.

이렇게 하면 새로운 Item이랑 git이랑 연동이 됐습니다.
지금 빌드를 눌러, 제대로 동작하는지 확인이 가능합니다.(아직 파일이 없으니 나중에)

Gitghub web hook

github에서 원하는 브랜치에 푸시가 되면 자동 빌드/배포가 되게 하기 위해서 webhook 설정을 해줍니다.
원하는 레포지토리에 가서 > setting > webhooks 에서 아래와 같이 세팅을 해줍니다.
주소 : ⭐️ http://{host}:40000/github-webhook/

  • application/json
  • Just the push event
  • Active

여기서 주소를 40000까지 쓰는것이 아닌 github-webhook을 추가를 해줘야 합니다.
이제 준비는 끝났습니다. (사실 이제 시작) Jenkinsfile을 생성합시다.

Jenkinsfile

pipeline {
    agent any

    environment {
        IMAGE_NAME = "dubu-server"
        DOCKER_IMAGE = "bbnerino/dubu-server"
        DOCKER_CREDENTIALS = "docker-hub"
        DOCKER_REGISTRY = "https://index.docker.io/v1/"
        TARGET_HOST = "bbnerino@heyhey.i234.me"
        ContainerPort = "3333"
        LocalPort = "3333"
        DOCKER_USER="bbnerino"
        DOCKER_PASS="****"
    }

    stages {
        stage('Build') {
            steps {
                sh 'cd server && ./gradlew build'
            }
        }
        stage('Dockerize') {
            steps {
                sh 'cd server && docker build -t ${IMAGE_NAME} -f Dockerfile .'
                sh 'docker tag ${IMAGE_NAME} ${DOCKER_IMAGE}'
            }
        }
        stage('Push to Registry') {
            steps {
                withDockerRegistry([credentialsId: "${DOCKER_CREDENTIALS}", url: "${DOCKER_REGISTRY}"]) {
                    sh 'docker push ${DOCKER_IMAGE}'
                }
            }
        }
        stage('Deploy') {
            steps {
                sh """
                    ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ${TARGET_HOST} '
                        export PATH=$PATH:/usr/bin
                        docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}
                        docker pull ${DOCKER_IMAGE}
                        docker stop ${IMAGE_NAME} || true
                        docker rm ${IMAGE_NAME} || true
                        docker run -d --name ${IMAGE_NAME} -p ${LocalPort}:${ContainerPort} ${DOCKER_IMAGE}
                    '
                """
            }
        }
    }
}

파이프라인 순서는
Build - Dockerize - Push to Registry - Deploy
총 네단계로 나눴습니다. 추후 test도 추가할 예정입니다.

Build

stage('Build') {
	steps {
		sh 'cd server && ./gradlew build'
	}
}

Build 단계로, gradlew build 를 이용해 build.gradle의 내용을 build합니다. 참고로 현재 위치가 root 안에 server 에 프로젝트가 위치해 있기 떄문에 cd 를 통해 위치를 이동했습니다.
저렇게 빌드를 하게 되면 build/libs/server-0.0.1-SNAPSHOT.jar 파일이 생성됩니다.

Dockerize

stage('Dockerize') {
    steps {
        sh 'cd server && docker build -t dubu-server -f Dockerfile .'
        sh 'docker tag dubu-server bbnerino/dubu-server'
    }
}

Docker 로 만드는 과정입니다.
다시 server로 이동한 다음, 현재 디렉토리에 있는 Dockerfile을 사용하여 dubu-server으로 지정된 이름을 가진 Docker 이미지를 빌드합니다.
그리고 DockerHub에 올리기 위해 태그를 아이디@이미지이름 형태로 추가해줍니다.

Push to Registry

steps {
    withDockerRegistry([credentialsId: "${DOCKER_CREDENTIALS}", url: "${DOCKER_REGISTRY}"]) {
        sh 'docker push dubu-server'
    }
}

Docker 허브에 올려줍니다.
docker push 를 통해 아까 만들었던 dubu-server를 올려줍니다.

withDockerRegistry() 함수에 대해 알아봅니다.

DOCKER_REGISTRY = https://index.docker.io/v1/ 고정이며
DOCKER_CREDENTIALS = Jenkins에서 dockerRegistry 등록을 할 때 적는 ID

Jenkins에서 Docker Hub에 접근할때, login을 해주는 Credentials 을 제공해줍니다.
jenkins > 관리 > Mangae Credentials > global 선택 > Add Credentials

설정 내용
kind : Username with password
scope : global
username : [docker hub 아이디]
password : [docker hub 비밀번호]
ID : DOCKER_CREDENTIALS 에서 사용할 이름. 저는 docker-hub 라고 했습니다.

Deploy

stage('Deploy') {
    steps {
        sh """
            ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ${TARGET_HOST} '
                export PATH=$PATH:/usr/bin
                docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}
                docker pull ${DOCKER_IMAGE}
                docker stop ${IMAGE_NAME} || true
                docker rm ${IMAGE_NAME} || true
                docker run -d --name ${IMAGE_NAME} -p ${LocalPort}:${ContainerPort} ${DOCKER_IMAGE}
            '
        """
    }
}

방법은 이렇습니다. ssh를 통해 원격 호스트로 접속합니다. 우리가 서버를 배포할 호스트로 이동합니다.
( 평소 ssh 로 접속하는 서버 bbnerino@heyhey.i234.me)( 포트는 22번이여서 저는 생략가능, 있으면 -p)
자세한 내용은 추가로 적어놓겠습니다.

export PATH=$PATH:/usr/bin
디렉토리를 PATH에 추가합니다.

docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}
그다음 docker로 로그인을 합니다. jenkins 환경이 아니라서 login을 따로 해줘야 한다고 이해했습니다.

docker pull ${DOCKER_IMAGE}
올려놓은 이미지를 가져옵니다.

docker stop ${IMAGE_NAME} || true
기존 컨테이너를 중지합니다.

docker rm ${IMAGE_NAME} || true
기존 컨테이너를 삭제까지 해줍니다.

docker run -d --name ${IMAGE_NAME} -p ${LocalPort}:${ContainerPort} ${DOCKER_IMAGE}
새 컨테이너를 백그라운드 환경에서 시작합니다 . 저는 3333포트를 사용해서 사용했습니다.

⚠️ ssh 로 docker가 권한이 없는 오류
ssh 로 접근하여 docker를 실행하려고 하는데, 같은 유저가 아니라? 아니면 다른 환경이라? 접근이 불가하다고 합니다. 그래서 누구나 사용가능하게 docker를 사용하기 위해 docker의 권한을 바꿔줍니다.
jenkins라는 유저가 사용하기 때문에 host에서 jenkins 유저로 변경한 뒤
sudo chmod 777 /var/run/docker.sock
docker 데몬에 접근할 수 있는 권한을 전부 부여합니다. 위험한 방법이긴 한데, usermod -aG 를 사용했더니 오류가 나더라구요..

ssh 접근하기

ssh 로 원격 호스트에 접근을 하게 되면, 원래 비밀번호를 물어보는 것이 원칙입니다.
하지만 자동으로 넘어가야 하기 때문에 비밀번호 없이 개인키 검사를 통해 넘어갈 수 있도록 합니다.
원격 호스트에 접속할 때 사용되는 키는 개인키와 공개키가 있습니다.
개인키로 암호화된 데이터는 공개키로만 복호화할 수 있습니다.
이제 두 호스트간에 키를 복사하면 호스트키 검사를 생략할 수 있습니다.

  1. 젠킨스에서 키를 생성합니다.
    ssh-keygen
    이렇게 만들면 ~/.ssh/ 폴더에 id_rsa , id_rsa.pub 파일이 생성됩니다.

  2. 공개키를 호스트로 복사해서 보내줍니다.
    ssh-copy-id -i ~/.ssh/id_rsa.pub [사용자명]@[호스트]
    이렇게 만들면 호스트에 ~/.ssh/ 폴더에 authorized_keys 가 들어오게 됩니다.

  3. 호스트에서 권한 설정을 해줍니다.
    원래라면 이 파일만 있으면 되지만,권한이 없다면 저 파일에 접근이 안됩니다.

chmod 755 /var/services/homes/abc
chmod 700 /var/services/homes/abc/.ssh
chmod 600 /var/services/homes/abc/.ssh/authorized_keys
  1. 설정 파일을 수정해야합니다.
    sudo vi /etc/ssh/sshd_config

PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

  1. sshd 서비스를 재시작합니다.
    synoservicectl --restart sshd

이렇게 까지하면 ssh 접속이 가능해질 겁니다.

배포하며 주의해야할 점

  • 포트포워딩 확인
  • 방화벽 확인
  • 권한 확인
  • 안된다 싶으면, 파일을 바꾸지 말고, 해당 컨테이너에 접속해서 파일 열어보기

느낀점

허엄... 정말 지옥같은 나날들이었달까, 프론트처럼 눈에 보이지도 않고, 에러도 안뜨고 그냥 꺼져버리는 상황이 많아서, 여러번의 시도가 필요했습니다.

Jenkins 배포만 120번 정도 했네요.. ㅎㅎ
정확한 이해 없이 따라하기만 하니 힘들었다고 생각합니다.
원리부터 알고 순서를 따라가다 보면 이해가 되고 있는 저를 보니 성장한게 느껴집니다 😀

profile
주경야독

0개의 댓글