회사 프로젝트에 CI/CD를 적용할 일이 생겼다.
이전에 Github + Jenkins + AWS CodeDeploy를 사용해 CI/CD를 적용한 적이 있었는데, 회사 사정 상 다른 방식으로 CI/CD를 적용해야 하는 상황이다.
따라서, Gitlab + Jenkins + Nginx + Docker + AWS EC2를 사용한 CI/CD 적용 방법을 기록한다.
CI/CD 구조는 다음과 같다.
1. Gitlab 저장소에 Push 한다.
2. webhook을 통해 Jenkins에서 Gitlab 레포지토리에 접근한다.
+) Gitlab과 Jenkins는 회사 서버에서 도커 컨테이너로 설치했다. 나는 회사 프로젝트여서 회사 서버에 설치한 것이지, EC2 등 각각 상황에 맞는 환경에 젠킨스를 설치하면 된다.
3. Jenkins에서 프로젝트 빌드를 마친 후 jar 파일을 AWS EC2에 전송한다.
4. Nginx에서 프로젝트 포트 번호를 다르게 하면서 번갈아 무중단 배포한다.
(이전 프로젝트가 현재 9001 포트를 사용 중이라면 새로운 프로젝트를 9002 포트에 배포하고 9001 프로젝트 삭제)
CI/CD 구조를 알았으니, 이제 차근차근 단계를 밟아보자.
스프링 프로젝트 설정
무중단 배포를 원하는 프로젝트에 다음 설정을 추가한다.
프로파일 설정
두 개의 포트에 대해 번갈아 프로젝트를 배포하기 위해, 두 개의 프로파일을 세팅한다.
<application.yml>
spring:
profiles:
group:
development:
- common
- development_database
- port_8080
production-set1:
- common
- production_database
- port_9001
production-set2:
- common
- production_database
- port_9002
---
spring:
config:
activate:
on-profile: common
...
---
spring:
config:
activate:
on-profile: development_database
datasource:
url: [테스트 DB]
---
spring:
config:
activate:
on-profile: production_database
datasource:
url: [운영 DB]
---
spring:
config:
activate:
on-profile: port_8080
server:
port: 8080
---
spring:
config:
activate:
on-profile: port_9001
server:
port: 9001
---
spring:
config:
activate:
on-profile: port_9002
server:
port: 9002
나는 프로파일을 3가지 (development, production-set1, production-set2)로 나누고, 각각 공통속성(common), DB 종류(테스트or운영), 포트번호를 지정해주었다.
development는 개인 테스트용 프로파일이고, production-set1, production-set2는 운영 시 무중단 배포를 위한 2가지 프로파일이다.
프로파일 조회 API 추가
현재 프로파일 조회용 API를 만들었다.
GET: /utils/profile
@RestController
@RequiredArgsConstructor
@RequestMapping("/utils")
public class UtilController {
private final Environment env;
@GetMapping("/profile")
public String getProfile() {
return Arrays.stream(env.getActiveProfiles()).findFirst().orElse("");
}
}
라이브러리 추가
배포 과정에서 health check를 하기 위해 다음 라이브러리를 추가한다.
dependencies {
...
implementation "org.springframework.boot:spring-boot-starter-actuator"
...
}
actuator는 스프링의 상태를 확인할 수 있는 라이브러리로, GET: /actuator/health를 요청하면 상태를 확인할 수 있다.
Gitlab 초기 설정
스프링 프로젝트를 push할 깃랩 저장소를 설치한다.
나는 깃랩을 회사에서 제공하는 서버에 설치했다. (192.168.0.xxx)
EC2 등 각자 상황에 맞는 환경에 설치하면 되겠다.
깃랩을 설치하는 방식이 아닌 Gitlab.com 사이트를 이용해도 당연히 가능하다.
깃랩 설치
도커로 깃랩을 설치하는 방법은 이전에 다룬 적이 있으므로 해당 링크로 대체한다.
2022.07.01 - [docker] - [Docker] 도커 컨테이너에 깃랩(Gitlab) 설치
Access token 발행
User Settings - Access Tokens로 이동한다.
아래와 같이 access token을 생성한다. scopes는 그냥 전부 체크했다.
Create personal access token을 클릭한다.
그러면 아래와 같이 access token이 나오는데 나중에 다시 조회할 수 없으므로 미리 복사해서 메모장 등에 저장해놓자.
+) 참고로 나처럼 회사(로컬) 서버에서 작업을 진행하는 경우 젠킨스 연동이 안되는 경우가 있다. 이런 경우, 다음 설정을 추가해주면 된다.
Admin - Settings - Network - Outbound requests - Allow request to the local network 부분들 체크
Jenkins 초기 설정
gitlab에 push 이벤트가 발생하면 자동으로 레포지토리를 pull하는 프로세스를 구축한다.
나는 젠킨스를 회사에서 제공하는 서버에 설치했다. (192.168.0.xxx)
EC2 등 각자 상황에 맞는 환경에 설치하면 되겠다.
젠킨스 설치
docker run -itd --name jenkins -p 9000:8080 jenkins/jenkins:lts-jdk11
9000포트를 사용한다.
이제 웹 브라우저로 젠킨스에 접속하자.
192.168.0.xxx:9000
아래와 같은 화면이 나오면 접속에 성공한 것이다.
다음 명령어를 통해 초기 비밀번호를 확인하고 위 칸에 적어주고 Continue 버튼을 누른다.
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
Install suggested plugins를 누른다.
그리고 플러그인이 전부 설치될 때까지 기다리면 된다.
젠킨스 계정을 생성한다. 계정은 까먹지 않도록 주의하자! 젠킨스는 친절하게 아이디/비밀번호 찾기를 지원하지 않는다. 까먹으면 환경변수를 다 까봐야 한다.
GitLab 플러그인 설치
Jenkins 관리 - 플러그인 관리 - 설치 가능으로 이동한다.
검색창에 gitlab을 입력하면 GitLab 플러그인을 설치할 수 있다.
install without restart를 클릭한다. 설치가 완료되면 플러그인을 바로 사용할 수 있다.
Gitlab Credential 추가
Jenkins 관리 - Manage Credentials로 이동한다.
아래 global에서 Add credentials를 클릭한다.
Kind: GitLab API token
Scope: Global
API token: [앞서 발급한 gitlab access token]
ID: [원하는 ID값]
정보를 모두 입력했다면 OK를 클릭한다.
Jenkins 관리 - 시스템 설정으로 이동한다.
Gitlab 부분에 생성한 Credential을 추가한다.
Connection name: [원하는 connection name]
Gitlab host URL: [깃랩이 설치된 URL]
Credentials: [앞서 생성한 GitLab API token]
Test Connection을 클릭하면 연결이 성공했는지 확인할 수 있다.
Item 생성
새로운 Item을 클릭해서 젠킨스에 item을 생성한다.
아이템 이름은 my-cicd 처럼 하지말고 프로젝트 이름을 명확히 적어주는 것이 좋다.
Freestyle project를 선택하고 OK를 클릭한다.
item을 생성하면 설정 화면이 나온다(해당 화면을 나가도 item - 구성을 통해 진입할 수 있다).
소스 코드 관리에서 Git을 클릭한다.
Repository URL에 깃랩 프로젝트 HTTP URL을 입력한다.
Failed to connect to repository 에러가 나오는데 해당 URL이 아직 인증되지 않았다는 말이다.
저장소와 연동하기 위한 Credential을 따로 추가해줘야 한다. 아래 Credentials 부분에서 Add - Jenkins를 클릭하자.
Kind: Username with password
Scope: Global
Username: [깃랩 아이디]
Password: [깃랩 비밀번호]
이렇게 설정하고 Add를 클릭하면 Failed to connect to repository 에러가 사라진다. 이제 Gitlab에서 해당 레포지토리에 접근할 수 있다.
Webhook 설정
아직 깃랩 - 젠킨스 연동이 끝난게 아니다. 현재는 젠킨스에서 깃랩 레포지토리에 접근만 가능한 상황일 뿐이다.
이제 깃랩에 push를 하면 자동으로 젠킨스에서 해당 레포지토리를 가져오도록 웹훅을 설정해야 한다.
Jenkins 설정
다시 item - 구성에 들어간다.
빌드 유발 부분에서 Build when a change is pushed to GitLab. GitLab webhook URL: [URL] 을 체크한다.
URL은 나중에 깃랩 설정 시 필요하므로 기억해두자.
체크하고 아래 고급 버튼을 클릭한다.
아래로 쭉 내리면 Secret token이 보이는데 Generate를 클릭해서 토큰을 생성해주자.
이 역시 깃랩 설정시 필요하기 때문에, 따로 복사해서 저장해두자.
Gitlab 설정
레포지토리 - Settings - Webhooks로 이동해서 웹훅을 생성한다.
URL: [앞서 확인한 젠킨스 URL]
Secret token: [앞서 생성한 젠킨스 Secret token]
Trigger: Push events 체크 + master 브랜치
Add webhook을 클릭하면 아래 웹훅이 생성되는데 Test - Push events를 클릭하면 젠킨스 연동을 테스트할 수 있다.
앞으로 깃랩 - 젠킨스 테스트를 할 때는 git push 명령어 없이 Test - Push events를 통해 테스트하는 것이 매우 편리하다.
Push events 테스트 시, 젠킨스에 아래와 같이 빌드가 성공했다는 아이콘이 생기면 깃랩 - 젠킨스 연동이 성공한 것이다.
Gradle 설정
깃랩 - 젠킨스 연동을 통해 젠킨스에서 깃랩 레포지토리를 가져왔다면, 이제 해당 프로젝트를 빌드해보자.
나는 Gradle을 통해 빌드를 진행할 것이다.
Jenkins 관리 - Global Tool Configuration - Gradle 부분에서 Add Gradle을 클릭한다.
name: [원하는 name]
Version: [스프링 프로젝트 버전에 맞는 gradle 버전]
gradle 버전을 확인하는 방법은 스프링 프로젝트 - gradle - gradle-wrapper.properties 파일에서 확인할 수 있다.
이제 item - 구성에서 gradle 설정을 추가하자.
Build - Add build step - Invoke Gradle script 클릭
Gradle Version: [앞서 생성한 gradle]
Tasks: clean build
이렇게 하면 Gitlab push 이벤트 발생 시, 젠킨스에서 해당 레포지토리를 가져와 Tasks에 있는 gradle clean build 명령을 수행할 수 있다.
이제 다시 깃랩 웹훅에서 Test - Push events를 발생시켜 보자.
빌드가 성공하고, gradle 빌드까지 성공하는 것을 볼 수 있다. 빌드 콘솔에서 확인해보자. (빌드 - Console Output)
실제로 웹이 아닌 docker exec -it jenkins /bin/bash 명령을 통해 젠킨스에 접근하면, 다음과 같이 깃랩 레포지토리 프로젝트가 빌드되어 잘 저장된 것을 볼 수 있다.
SSH 설정
이제 깃랩 - 젠킨스 설정은 끝났으니, 배포를 위해 젠킨스 - EC2를 연결해보자.
원하는 것은 젠킨스에서 프로젝트 빌드가 끝나면 jar 파일을 EC2에 전송하는 것이다. EC2는 jar 파일을 받아서 프로젝트를 배포한다.
EC2 SSH 연결
먼저 젠킨스에서 jar 파일 전송을 위한 플러그인을 설치한다. SSH 플러그인을 검색해서 설치하자.
+) 검색해보니 Publish Over SSH 플러그인을 사용하는 글이 많은데, 보안, 로그 출력 안됨, 더 이상 지원하지 않는 문제 등 여러 이슈들이 있어서 나는 SSH 플러그인을 사용하기로 했다.
+) 아무리 기다려도 플러그인 설치가 안되는 경우가 있는데, 이미 설치됐는데 젠킨스가 가만히 있는 상황인 것 같아서 docker restart jenkins 하니까 해결됐다. 플러그인도 잘 설치되어있다.
Jenkins 관리 - Manage Credentials에서 SSH를 위한 새로운 Credential을 추가하자.
Kind: SSH Username with private key
Scope: Global
ID: [원하는 ID]
Username: [EC2 사용자 이름] -> 나는 Ubuntu 서버를 사용하기 때문에, ubuntu로 지정했다.
Praivate Key: Enter directrly 체크 -> Key에는 EC2 접속 .pem 파일을 복사 붙여넣기 한다.
+).pem 파일을 메모장으로 열면 쉽게 내용을 복사할 수 있다.
Jenkins 관리 - 시스템 설정 - SSH Remote hosts 부분에 다음 설정을 추가하자.
Hostname: [EC2 public ip]
Port: 22
Credentials: [앞서 생성한 credential]
Check connection을 클릭하면 연결 성공 여부를 확인할 수 있다.
+) 참고로 EC2 Ubuntu를 사용하는 경우 버전 호환 이슈인지 22.04는 젠킨스에서 연결을 할 수 없었다. 따라서 젠킨스를 통해 C
I/CD를 적용하고자 한다면 20.04 버전을 설치하는 것을 추천한다.
빌드 설정
이제 item - 구성 - Build로 이동하자.
Add build step - Execute shell을 추가하고, jar 파일을 전송하는 코드를 입력한다.
전송 코드는 다음과 같다.
scp -v -o StrictHostKeyChecking=no -i [젠킨스 pem 파일 경로] [젠킨스 jar 파일 경로] [ec2 사용자]@[ec2 public ip]:[ec2 경로]
예를 들어, 다음과 같이 코드를 작성할 수 있다.
my-cicd.pem 파일로 EC2 접속 인증을 하고, 젠킨스에 있는 jar 파일을 EC2의 app-server라는 디렉토리로 전송한다.
scp -v -o StrictHostKeyChecking=no -i /var/jenkins_home/my-cicd.pem /var/jenkins_home/workspace/my-cicd/build/libs/my-cicd-0.0.1-SNAPSHOT.jar ubuntu@xx.xx.xx.xx:/home/ubuntu/app-server
+) 이 방식은 젠킨스에 EC2 pem 파일이 존재해야 한다. scp 명령과 docker cp 명령을 이용하여 jenkins 컨테이너 내부 원하는 위치에 pem 파일을 전송하자. 이 때, chmod 755로 설정해야 한다.
다시 item - 구성 - Build - Add build step에서 Execute shell script on remote host using ssh를 추가하자.
SSH site: [앞서 설정한 EC2]
Command: /home/ubuntu/app-server/deploy.sh -> app-server 디렉토리 내에 deploy.sh 스크립트를 실행한다는 명령이다. deploy.sh는 아래에서 작성할테니 우선 이렇게 적어두고 넘어가자.
Nginx 설정
이제 젠킨스 세팅은 끝났고, EC2에서 배포를 위한 nginx를 설정하자. nginx는 apache와 같은 웹 서버 소프트웨어로, 여기서는 배포를 위한 프록시 서버로서 사용할 것이다.
Nginx 설치
EC2에 아래 명령을 차례대로 입력하자
sudo apt update
sudo apt upgrade
sudo apt install nginx
sudo service nginx start
sudo service nginx status
다음과 같이 active 표시가 생기면 설치 성공이다.
Nginx 설정
아래 명령어로 설정 파일을 연다.
sudo vim /etc/nginx/sites-enabled/default
그리고 아래 두줄을 다음과 같이 추가한다.
include /etc/nginx/conf.d/service-url.inc;
proxy_pass $service_url;
+) proxy_pass 위에 try files... 는 주석 처리하자.
+) 맨 위 두줄에서 listen 옆에 있는 포트를 변경하면서 기본 포트를 바꿀 수도 있다.
service-url.inc 파일을 추가하자.
sudo vim /etc/nginx/conf.d/service-url.inc
해당 파일에 다음 한 줄을 추가한다. 설정 추가 시, nginx가 자동으로 9001 포트에 포워딩을 해준다.
즉, 기본 포트(80)으로 접속시 자동으로 9001 포트에 연결되는 것이다.
이 코드가 향후 9001, 9002 번갈아 가면서 변경되는 부분이다.
set $service_url http://127.0.0.1:9001
nginx를 재시작한다.
sudo service nginx restart
배포 스크립트 작성
이제 배포를 위한 스크립트를 작성해보자.
배포 스크립트
나는 /home/ubuntu/app-server 안에 deploy.sh 를 생성하고 다음 내용을 추가했다.
#!/bin/bash
echo "> 현재 구동중인 profile 확인"
CURRENT_PROFILE=$(curl -s http://localhost/utils/profile)
echo "> $CURRENT_PROFILE"
if [ $CURRENT_PROFILE == production-set1 ]
then
IDLE_PROFILE=production-set2
IDLE_PORT=9002
elif [ $CURRENT_PROFILE == production-set2 ]
then
IDLE_PROFILE=production-set1
IDLE_PORT=9001
else
echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
echo "> set1을 할당합니다. IDLE_PROFILE: set1"
IDLE_PROFILE=production-set1
IDLE_PORT=9001
fi
IMAGE_NAME=app_server
TAG_ID=$(docker images | sort -r -k2 -h | grep "${IMAGE_NAME}" | awk 'BEGIN{tag = 1} NR==1{tag += $2} END{print tag}')
echo "> 도커 build 실행 : docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -t ${IMAGE_NAME}:${TAG_ID} ."
docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -t ${IMAGE_NAME}:${TAG_ID} /home/ubuntu/app-server
echo "> $IDLE_PROFILE 배포"
echo "> 도커 run 실행 : sudo docker run --name $IDLE_PROFILE -d --rm -p $IDLE_PORT:${IDLE_PORT} ${IMAGE_NAME}:${TAG_ID}"
docker run --name $IDLE_PROFILE -d --rm -p $IDLE_PORT:${IDLE_PORT} ${IMAGE_NAME}:${TAG_ID}
echo "> $IDLE_PROFILE 10초 후 Health check 시작"
echo "> curl -s http://localhost:$IDLE_PORT/actuator/health "
sleep 10
for retry_count in {1..10}
do
response=$(curl -s http://localhost:$IDLE_PORT/actuator/health)
up_count=$(echo $response | grep 'UP' | wc -l)
if [ $up_count -ge 1 ]
then
echo "> Health check 성공"
break
else
echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
echo "> Health check: ${response}"
fi
if [ $retry_count -eq 10 ]
then
echo "> Health check 실패. "
echo "> Nginx에 연결하지 않고 배포를 종료합니다."
exit 1
fi
echo "> Health check 연결 실패. 재시도..."
sleep 10
done
echo "> 스위칭을 시도합니다..."
sleep 5
/home/ubuntu/app-server/switch.sh
1. 현재 프로파일을 조회해서, 새로운 프로젝트가 배포될 IDLE_PROFILE을 저장한다.
2. TAG_ID를 생성하고 (아무 이미지도 없으면 1, 있으면 순차적으로 2, 3, ...), docker build, docker run을 통해 컨테이너를 생성한다.
3. 새로 생성된 프로젝트에 대해 health check를 수행한다.
4. health check에 성공하면 프로젝트 스위칭을 위해 (ex. 9001 -> 9002) switch.sh 스크립트를 실행한다.
switch.sh 내용은 다음과 같다. 동일하게 /home/ubuntu/app-server에 생성했다.
#!/bin/bash
echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s http://localhost/utils/profile)
if [ $CURRENT_PROFILE == production-set1 ]
then
CURRENT_PORT=9001
IDLE_PORT=9002
elif [ $CURRENT_PROFILE == production-set2 ]
then
CURRENT_PORT=9002
IDLE_PORT=9001
else
echo "> 일치하는 Profile이 없습니다. Profile:$CURRENT_PROFILE"
echo "> 9001을 할당합니다."
IDLE_PORT=9001
fi
echo "> 현재 구동중인 Port: $CURRENT_PORT"
echo "> 전환할 Port : $IDLE_PORT"
echo "> Port 전환"
echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
echo "> ${CURRENT_PROFILE} 컨테이너 삭제"
sudo docker stop $CURRENT_PROFILE
sudo docker rm $CURRENT_PROFILE
echo "> Nginx Reload"
sudo service nginx reload
1. 현재 프로파일을 조회해서, 새로운 프로젝트가 배포될 IDLE_PORT를 저장한다.
2. 앞서 생성한 service_url.inc 파일의 포트 부분을 IDLE_PORT로 변경한다.
3. nginx를 리로드한다.
Dockerfile
/home/ubuntu/app-server 안에 Dockerfile을 생성하고 다음 내용을 추가했다.
FROM openjdk:11
ARG IDLE_PROFILE
ARG JAR_FILE=*.jar
ENV ENV_IDLE_PROFILE=$IDLE_PROFILE
COPY ${JAR_FILE} app.jar
RUN echo $ENV_IDLE_PROFILE
ENTRYPOINT ["java", "-Dspring.profiles.active=${ENV_IDLE_PROFILE}", "-jar","/app.jar"]
openjdk:11 이미지를 기반으로 컨테이너를 생성하고, IDLE_PROFILE에 대해 스프링 프로젝트를 실행한다.
스크립트 실행해보기
이제 스크립트를 테스트해보자.
먼저 앞서 작성한 deploy.sh, switch.sh, Dockerfile은 모두 chmod 755로 설정해두자.
app-server의 디렉토리 구조는 다음과 같다. 아직 젠킨스를 테스트하기 전이니, scp 명령 등을 사용해 jar 파일을 미리 EC2에 넣어두자.
deploy.sh를 실행한다.
위처럼 새로운 프로젝트 배포 후 포트 변경이 잘 된다면 스크립트가 잘 작성된 것이다.
최종 테스트
이제 깃랩 - 젠킨스 - EC2까지 잘 연동이 되는지 테스트해보자.
원하는 프로세스는
1. 깃랩에 Push를 하면
2. 젠킨스에 빌드가 생성되고
3. jar 파일이 전송되며
4. deploy.sh 스크립트가 수행되는 것이다.
깃랩에 push 했을 때 젠킨스가 정상 작동한다면 다음과 같이 deploy.sh 로그를 볼 수 있다.
이제 배포할 때마다, 프로파일과 포트가 실제로 변경되는지 확인해가면서 계속 테스트해보면 된다.
지금까지 Gitlab + Jenkins + Nginx + Docker + AWS EC2를 사용한 CI/CD 방법을 알아보았다.
CI/CD는 다양한 방식이 있고 한 번에 되는 경우는 거의 없다. 리소스도 많은 편이 아니기 때문에, 여러 차례 삽질해보면서 스스로 터득하는 수 밖에 없다. 그렇기에 희소하고 가치있는 작업이라고 생각한다.
CI/CD에 계속 관심을 가지면서 나만의 차별성을 꾸준히 길러나가야겠다.
참고자료
https://velog.io/@psystar99/깃랩과-젠킨스-연동
http://dev.blog.sellmate.co.kr/post/continuous-deployment/
https://hyunminh.github.io/nonstop-deploy/#nginx-설정하기
'devops > cicd' 카테고리의 다른 글
Jenkins - Slack 연동 (0) | 2022.08.19 |
---|---|
[Jira] Gitlab 서버 - Jira Cloud 연동 (2) | 2022.07.01 |
이얌 AWS 클라우드 아키텍처 - Jenkins, CodeDeploy Blue Green 배포, Auto-Scaling, Load-Balancer (4) | 2022.01.09 |