다음과 같은 세팅을 목표로 합니다.
Git Submodule
을 활용한 배포 서버 전용 외부 설정 파일(application-prod.yml
) 분리 및 형상관리도커 + 젠킨스
를 통한 배포 자동화현재 사용하고 있는 EC2
의 제약 조건은 다음과 같습니다.
t4g.micro
ssh
접속 가능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
을 import
EC2
에 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
관련 Scope
Github Webhook
을 감지하기 위한 repo_hook
관련 Scope
지금 생성된 토큰을 통해 젠킨스에 깃허브 계정을 등록합니다.
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}
로 접근할 수 있습니다.
새로운 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
에 빌드를 수행합니다.publish
scp & ssh
를 통해 EC2
에 직접 접속해 배포를 수행합니다.credentials
: 등록한 EC2 .pem Key ID
를 입력합니다.post
Pipeline
동작 이후 수행할 작업을 명시합니다.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-webhook
Content type
: application/json
With 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
에서 현재 빌드 상태를 확인할 수 있습니다.