Open RIANAEH opened 2 years ago
morak-ci-back 어떤 브랜치든 PR 요청 시 backend 디렉토리에서 테스트를 진행하고 PR에 이에 대한 체크를 달아준다. morak-ci-front 어떤 브랜치든 PR 요청 시 frontend 디렉토리에서 테스트를 진행하고 PR에 이에 대한 체크를 달아준다. morak-was-dev dev 브랜치에 Merge 진행 시 Jenkins 서버에서 build를 진행해 jar 파일을 생성한다. 그리고 이 jar 파일을 was-dev 서버로 보내 실행시킨다. morak-was-main main 브랜치에 Merge 진행 시 Jenkins 서버에서 build를 진행해 jar 파일을 생성한다. 그리고 이 jar 파일을 was-main 서버로 보내 실행시킨다. morak-front main 브랜치에 Merge 진행 시 Jenkins 서버에서 build를 진행해 dist 디렉토리를 생성한다. 그리고 이 dist 디렉토리를 ws 서버로 보내 실행시킨다.
Generic Webhook Trigger Plugin을 설치한 후 Jenkins를 재시작해줍니다.
왜 CI 요청이 두번이나 가는걸까??
모락 팀은 CI/CD 도구로 Jenkins를 사용하고 있습니다. dev 브랜치에 PR 시 CI가 일어나며, main에 Merge 시 CI/CD가 일어나도록 설정했었습니다. 하지만 이 과정에는 살짝의 문제가 있었습니다. 현재 모락 서비스는 하나의 레포지토리에서 관리되고 있습니다. 프론트엔드와 백엔드의 소스 코드가 한 곳에서 관리되고있기 때문에 CI/CD 시 프론트엔드의 코드가 변경되도 백엔드 코드까지 CI/CD가 일어고 있었습니다. 저희는 이 문제를 해결하기 위해 Jenkins에서 PR과 Merge 시 라벨을 인식해 프론트엔드와 백엔드를 필터링하도록 설정하였습니다. 다른 분들에게도 도움이 되었으면 하는 마음에 해당 과정을 공유합니다. (잘못 된 부분이 있다면 알려주세요🙇♀️)
이번에 소개해드릴 방법은 두가지로, 하나는 GitHub Pull Request Builder에 라벨을 적용해 이용해 CI만을 하는 방법이고, 다른 하나는 Generic Webhook Trigger에 라벨을 적용해 CI/CD를 모두 하는 방법입니다.모락 팀에서는 두 방법 모두가 필요했었지만, 필요한 방법만 선택하시면 될 것 같습니다.
GitHub Pull Request Builder Plugin 문서를 살펴보면 라벨에 따라 트리거를 설정할 수 있는 기능을 제공한다는 것을 알 수 있습니다.
이 방법은 구글에 "Jenkin PR 라벨 적용" 이런 식으로 검색하면 많이 나오는 방법입니다. 처음에는 CI 과정에 Generic Webhook Trigger를 적용했었습니다. 하지만 webhook 요청이 일어났을 때 Jenkins에서 CI를 여러 번 하는 문제가 발생했고, PR의 코드를 CI하는 것이 아니라
sudo apt-get update
sudo apt-get install nginx -y
sudo service nginx start
WS public ip의 80번 포트로 접속하면 다음과 같이 nginx가 환영해준다!
dev.mo-rak.com
으로 연결했습니다.
Host key verification failed.
Jenkins에서 ws 서버로 ssh 접속을 사전에 해주어야합니다. 왜?
apt-get update
apt-get install openjdk-11-jdk
java -version
요구사항을 보고 1차적으로 "정적 분석이란 뭘까?"라는 의문이 들었습니다.
정적 분석 도구에는 PMD, SonarQube, checkstyle, cppcheck 등이 있습니다.
Sonar Blog: SonarCloud vs. SonarQube
현재 모락의 Github 저장소에 PR이 오면 Jenkins에서는 이에 대한 빌드(테스트)를 진행하고, 해당 PR의 코멘트로 결과를 남겨줍니다. 이때 빌드가 실패하면 Merge를 할 수 없도록 하고 있습니다.
현재 구조에서 모락의 Github 저장소에 PR이 오면,
설치할 때 소나큐브 공식 사이트에서 최신 버전과 LTS를 확인 후 원하는 버전을 선택해 다운받습니다.
8.9.9.56886
입니다.
sudo wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-8.9.9.56886.zip
unzip
을 다운 받고, 설치한 소나큐브의 압축을 풀어줍니다. 설치 완료:)sudo apt -y install unzip
sudo unzip sonarqube-8.9.9.56886.zip
conf/sonar.properties
에서 다음과 같이 사용할 포트를 설정할 수 있습니다. 따로 설정을 하지 않으면 소나큐브는 9000 포트를 사용합니다.
# TCP port for incoming HTTP connections. Default value is 9000.
sonar.web.port=8080
~/sonarqube-8.9.9.56886/bin/linux-x86-64$ ./sonar.sh start
Starting SonarQube...
./sonar.sh: 1: eval: /home/ubuntu/sonarqube-8.9.9.56886/bin/linux-x86-64/./wrapper: Exec format error
Failed to start SonarQube.
1.8 버전 이상의 자바를 설치해야합니다.
sudo apt-get install openjdk-11-jdk
java -jar lib/sonar-application-8.9.9.56886.jar
sudo chown -R ubuntu:ubuntu sonarqube-8.9.9.56886/
'메모리가 부족한 것인가?'하는 의심이 들었습니다.
실행한 SonarQube에는 http://[EC2 인스턴스 IP 주소]:[port 번호]
로 접속할 수 있습니다.
첫 로그인 시 ID와 Password는 admin으로 이후 자유롭게 변경하면 됩니다.
SonarQube 접속 완료!!
SonarQube에서는 외부 데이터베이스를 사용하기를 권장합니다.
Jenkins의 Manage Jenkins > Manage Plugins에서 SonarQube Scanner for Jenkins를 설치합니다.
Manage Jenkins > Configure System > SonarQube Server에서 설정을 진행합니다.
Manage Jenkins > Global Tool Configuration > SonarQube Scanner에서 설정을 진행합니다.
sonar.host.url=http://sonarqube.mo-rak.com:8080
sonar.login=a605c313e1754fe7dede2cea6f97a7db00085ed3
sonar.projectName=morak-backend
sonar.projectKey=morak-backend
sonar.sources=backend/src
sonar.lanquage=java
sonar.projectVersion=1.1.0-SNAPSHOT
sonar.sourceEncoding=UTF-8
sonar.java.binaries=classes
sonar.test.inclusions=**/*Test.java
sonar.exclusions=**/resources/**, **/test/**
모락팀은 모든 PR 시 Jenkins에서 빌드를 진행해 테스트의 성공/실패를 Github Check Status로 표시하고 Merge를 제한하도록 하고 있었습니다.
하지만 이 과정에는 살짝의 문제가 있었습니다. 현재 모락 서비스는 하나의 레포지토리에서 관리되고 있습니다. 프론트엔드와 백엔드의 소스 코드가 한 곳에서 관리되고있기 때문에 프론트엔드만의 코드를 변경 후 PR을 보내도 백엔드 코드까지 빌드하는 리소스 낭비가 일어나고 있었습니다.
모락팀에서는 다음과 같이 PR에 라벨
을 붙여 구분하고 있기 때문에, 저희는 Jenkins에서 이 라벨을 인식해 프론트엔드에서의 PR과 백엔드에서의 PR을 구분하도록 설정을 추가하였습니다.
저희는 기존에 GitHub Pull Request Builder Plugin
을 추가해 PR 시 CI 과정만 일어나도록 하는 아이템을 사용하고 있었습니다. 따라서 다음의 과정은 GitHub Pull Request Builder Plugin을 사용하고 있었다는 상황을 전제로 작성된 내용임을 알려드립니다.
GitHub Pull Request Builder Plugin 문서를 살펴보면 라벨에 따라 트리거를 설정할 수 있는 기능을 제공한다는 것을 알 수 있습니다.
Jenkins 아이템의 구성에 들어가 조금 더 세부적으로 살펴볼까요?
GitHub Pull Request Builder의 고급
버튼을 클릭하면 여러가지 추가 설정을 할 수 있습니다. 그 중 특정 라벨이 붙어 있으면 트리거되지 않도록 설정하는 부분
과 특정 라벨이 붙어 있을 때만 트리거되도록 설정하는 부분
이 있는 데, 저희는 두번째 설정에 다음과 같이 💻 backend
라벨을 넣어줬습니다.
여러개의 라벨을 넣고 싶을 경우 개행으로 구분해 입력하면 된다고 합니다.
이 다음에는 탈도 많고 에러도 많았던 Generic Webhook Trigger
에 라벨을 적용하는 과정을 소개해드리려고 합니다.
모락팀에서는 500 에러가 나는 상황에 대해 에러에 대한 스택 트레이스 뿐만 아니라 요청 정보도 로깅한다면 에러 상황을 재현해봄으로써 디버깅을 수월하게 할 수 있을 것이라고 생각했다. 하지만 우리는 요청 정보에 대한 출력은 커녕 HttpMessageNotReadableException을 만나게됐다. 먼저 해당 예외는 HttpServletRequest의 Body 정보를 2번 이상 읽으려고 시도할 때 발생한다.
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CacheBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
CachedBodyHttpServletRequest cachedBodyHttpServletRequest =
new CachedBodyHttpServletRequest(httpServletRequest);
filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);
}
}
ContentCashingRequestWrapper의 docs를 읽어보면, 이 클래스는 input stream이나 reader를 읽을 때만 캐싱을 진행하고 캐싱 된 데이터는 getContentAsByteArray() 메서드를 통해 byte array로 제공한다고 합니다. 즉, 여전히 getInputStream() 메서드와 getReader() 메서드는 단 한번만 사용가능하며, 둘 중 하나의 메서드가 호출 된 이후에야 getContentAsByteArray() 메서드로 캐싱된 데이터를 불러와 사용할 수 있습니다.
다음과 같이 로깅 데이터에 body가 잘 담겨있는 모습을 확인할 수 있습니다.
park algorithm 따라잡기!!
sudo apt update
sudo apt upgrade
sudo apt-get install letsencrypt -y
sudo add-apt-repository ppa:certbot/certbot
sudo apt install python3-certbot-nginx
sudo certbot --nginx -d dev.mo-rak.com
sudo certbot renew --dry-run
task createDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs") // index.html 경로를 /docs로 설정합니다.
}
/etc/nginx/sites-available/default
에서 location을 설정할 때 정규표현식을 사용할 수 있습니다.
location ~ ^/(api|docs) {
...
}
dev.mo-rak.com/docs/index.html
로 접근 시 API 문서가 잘 호스팅됩니다. 오예!!
server {
listen 8081 default_server;
listen [::]:8081 default_server;
server_name dev.mo-rak.com;
location ~ ^/(api|docs) {
proxy_pass http://192.168.1.123:8081;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
}
Nginx를 타고 서버까지 갔지만 해당 포트에 서버가 열려있지 않은 경우 다음 에러가 발생합니다.
top
, watch -n 5 free -m
of에는 스왑 파일의 위치와 이름을 지정할 수 있습니다. 저는 / 디렉토리에 swapfile이라는 이름으로 생성하였습니다. bs는 블록의 크기, count는 블록의 개수를 의미하며, 스왑 파일의 총 크기는 bs * count가 됩니다. 저는 총 2.1GB의 스왑 파일을 생성했습니다.
$ sudo dd if=/dev/zero of=/swapfile bs=128M count=16
16+0 records in
16+0 records out
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 14.3367 s, 150 MB/s
$ sudo chmod 600 /swapfile
/swapfile
)을 스왑파일로 설정합니다.$ sudo mkswap /swapfile
Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
no label, UUID=...
이제 물리적 메모리 1G + 가상 메모리 2G를 가지게 되었습니다😎
$ sudo swapon /swapfile
$ sudo swapon -s
Filename Type Size Used Priority
/swapfile file 2097148 0 -2
/etc/fstab
파일 마지막 줄에 다음 내용을 추가해, 인스턴스가 재부팅되더라도 스왑 파일을 사용하도록 설정해줍니다./swapfile swap swap defaults 0 0
🕐 TimeZone 설정하기
AWS EC2의 기본 TimeZone은 UTC
현재 모락 서비스는 AWS EC2 환경에 배포되어 있습니다. 모락의 백엔드 서버에서는 프로그램 실행 과정에서의 중요 내용들을 로깅하여 로그 파일에 저장하고 있는데, 이때 시간 정보 또한 출력하고 있습니다. 이 과정에서 로그 파일에 출력된 시간이 현재 한국 시간과 같지 않은 문제가 발생했습니다. 이는 EC2의 TimeZone이 기본적으로 UTC로 설정되어 있어 발생한 문제였습니다.
당시 현재 시각은 2022년 8월 8일 오후 12시 34분이었는데, 로그 파일에는 오전 3시 34분으로 찍혀있었습니다.
UTC vs. KST
잠시 UTC와 KST에 대한 개념을 잡고가자면, UTC(Universal Time Coordinated)는 세계 협정시를 의미하고, KST(Korean Standard Time)은 한국 표준 시간을 의미합니다. 이때 KST의 경우
UTC + 09:00
시를 나타내기 때문에 로그 파일에 현재보다 9시간 전인 3시 34분으로 찍혀있었던 것이었습니다.AWS EC2의 TimeZone 설정하기
그렇다면 이제 TimeZone을 우리가 원하는 KST로 설정해봐야겠죠?
Java의 LocalDateTime.now()
현재 모락팀에서 구현한 로직에서는 Java의
LocalDateTime.now()
를 호출하는 부분이 있습니다.Jenkins에는 왜 시간 설정이 적용되지 않았을까?
알고보니 Jenkins의 경우 본인의 시간 설정이 따로 있었습니다. (귀찮게..)
젠킨스 재시작하기
사이트트에서 확인해보면 timezone이 잘 변경되어 있습니다:)
"지금 빌드"로 직접 빌드를 해보면 빌드 시간 또한 잘 적용되어 있는 것을 확인할 수 있습니다.
끝!!
참고 자료