[배포를 진행해보자] Jenkins를 통한 CI/CD 구축
Xshell을 통해 EC2 접속
이제 EC2에 접속하여 실제 배포 환경을 만들어보자.
나는 윈도우 환경에서 EC2에 접속하기위해 Xshell을 활용하였다.
Xshell에서 접속하기 위해 다음과 같이 설정을 진행한다.
호스트에는 자신의 퍼블릭 IP을 입력해주고 해당 세션에 대한 이름을 지정해준다.(이름은 임의로 지정해주면 된다.)
그리고 우리는 EC2를 생성할 때 SSH로 접근하기 위한 키 페어를 생성했다.
이를 적용시켜주기 위해 사용자 인증 탭에서 Public Key를 체크해주고 설정 버튼을 누른다.
그리고 가져오기를 통해 해당 키 페어를 가져오고 비밀번호를 입력한다.
비밀번호를 지정하지 않았을 경우 빈 상태로 진행하고 다시 해당 페이지로 돌아와 아래 사진에서 보이는 등록 정보를 클릭해 비밀번호를 설정할 수 있다.
이렇게 생성하고 실제로 왼쪽에 보이는 세션을 클릭해 접속하면 원활하게 접속이 되는 모습을 확인할 수 있다.
swapfile 설정
위처럼 진행한다면 원활하게 접속을 진행할 수 있다.
하지만 그 전에 swapfile을 설정해보자.
왜 swapfile을 설정하는가?
- 메모리가 1GB에 불과하다. (프리 티어라는 한계)
간단하게 말하면 메모리가 부족할 경우, EC2가 먹통이 되는 현상이 발생한다.
필자의 경우 단순 빌드를 진행하는 경우에도 버벅이는 현상이 발생했던 적도 있다.
그에 따라 이 현상을 방지하기 위해 메모리를 늘리는 작업을 진행해야하는가? 라고 했다면 비용적인 측면에서 부담을 느낄 수도 있다.
그래서 우리는 스토리지에서 2GB만큼을 떼와 이를 메모리처럼 활용하는 방법을 고려할 수 있는데 이것이 바로 swapfile 설정을 통해 이루어낼 수 있다.
물론 이 방법은 만능이 아니기에 메모리에 비해 속도가 현저히 느리다.
하지만, 메모리가 부족해 EC2가 먹통이 될 일을 막아준다.
이제 실제로 이를 설정해보자.
아래 명령어들을 순차적으로 실행하면 된다.
// dd 명령어를 통해 swap 메모리를 할당
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
// 스왑 파일에 대한 RW권한 업데이트
sudo chmod 600 /swapfile
// linux 스왑 영역을 설정
sudo mkswap /swapfile
// 스왑 파일 추가하여 스왑 파일을 즉시 사용가능하도록
sudo swapon /swapfile
// 절차 성공 확인
sudo swapon -s
// 부팅 시 스왑 파일 활성화
sudo vi /etc/fstab
/swapfile swap swap defaults 0 0
그리고 free 명령어를 통해 실제로 확인해보자.
그러면 다음과 같이 swap 공간이 생긴 것을 확인할 수 있다.
git, docker, jenkins 설치
이제 실제 배포를 위해 환경을 구성해보자.
우리는 CI/CD 환경을 구축해 PR을 머지하거나 푸쉬할 때마다 빌드 및 테스트가 이루어지고 배포가 되게끔하려고 한다.
즉, 자동화를 시키려고 한다.
그러기 위해서 Git과 Docker, Jenkins가 필요하니 이를 설치해보자.
- Git은 실제로 우리가 Git을 자주 다루기에 설치한다.
- Docker는 우리가 다양한 이미지를 도커 컨테이너에 띄워 배포할 것이기에 설치한다.
- Jenkins는 CI/CD 환경 구축을 위해 진행한다.
- Jenkins는 도커를 통해 설치한다.
// Git
sudo yum install -y git
// Docker
// 도커 설치
sudo yum install -y docker
// 도커 실행
sudo service docker start
// 도커 권한 부여(유저)
sudo usermod -a -G docker ec2-user
/var/run/docker.sock 파일의 권한을 666으로 변경하여 그룹 내 다른 사용자도 접근 가능하게 변경
sudo chmod 666 /var/run/docker.sock
// 도커 auto-start
sudo chkconfig docker on
// Docker Compose
// 도커 컴포즈 다운로드
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
// 도커 컴포즈 권한 부여
sudo chmod +x /usr/local/bin/docker-compose
// Jenkins 설치
docker pull jenkins/jenkins:jdk11
jenkins 실행
이제 jenkins를 실행하여 CI/CD 환경을 구축하여 실제 배포를 해보자.
우리는 이전에 jenkins를 도커 이미지로 끌어왔다.
그래서 우리가 jenkins를 사용하기 위해서는 도커를 통해 컨테이너에 띄워줘야한다.
docker run -d --restart always -p 9090:8080 \
-v /jenkins:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/local/bin/docker-compose:/usr/bin/docker-compose \
-v /home/ec2-user/web.env:/var/jenkins_home/web.env \
--name jenkins -u root jenkins/jenkins:jdk11
docker exec -it jenkins /bin/bash
apt-get update
curl -fsSL https://get.docker.com/ | sh
curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
위의 도커 run 명령어로 젠킨스를 도커 컨테이너로 띄울 수 있다.
천천히 하나씩 살펴보자.
- -d : 백그라운드 실행
- --restart : 재부팅 시, 자동으로 실행
- -p : EC2에 9090 포트로 접근하는 부분을 8080으로 바인딩
- -v : 볼륨 마운팅으로 :을 기준으로 오른쪽 도커 컨테이너의 부분을 왼쪽 부분과 연결하여 파일을 공유한다.
- jenkins를 매핑
- docker.sock를 매핑 (Docker in Docker를 위해)
- docker-compose를 사용하기 위해 매핑
- 현재 Spring Boot 프로젝트가 암호화를 위해 yml을 Jasypt로 암호화한 상태인데 암호화를 할 때 사용한 Key에 대한 정보를 저장한 부분을 web.env 저장해두었는데 이를 매핑
- --name : 컨테이너의 이름
- -u : 유저 이름 root
위에서 Docker in Docker라고 말했는데 이는 무엇일까?
바로 도커 안에서 도커를 사용한다는 뜻이다.
우리는 현재 젠킨스를 도커 컨테이너에 띄워 사용하고 있다.
그런데 우리는 젠킨스 안에서도 도커를 사용해야 한다.
우리는 젠킨스에서 자동화를 진행할 때 슬랙에 알림도 보내고 빌드도 하고 다양한 작업을 진행한다.
바로 여기서 도커가 사용된다. (빌드)
우리는 Dockerfile과 Docker-Compose를 사용해 빌드를 진행하기에 도커 컨테이너로 띄운 젠킨스 안에서 도커를 사용해야 한다.
그래서 젠킨스를 실행중인 도커 컨테이너에서 명령어를 실행하여 그 안에 도커를 설치해야한다.
따라서 그 아래에 docker exec 명령어를 통해 컨테이너에 접근하고 docker와 docker-compose를 설치한다.
실제로 도커 컨테이너에 띄워 실행을 한 뒤 해당 url로 접근해보자.
다음과 같이 잘 접근이 된 모습을 확인할 수 있다.
이제 Default 비밀번호를 입력해야하는데 이 부분에 대한 부분은 다음 명령어로 확인 가능하다.
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
해당 부분을 통해 얻은 비밀번호를 입력하면 다음과 같은 화면을 얻을 수 있다.
여기서 권장 설치를 진행하자.
완료되면 다음과 같은 화면을 얻을 수 있다.
이제 실제로 Jenkins 환경을 구성해보자.
Jenkins 관리 탭에서 시스템 설정을 누른다.
먼저 GitHub Server을 등록해야한다.
그러기 위해 Add GitHub Server 셀렉트박스를 클릭해 실제로 만들어보자.
GitHub Server를 등록하기 위해서는 Name, API URL, Credentials가 필요하다.
Name의 경우 임의로 지정하면 되고 API URL은 github로부터 받아오는 것이기에 그대로 둔다.
Credentials의 경우 Private Repository라면 접근 허가가 필요하기에 적용해야하는데 이를 만들어보자.
Kind는 Secret text로 맞추어주고 Secret에 GitHub의 AccessToken을 담고 ID에는 단순 임의 이름을 지정해 추가할 수 있다.
우리가 이를 생성하기 위해 Access Token을 만들어야 하는데 이는 GitHub에서 생성할 수 있다.
이를 생성하기 위해서는 GitHub Setting으로 들어가 Develop setting 탭에 들어간다.
그리고 Personal access tokens => Generate new Token을 통해 토큰을 생성한다.
토큰을 만드는데 있어 repo에 대한 권한을 허용해주고 추후에 PR 머지 또는 푸쉬에 대한 훅을 보내야하기에 admin hook에 대한 권한을 허용해주면 된다.
토큰을 만들었다면 실제로 등록된 토큰을 토대로 테스트를 진행하면 다음과 같이 성공하는 모습을 확인할 수 있을 것이다.
설정이 완료되었다면 Credentials를 만든 김에 다른 부분에 대한 것들도 만들어보자.
추후 Repository Clone을 위해 계정에 대한 Credentials가 필요하므로 다음과 같이 추가로 만들어준다.
그리고 아래로 내려가면 Slack 알림을 위한 설정이 존재할 것이고 이를 등록해보자.
Workspace에는 Slack을 만들 때 얻은 팀 서브 도메인을 넣어준다.
그리고 팀 서브 도메인과 함께 얻은 Slack의 통합 토큰 자격 증명 ID을 이용해 GitHub AccessToken을 만들었을 때와 같이 만들어 이를 Credential로 적용해준다.
마지막으로 알림을 받기 위해 생성한 채널의 이름을 적어주고 테스트를 진행하면 된다. (성공한다면 Success가 뜰 것이다.)
이와 같이 설정을 진행하면 Jenkins에 대한 기본적인 설정을 마친 것이라고 할 수 있다.
하지만 본격적인 Jenkins Pipeline을 구성하지 않았다.
이를 실제로 구성해보자.
메인 메뉴에서 새로운 Item을 선택한다.
Freestyle로 진행하면 좀 더 간편하게 진행할 수 있지만 좀 더 커스텀하게 진행할 수 없다.
추후 어떤 프로젝트를 어떤 환경의 CI/CD 환경 속에서 진행할지 모르기 때문에 pipeline을 사용하여 구현해보고자 한다.
우리는 이전에 언급했듯이 PR을 머지하거나 푸쉬했을 때를 기반으로 자동화시킨다고 했다.
그러기 위해서 GitHub에서 이러한 동작이 발생시 Webhook을 보내주는데 이에 대한 설정을 진행해야한다.
Webhook에 관한 옵션이 보이지 않는다면 Plugin을 설치하면 보일 것이다.
Webhook에 대해 Json으로 Payload가 담겨 오게되는데 merged에 대한 옵션에 대한 부분을 적용할 것이고
브랜치 옵션에 대한 부분을 적용할 수 있다.
우리는 Token을 설정할 수 있다.
우리는 다양한 pipeline을 생성할 수 있고 다양한 pipeline에 hook을 보낼 수 있다.
그렇다면 다양한 것들 사이에서 어떻게 구분을 할 수 있을까?
그게 바로 Token이다. (이 Token을 url 쿼리 파라미터로 지정하여 보내기에 이를 구분할 수 있다.)
위에서 설정한 json 값에 대해 어떠한 경우 진행할 것인지에 대한 설정 부분이다.
merged가 true인 경우, branch가 main인 경우이다.
GiHub에서도 WebHook에 대한 부분을 적용해줘야한다.
Repository Settings의 Webhooks의 Add webhook을 누른다.
아래와 같이 Jenkins의 url 및 포트번호와 맨 뒤에 위에서 설정한 Token 정보를 넣어주면 된다.
우리는 Push와 PR에 대해서 정보를 받을 것이므로 이에 대한 부분을 체크해놓는다.
이제 실제로 pipeline 동작에 대한 부분을 정의해보자.
pipeline의 동작은 아래와 같다.
pipeline은 다양한 stage를 통해 적용된다. (파이프라인 문법을 통해 작성되지만 pipeline syntax를 버튼을 통해 쉽게 작성할 수 있다.)
pipeline {
agent any
stages {
stage('Start') {
agent any
steps {
slackSend (channel: '#채널명', color: '#FFFF00', message: "STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
stage('Git Clone') {
steps {
git branch: 'main', credentialsId: 'repo-and-hook-access-token-credentials', url: 'https://github.com/SangHyunGil/DemoProject'
}
}
stage('Build') {
steps {
sh '''
chmod +x gradlew
./gradlew build
'''
}
}
stage('Docker-Compose') {
steps {
sh '''
echo "> PID 확인"
CONTAINER_NAME=$(docker ps -a | grep java | grep koner-pipeline | awk '{print $2}')
echo "CONTAINER_NAME"
if [ -z ${CONTAINER_NAME} ] ;then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
CURRENT_PID=$(docker ps -aqf "name=^$CONTAINER_NAME")
echo "> 도커 종료 ($CURRENT_PID)"
docker stop $CURRENT_PID
docker rm $CURRENT_PID
sleep 10
fi
echo "도커 시작"
docker-compose up --build -d
'''
}
}
}
post {
success {
slackSend (channel: '#채널명', color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
failure {
slackSend (channel: '#채널명', color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
}
- Stage : Start
- 시작하면 Slack 알림 메시지를 보낸다.
- 채널명을 기입해야한다.
- Stage : Git Clone
- Git Clone을 진행한다. (Private Repository인 경우 Credentials를 이용한다.)
- Stage : Build
- 프로젝트를 Build한다.
- Stage : Docker-Compose
- 이미 도커 컨테이너에 실행되어 있는 지를 확인하기 위해 ps -a을 이용하여 컨테이너 명을 찾는다.
- 만약 존재한다면, ps -aqf을 이용하여 컨테이너 명을 통해 컨테이너 ID를 찾는다.
- stop, remove를 이용하여 컨테이너를 종료한다.
- 그리고 새롭게 이미지를 생성하여 컨테이너에 띄운다.
- 최종 결과가 성공했다면 성공 메시지를, 실패했다면 실패 메시지를 전송한다.
- 채널명을 기입해야한다.
이제 실제로 테스트해보기 위해 Build Now로 실행해보면 잘 동작하는 것을 확인할 수 있다.
그리고 실제로 PR을 하거나 Push를 했을 시에도 정상적으로 동작하는 모습을 확인할 수 있었다.
그리고 실제 Docker에서 띄워진 컨테이너를 확인해보면 정상적으로 동작 중이라는 것을 볼 수 있으며, 해당 포트로 잘 동작이 되는 모습을 확인할 수 있다.
길고 긴 Jenkins CI/CD 구축 및 배포 과정이 완료되었다.
실제로 접속해보자.
잘 동작이 되는 모습을 확인할 수 있었다. (위에 대한 Reponse는 Security 적용으로 인증이 안된 사용자가 접근한 모습이다.)