Jenkins + Git Submodule

appti·2023년 6월 2일
3

학습 로그

목록 보기
8/8

목표

다음과 같은 세팅을 목표로 합니다.

  • Git Submodule을 활용한 배포 서버 전용 외부 설정 파일(application-prod.yml) 분리 및 형상관리
  • 빌드 스크립트 형상 관리
  • 도커 + 젠킨스를 통한 배포 자동화
    • 상세한 이해가 아닌 단순 사용이 목표

제약 조건

현재 사용하고 있는 EC2의 제약 조건은 다음과 같습니다.

  • t4g.micro
  • 캠퍼스에서만 ssh 접속 가능
  • 8080, 8081 포트만 제약 없이 접속 가능
  • 별도의 설정 커스터마이징 불가능

그러므로 8080은 스프링 부트 애플리케이션 포트로, 8081은 젠킨스의 포트로 사용합니다.

Git Submodule

Git 서브모듈Git Repository 내에서 다른 Git Repository를 포함하는 방법입니다.

다양한 용도로 사용되지만, 해당 글에서는 민감한 정보가 담긴 외부 설정 파일인 application.yml 분리 및 형상 관리의 목적으로 사용합니다.

Private Repository 생성

root directory에 다음과 같이 배포 환경에서의 외부 설정 파일 application-prod.yml을 생성했습니다.

spring:
  config:
    activate:
      on-profile: prod
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: (jdbc url)
    username: (usename)
    password: (password)
log:
  directory: /home/ubuntu/log

Private Repository이기 때문에, 환경 변수로 분리할 필요 없이 바로 입력할 수 있습니다.

Main Repository에 Submodule 적용

Main Repository에서 다음과 같은 명령어로 서브모듈을 등록할 수 있습니다.

git submodule add -b {branch 명} {Repository URL} {Directory} 

여기서 서브모듈을 세팅할 디렉토리에서 고려할 부분이 있습니다.


다음과 같은 전략을 고려할 수 있습니다.

  • application.yml에 서브모듈에서 관리하는 application-prod.ymlimport
  • EC2jar 파일 뿐만 아니라 서브모듈에서 관리하는 application-prod.yml까지 배포
    • --spring.config.location 지정
    • jar과 같은 디렉토리에 위치시켜 자동으로 적용

제 경우 import하기로 결정했습니다.

이 경우 src/main/resources에 존재하는 application.yml에서 import를 하면 애플리케이션 실행 시 resources 외부로 접근할 수 없기 때문에, src/main/resources 내부에 서브 모듈을 위치시켜야 합니다.


그러므로 다음과 같이 서브모듈을 설정했습니다.

git submodule add -b main https://github.com/apptie/jwp-shopping-order-sub src/main/resources/properties

그럼 다음과 같이 .gitmodules가 생성됩니다.

[submodule "src/main/resources/properties"]
	path = src/main/resources/properties
	url = https://github.com/apptie/jwp-shopping-order-sub
	branch = main

그리고 서브모듈 저장소도 지정한 디렉토리에 생성됩니다.

모두 커밋해주시면 됩니다.

정상적으로 적용되었다면 다음과 같이 서브모듈이 적용된 것을 확인할 수 있습니다.

애플리케이션

application.yml

다음과 같이 application.yml을 작성했습니다.

spring:
  profiles:
    default: local
  config:
    import: classpath:/properties/application-prod.yml
---
spring:
  config:
    activate:
      on-profile: local
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
  h2:
    console:
      enabled: true
server:
  port: 8080

아무런 Profile도 지정하지 않는 경우 local로 동작하도록 설정했으며, import를 통해 서브모듈에 정의한 application-prod.yml을 가져오도록 설정했습니다.

빌드 스크립트

다음과 같이 빌드 스크립트를 작성했습니다.

#!/bin/bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
APPNAME="jwp-shopping-order"
APPDIR=${ABSDIR}/${APPNAME}

echo "구동중인 애플리케이션을 확인합니다."

CURRENT_PID=$(pgrep -f ${APPNAME}.jar)

if [ ! -z ${CURRENT_PID} ]; then
        echo "기존 애플리케이션이 실행중이므로 종료합니다."
        kill -15 ${CURRENT_PID}
        sleep 5
fi

echo "애플리케이션을 실행합니다."

nohup java -jar ${ABSDIR}/${APPNAME}.jar --spring.profiles.active=prod 1>> build.log 2>> build_error.log &

운영 환경인 EC2에서는 prod profile로 동작하도록 지정했습니다.

EC2

Swap 메모리 설정

t4g.micro를 사용해야 하기 때문에, 도커 + 젠킨스 사용 도중 빌드를 수행하면 EC2가 죽어버릴 수 있습니다.

그렇기 때문에 Swap 메모리를 통해 임시로 메모리 문제를 해결했습니다.

권장하는 SWAP 메모리의 크기는 다음과 같습니다.

# dd 명령어를 통해 swap 메모리 할당
# 시간이 1분 ~ 5분정도 걸릴 수 있음
# 크기는 2GB(128MB x 16)
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
16+0 records in
16+0 records out
2147483648 bytes (2.1 GB) copied, 15.8767 s, 135 MB/s

# swap 파일의 읽기 및 쓰기 권한 업데이트
sudo chmod 600 /swapfile

# Ubuntu swap 영역 설정
sudo mkswap /swapfile
Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
no label, UUID=22c80ff2-8555-48cd-91ce-921d45237086

# swap 공간에 swap file을 추가해 즉시 사용할 수 있도록 설정
sudo swapon /swapfile

# 정상적으로 설정되었는지 확인
sudo swapon -s
Filename				Type		Size	Used	Priority
/swapfile                              	file    	2097148	0	-2

# fstab에 /swapfile 설정 추가
sudo vi /etc/fstab
/swapfile swap swap defaults 0 0

# 정상적으로 적용되었는지 확인
free
              total        used        free      shared  buff/cache   available
Mem:         987700      419664      242604         500      325432      425528
Swap:       2097148           0     2097148

Swapfree의 값을 통해 정상적으로 적용되었는지 확인할 수 있습니다.

Docker 설치

젠킨스를 사용하기 위해 EC2에 관련 의존성을 설정해주는 것 보다는, 도커를 활용하는 것이 더 편하기 때문에 도커를 설치했습니다.

설치 방법은 docker 공식 문서에서 확인하시는 편이 가장 확실합니다.

제가 입력한 스크립트는 다음과 같습니다.

sudo apt-get update
sudo apt-get install ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Jenkins Docker Image 다운로드 & 실행

다음 명령어를 통해 젠킨스 도커 이미지를 다운받고 도커 컨테이너를 실행해주었습니다.

sudo docker run -d --restart always -p 8081:8080 -v /jenkins:/var/jenkins_home -e TZ=Asia/Seoul --user root --name jenkins jenkins/jenkins:jdk11
  • --restart always : 문제가 발생해 컨테이너가 종료될 경우 항상 재시작
  • -p 8081:8080 : 내부 포트 80808081 포트와 바인딩
  • -v /jenkins:/var/jenkins_home : /var/jenkins_home/jenkins Directory로 볼륨 바인딩
  • -e TZ=Asia/Seoul : 시간대 설정
  • --user root : root 권한으로 실행
  • -name jenkins : 도커 컨테이너 이름 지정

Jenkins

Jenkins 초기 설정

http://ec2-ip:8081으로 접속합니다.

첫 접속 시, 비밀번호를 요구합니다.
다음을 통해 확인할 수 있습니다.

# 마운트한 디렉토리에 접근해 비밀번호 출력
cat /jenkins/secrets/initialAdminPassword

비밀번호를 입력해줍니다.

Install suggested plugins를 선택합니다.

설치가 진행됩니다.

설치가 끝나면 관리자 계정을 생성할 수 있습니다.

젠킨스의 URL을 입력합니다.
변경할 필요 없이 기본으로 설정되어 있습니다.

EC2 .pem 등록

Jenkins 관리 - Plugins - Available plugins에서 SSH Agent를 설치합니다.

Jenkins 관리 - Credentials에서 추가할 Domain Scope를 선택해 Add Credentials를 선택합니다.

SSH Username with private key를 선택하고, 다음 항목을 입력합니다.

  • ID : 파이프라인 스크립트`에서 사용할 식별자
  • Username : ec2 Username(ubuntu)

ec2 .pem key의 값을 입력합니다.

-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----

이렇게 등록된 ec2 .pem key는 암호화되어 저장됩니다.

Github Token 생성

Private Repository로 서브모듈을 사용할 것이기 때문에, 젠킨스에서 접근할 수 있도록 깃허브 토큰을 생성해야 합니다.

Settings - Developer settings - Personal access tokens - Tokens (classic) - Generate new token으로 새로운 Token을 생성합니다.

다음 Scope를 활성화합니다.

  • Private Repository에 접근하기 위한 repo 관련 Scope
  • Github Webhook을 감지하기 위한 repo_hook 관련 Scope

지금 생성된 토큰을 통해 젠킨스에 깃허브 계정을 등록합니다.

Github Account Credentials 등록

Jenkins 관리 - Credentials에서 추가할 Domain Scope를 선택해 Add Credentials를 선택합니다.

Username with password를 선택하고, 다음과 같은 항목을 입력합니다.

  • Username : Github username
  • Password : 토큰
  • ID : 파이프라인 스크립트에서 사용할 식별자

환경 변수 등록

파이프라인 스크립트를 Git에 등록해 형상 관리를 할 것이기 때문에, EC2 IP와 같이 중요한 내용을 노출하지 않도록 젠킨스의 변수로 등록하는 것을 권장합니다.

Jenkins 관리 - System - Global properties에서 Environment variables에서 key - value의 형식으로 등록할 수 있습니다.

이러한 환경 변수는 파이프라인 스크립트에서 ${env.KEY}로 접근할 수 있습니다.

Pipeline 생성

새로운 Item을 선택합니다.

해당 Item의 이름을 입력하고, Pipeline을 선택합니다.

지금은 복잡한 조건이 없기 때문에, GitHub hook trigget for GITScm polling을 선택합니다.

이제 파이프라인 스크립트를 작성해야 하는데, 하단의 Pipeline Syntax를 통해 비교적 수월하게 작성할 수 있습니다.


예시로 Github Repository에서 Main Repository / SubmoduleClone하는 Pipeline Script를 작성해보도록 하겠습니다.

checkout을 선택합니다.

Repositories에 빌드할 깃허브 저장소를 입력합니다.

Branches to build에 빌드 시 기준이 될 브랜치를 입력합니다.

Generate Pipeline Script를 선택하면 입력한 내용을 토대로 Pipeline Script를 작성해줍니다.


이를 토대로 작성한 파이프라인은 다음과 같습니다.

pipeline {
    agent any

    stages {
        stage('github clone') {
            steps {
                checkout scmGit(
                    branches: [[name: '*/step2']],
                    extensions: [submodule(parentCredentials: true,reference: '', trackingSubmodules: true)],
                    userRemoteConfigs: [[credentialsId: 'github-account', url: 'https://github.com/apptie/jwp-shopping-order']]
                )
            }
        }

        stage('build'){
            steps{
                sh'''
                    ./gradlew clean bootJar
                '''
            }
        }

        stage('publish') {
            steps {
               sshagent(credentials: ['zeeto-aws']) {
                   sh "scp build/libs/jwp-shopping-order.jar ubuntu@${env.EC2_IP}:${env.EC2_DIR}"
                   sh "scp script/deploy-script.sh ubuntu@${env.EC2_IP}:${env.EC2_DIR}"
                   sh "ssh ubuntu@${env.EC2_IP} 'sh ${env.EC2_DIR}/deploy-script.sh' "
               }
           }
        }
    }

    post {
        always {
            cleanWs(cleanWhenNotBuilt: true,
                    deleteDirs: true,
                    disableDeferredWipeout: true,
                    notFailBuild: true)
        }
    }
}
  • github clone : checkout을 통해 Main Repository / Submodulejenkins_homeclone합니다.
  • build : clone 받은 Main Repository에 빌드를 수행합니다.
  • publish
    • scp & ssh를 통해 EC2에 직접 접속해 배포를 수행합니다.
    • credentials : 등록한 EC2 .pem Key ID를 입력합니다.
  • post
    • Pipeline 동작 이후 수행할 작업을 명시합니다.
    • cleanWs를 통해 Work SpaceClone받은 Main Repository를 삭제합니다.

이렇게 작성한 파이프라인 스크립트를 젠킨스에서에서 관리하는 것이 아닌, 깃허브 저장소에서 관리할 것이기 때문에 다음과 같이 설정합니다.

Pipeline script from SCM을 선택합니다.

Repository URLCredentials를 입력합니다.
현재 Main Repositorypublic이므로, Credentials를 선택할 필요는 없습니다.

기준 브랜치를 입력합니다.

.jenkinsfile의 경로를 입력합니다.


제 경우 Main Repository/script라는 디렉토리를 추가했습니다.


이걸로 Pipeline Item 설정을 끝냈습니다.

Github Webhook 설정

Main RepositoryPush를 감지하고 자동으로 배포를 하기 위해, Github Webhook 설정이 필요합니다.

Main Repository - settings - Webhooks - Add webhook를 선택합니다.

다음과 같이 입력합니다.

  • Payload URL : http://jenkins-url/github-webhook
  • Content type : application/json
  • With events - : Just the push event

이 경우, Main RepositoryPush를 하면 해당 Push EventGithub Webhook이 감지하고 관련 payload를 지정한 Payload URL(Jenkins)로 전송합니다.

정상적으로 설정되었다면, 다음과 같이 ping 요청이 성공적으로 수행됩니다.

known_host 설정

도커 젠킨스에서 EC2ssh 접속을 해야 하기 때문에, EC2 ~/.ssh/known_host에 도커 젠킨스 ip를 등록해야 합니다.

다음 명령어를 통해 젠킨스 컨테이너의 ip를 확인합니다.

sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' (젠킨스 컨테이너 ID)

이렇게 얻은 IP를 다음 명령어에 입력하면 됩니다.

ssh-keyscan -H (젠킨스 컨테이너 ip) >> ~/.ssh/known_hosts

만약 위 명령어로도 ssh 접근이 되지 않는다면, 다음과 같이 파이프라인 스크립트를 변경해도 됩니다.

stage('publish') {
    steps {
        sshagent(credentials: ['zeeto-aws']) {
            sh "ssh -o StrictHostKeyChecking=no ubuntu@${env.EC2_IP}
            sh "scp build/libs/jwp-shopping-order.jar ubuntu@${env.EC2_IP}:${env.EC2_DIR}"
            sh "scp script/deploy-script.sh ubuntu@${env.EC2_IP}:${env.EC2_DIR}"
            sh "ssh ubuntu@${env.EC2_IP} 'sh ${env.EC2_DIR}/deploy-script.sh' "
        }
    }
}

-o StrictHostKeyChecking=no 옵션을 통해 키 검증 로직을 생략할 수 있습니다.

ssh 접속만 이루어지면 known_hosts에 키가 등록되기 때문에, 이후 해당 명령어를 삭제해도 정상적으로 진행할 수 있습니다.

접속에 성공했다면 보안에 취약해질 수 있으므로 해당 명령어는 삭제하는 것을 권장합니다.

동작 테스트

Main Repositorypush를 하거나, 다음과 같이 지금 빌드를 통해 Pipeline의 동작을 유발할 수 있습니다.

그럼 다음과 같이 Stage View에서 현재 빌드 상태를 확인할 수 있습니다.

profile
안녕하세요

0개의 댓글