다음과 같은 세팅을 목표로 합니다.
Git Submodule을 활용한 배포 서버 전용 외부 설정 파일(application-prod.yml) 분리 및 형상관리도커 + 젠킨스를 통한 배포 자동화현재 사용하고 있는 EC2의 제약 조건은 다음과 같습니다.
t4g.microssh 접속 가능8080, 8081 포트만 제약 없이 접속 가능그러므로 8080은 스프링 부트 애플리케이션 포트로, 8081은 젠킨스의 포트로 사용합니다.
Git 서브모듈은 Git Repository 내에서 다른 Git Repository를 포함하는 방법입니다.
다양한 용도로 사용되지만, 해당 글에서는 민감한 정보가 담긴 외부 설정 파일인 application.yml 분리 및 형상 관리의 목적으로 사용합니다.

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에서 다음과 같은 명령어로 서브모듈을 등록할 수 있습니다.
git submodule add -b {branch 명} {Repository URL} {Directory}
여기서 서브모듈을 세팅할 디렉토리에서 고려할 부분이 있습니다.
다음과 같은 전략을 고려할 수 있습니다.
application.yml에 서브모듈에서 관리하는 application-prod.yml을 importEC2에 jar 파일 뿐만 아니라 서브모듈에서 관리하는 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을 작성했습니다.
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로 동작하도록 지정했습니다.
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
Swap에 free의 값을 통해 정상적으로 적용되었는지 확인할 수 있습니다.
젠킨스를 사용하기 위해 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
다음 명령어를 통해 젠킨스 도커 이미지를 다운받고 도커 컨테이너를 실행해주었습니다.
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 : 내부 포트 8080을 8081 포트와 바인딩-v /jenkins:/var/jenkins_home : /var/jenkins_home을 /jenkins Directory로 볼륨 바인딩-e TZ=Asia/Seoul : 시간대 설정--user root : root 권한으로 실행-name jenkins : 도커 컨테이너 이름 지정http://ec2-ip:8081으로 접속합니다.

첫 접속 시, 비밀번호를 요구합니다.
다음을 통해 확인할 수 있습니다.
# 마운트한 디렉토리에 접근해 비밀번호 출력
cat /jenkins/secrets/initialAdminPassword
비밀번호를 입력해줍니다.

Install suggested plugins를 선택합니다.

설치가 진행됩니다.

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

젠킨스의 URL을 입력합니다.
변경할 필요 없이 기본으로 설정되어 있습니다.
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는 암호화되어 저장됩니다.
Private Repository로 서브모듈을 사용할 것이기 때문에, 젠킨스에서 접근할 수 있도록 깃허브 토큰을 생성해야 합니다.
Settings - Developer settings - Personal access tokens - Tokens (classic) - Generate new token으로 새로운 Token을 생성합니다.



다음 Scope를 활성화합니다.
Private Repository에 접근하기 위한 repo 관련 ScopeGithub Webhook을 감지하기 위한 repo_hook 관련 Scope지금 생성된 토큰을 통해 젠킨스에 깃허브 계정을 등록합니다.
Jenkins 관리 - Credentials에서 추가할 Domain Scope를 선택해 Add Credentials를 선택합니다.

Username with password를 선택하고, 다음과 같은 항목을 입력합니다.
Username : Github usernamePassword : 토큰ID : 파이프라인 스크립트에서 사용할 식별자파이프라인 스크립트를 Git에 등록해 형상 관리를 할 것이기 때문에, EC2 IP와 같이 중요한 내용을 노출하지 않도록 젠킨스의 변수로 등록하는 것을 권장합니다.
Jenkins 관리 - System - Global properties에서 Environment variables에서 key - value의 형식으로 등록할 수 있습니다.

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

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

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

이제 파이프라인 스크립트를 작성해야 하는데, 하단의 Pipeline Syntax를 통해 비교적 수월하게 작성할 수 있습니다.
예시로 Github Repository에서 Main Repository / Submodule을 Clone하는 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 / Submodule을 jenkins_home에 clone합니다.build : clone 받은 Main Repository에 빌드를 수행합니다.publishscp & ssh를 통해 EC2에 직접 접속해 배포를 수행합니다.credentials : 등록한 EC2 .pem Key ID를 입력합니다.postPipeline 동작 이후 수행할 작업을 명시합니다.cleanWs를 통해 Work Space에 Clone받은 Main Repository를 삭제합니다.이렇게 작성한 파이프라인 스크립트를 젠킨스에서에서 관리하는 것이 아닌, 깃허브 저장소에서 관리할 것이기 때문에 다음과 같이 설정합니다.

Pipeline script from SCM을 선택합니다.

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

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

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

제 경우 Main Repository에 /script라는 디렉토리를 추가했습니다.
이걸로 Pipeline Item 설정을 끝냈습니다.
Main Repository에 Push를 감지하고 자동으로 배포를 하기 위해, Github Webhook 설정이 필요합니다.
Main Repository - settings - Webhooks - Add webhook를 선택합니다.

다음과 같이 입력합니다.
Payload URL : http://jenkins-url/github-webhookContent type : application/jsonWith events - : Just the push event이 경우, Main Repository에 Push를 하면 해당 Push Event를 Github Webhook이 감지하고 관련 payload를 지정한 Payload URL(Jenkins)로 전송합니다.

정상적으로 설정되었다면, 다음과 같이 ping 요청이 성공적으로 수행됩니다.
도커 젠킨스에서 EC2로 ssh 접속을 해야 하기 때문에, 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 Repository에 push를 하거나, 다음과 같이 지금 빌드를 통해 Pipeline의 동작을 유발할 수 있습니다.

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