👾개발지식/DevOps

젠킨스와 도커를 이용한 CICD 배포방법

서버요정 호토론 2022. 9. 2. 10:14

진행 상황

백엔드와 프론트 서버가 배포되어 있는 상태입니다. CICD가 완료되었고, 결과는 mm을 통해 전송됩니다.

현재 rest docs 서버 설정 부분 작업 중입니다.

시스템 구성

  • jenkins 2.3
    • blueocean : 지속적 배포와 관리 할 수 있는 UI를 지원합니다.
  • docker
  • docker-compose - jenkins
  • DB 서버 : aws s3
  • 파일 서버 : aws rds

jenkinsfile은 Declarative Pipeline 방식으로 구현했습니다.

jenkins blueocean과 front + nginx, backend 도커 서버로 구축되어 있습니다.

주의사항

빌드할 때 시간이 걸립니다(5분정도??) 충돌을 막기 위해 머지할 때 최소 5~10분 정도의 간격을 두고 해야합니다.

mm을 통해 빌드 상황을 알려주고 있으며 jenkins-log 채널의 최근 메세지가 started 상태라면 기달려야합니다.

충돌 시, 우분투에서 해당 도커 컨테이너 삭제(jenkins 말고 backend, frontend 같은거)하고 이미지 삭제해준 후 다시 트리거 실행시키거나 푸시하면 해결됩니다.

Docker 명령어

  • 도커 컨테이너 보는 법
    • sudo docker ps -a
  • 도커 컨테이너 삭제
    • sudo docker rm <container_id>
  • 도커 이미지 보는 법
    • sudo docker images
  • 도커 이미지 삭제
    • sudo docker rmi <image_id>
  • 강제 옵션 -f (삭제가 안될 때,)
    • ex) docker rmi -f <image_id>
  • 도커 로그 확인
    • docker logs <image_id> : 도커가 꺼져있어도(컨테이너가) 최종적으로 실행된 기록을 가지고 있습니다(오류 잡을 때, 매우 유용)
  • 그 외, 빌드할 때, run에서 애러가 나면! -> jenkinsfile의 run 부분의 stop과 remove 부분 코드를 서버에서 직접 하나씩 복붙해서 제거합니다. 만약 제거에 오류가 있을 경우, 강제옵션을 사용해서 수동으로 제거해줍니다. 그리고 다시 빌드합니다.

배포과정

기본 설치

sudo apt-get update
sudo apt-get install nodejs
sudo apt-get install npm
# 설치 확인
nodejs -v
npm -v

docker 설치

# 필수 패키지 설치
sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
# GPG Key 인증
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# docker repository 등록
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# 도커 설치
sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io
# 시스템 부팅시 도커 시작
sudo systemctl enable docker && service docker start
# 도커 확인
sudo service docker status

예전에 rds를 쓸지 결정이 안된 상태에서 작성한 거라 다음 구분 선까지 넘어가셔도 괜찮습니다.

Mysql 도커 설치

# mysql 이미지 불러오기
sudo docker pull mysql
# 도커 이미지 확인
sudo docker images
# 도커 이름은 --name 뒤에 넣고, password는 root 패스워드(사용자 지정)
sudo docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=finalproject --name thankstore_mysql mysql
# 도커 컨테이너 bash 접속
sudo docker exec -it thankstore_mysql bash
# mysql 접속
mysql -u root -p
# 패스워드 입력
finalproject

도커 컨테이너 mysql 접속 후

# DB 생성
create database 뭐라할까요;
# 사용자 생성
CREATE USER 'thankstore'@'%' IDENTIFIED BY 'finalproject';
# 사용자 권한 부여
GRANT ALL PRIVILEGES ON *.* TO 'thankstore'@'%';
# 권한 새로고침(안해도 됨)
flush privileges;

Https 키 발급

sudo apt-get install letsencrypt
# 만약 nginx를 사용중이면 중지!
sudo systemctl stop nginx
# 인증서 발급
sudo letsencrypt certonly --standalone -d www제외한 도메인 이름
# 이메일 쓰고 Agree
# 뉴스레터 no
# 이제 인증서가 발급된다. 이 인증서를 잘보관하자
# 2가지 키가 발급되는데 이 두가지를 써야한다. 밑의 경로에 각각 하나씩 있다.
# 마지막 부분에서 씀!!!! 
 ssl_certificate /etc/letsencrypt/live/thankstore.click/fullchain.pem; 
 ssl_certificate_key /etc/letsencrypt/live/thankstore.click/privkey.pem; 

개인적으로 만든 aws의 도메인 이름은 letsencrypt 에서 발급 안됩니다!

​ ex) ec2-3-36-71-194.ap-northeast-2.compute.amazonaws.com

ssafy 프로젝트는 가능합니다

​ ex) j4f002.p.ssafy.io 이런거

젠킨스

  • docker-compose.yml 파일 작성 - 젠킨스 도커 생성
version: '3.7'  # docker -v 버전을 입력

services:  # 실행하려는 컨테이너들을 정의
 jenkins:  # 서비스의 이름
  image: 'jenkinsci/blueocean'  # 사용할 도커 이미지
  restart: unless-stopped  # 명시적으로 중지되거나, Docker 자체가 중지되는 경우 재시작
  user: root
  privileged: true  # permission denied 관련 설정
  ports:  # 사용할 포트 번호 설정
   - '9090:8080'
  volumes:  # 로컬 디렉토리의 특정 경로를 컨테이너 내부로 마운트할 수 있음
   - '/home/ubuntu/docker/jenkins-data:/var/jenkins_home'
   - '/var/run/docker.sock:/var/run/docker.sock'
   - '$HOME:/home'
  container_name: 'jenkins'
  • 젠킨스 블루오션 실행
# 해당 디렉토리에서 도커 컴포즈 업!
sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 권한 줘야합니다.
$ sudo chmod +x /usr/local/bin/docker-compose

sudo docker-compose up -d
# 실행시킨 도커 컨테이너에 접속
sudo docker exec -it jenkins /bin/bash
# 비밀번호 겟해야함. 젠킨스의 초기 비밀번호로 인터넷상에서 들어갈 수 있음.
cat /var/jenkins_home/secrets/initialAdminPassword
# 이 비밀번호로 http://도메인:9090 들어갈 수 있음.

젠킨스 환경 설정 및 GITLAB 웹훅 설정

  1. 들어가서 인증 후, 웹으로 들어가서 권장 설치 선택
  2. 아이디 비번 만들고, url에서 그냥 next! -> 젠킨스로 들어와짐.
  3. 왼쪽 매뉴 바에서 jenkins 관리->플러그인관리 -> 설치가능 탭에서 -> Gitlab Plugin / gitlab hook Plugin 설치
  4. jenkins 관리 → 시스템 설정에서 gitlab 관련 설정 추가

여기서 gitlab에 대한 토큰을 만들어야합니다. name에는 gitlab connection 이름을 지정하고, gitlab host url을 써주면 됩니다.(https://lab.ssafy.com)

credentials는 gitlab api token에서 입력합니다.

gitlab 토큰 얻는 방법

  • gitlab에서 seettings 클릭

 

  • Access Tokens 탭에 들어와서 이름 지정하고(날짜는 따로 지정안해도 됩니다.) 원하는 scopes 지정 후 토큰 발행.
  • 그럼 상단에 토큰 번호 하나가 나옵니다. 이 토큰 값은 한 번밖에 안 보여주기 때문에 다른 곳에 저장해 놓으세요.

  • 다시 원래 창으로 들어와서 토큰을 넣고 나머지 정보를 입력합니다.

  • add 후, test connection를 눌러서 success가 뜨면 성공입니다!
  • 메인화면에서 새로운 아이템(new item) 선택 -> 파이프라인 선택 후, 생성
  • 생성된 item에 접속 후, 구성 클릭 -> 스크롤을 내려서 Pipeline을 찾으세요.

SCM -> git 선택. Repository URL은 Gitlab Repository URL 입력 .

  • Credentials는 ADD 한 후, name 에는 id gitlab, password 는 패스워드 적고 생성해줍니다.

  • 위로 스크롤 좀 올려서 이제 development에 머지가 될 때, 서버로 ci/cd가 될 수 있도록 웹훅 설정합니다.

  • Build Triggers 고급... 클릭 후, include에서 webhook 브랜치 선택

우측 아래 Generate 클릭 - 키 생성됩니다. -> 소중히 보관하세요.

  • 다시 깃랩의 설정으로 갑니다.

프로젝트 레포지토리에서 Settings → Integrations 선택

URL은 Build Triggers 설정 시 보였던 Gitlab Webhook URL 입력

Secret token은 Build Triggers 설정 시 생성했 던 Secret Token 입력

add webhook 후, 푸시 이벤츠해보고 success 확인.

그럼 젠킨스 환경 설정과 연결은 끝나게 됩니다!

도커 네트워크 설정

도커 네트워크란? : 같은 Docker Host내에서 실행중인 Container간 연결할 수 있도록 돕는 논리적 네트워크같은 개념입니다. 서로 간 통신을 가능하게 합니다.

sudo docker network create thxstorecicdnetwork;

위에서 말한 것처럼. 각 서버로 구성되어야할 폴더에 dockerfile과 전체적인 ci/cd과정을 관리해줄 수 있는 jenkinsfile이 루트 디렉토리에 들어가서 정상 작동만하면 끝입니다. dockerfile과 jenkinsfile 내용입니다.

Jenkinsfile

각 stage를 설정해서 pull 받아오는 과정(git pull), 빌드 과정(Docker build), 배포 과정(Docker run) 단계별로 나눠서 파이프라인을 작성했습니다.

여기는 mm webhook 부분이 빠져있습니다.

# 도커 파이프라인
pipeline {
	agent none
	options { skipDefaultCheckout(false) }
	stages {
		stage('git pull') { # pull 받아오는 상태
			agent any
			steps {
				checkout scm
			}
		}
		stage('Docker build') { # docker build 상태
			agent any
			steps {
				sh 'docker build -t frontend:latest /var/jenkins_home/workspace/jenkins-cicd/frontend' 
				# frontend -t 는 생성할 이미지 이름. 
				sh 'docker build -t backend:latest /var/jenkins_home/workspace/jenkins-cicd/backend' 
				# backend 도커가 있는 위치. 빌드는 도커 이미지 파일을 만들어 주는 것입니다!! 아직 실행 X 

			}
		}
		stage('Docker run') {# docker 배포 상태
			agent any
			steps {
				# 도커 시작 전, 기존에 실행중인 도커를 멈추고 제거하는 작업.
				sh 'docker ps -f name=frontend -q \
        | xargs --no-run-if-empty docker container stop'
				sh 'docker ps -f name=backend -q \
		| xargs --no-run-if-empty docker container stop'

				# 컨테이너 제거
				sh 'docker container ls -a -f name=frontend -q \
        | xargs -r docker container rm'
				sh 'docker container ls -a -f name=backend -q \
		| xargs -r docker container rm'

				# 도커 이미지 제거-> 도커 이미지 중 none tag의 id를 구해서 강제로 삭제하는 명령어 
				sh 'docker images -f dangling=true && \ 		# -f 강제, dangling=true, Docker 에서 none tag 삭제
				docker rmi $(docker images -f dangling=true -q)' 	# 이미지 해시 제거, 이미지 제거. 사용되지 않은 모든 이미지 제거?  -q 옵션 이미지 ID

				# -v 호스트 경로:컨테이너경로 연결
				# 도커 실행 
				# frontend 이름으로,  jenkinsnetwork에서 frontend:latest를, 포트는 포트(nginx)
				sh 'docker run -d --name frontend \
				-p 80:80 \
				-p 443:443 \
				-v /home/ubuntu/sslkey/:/var/jenkins_home/workspace/jenkins-cicd/sslkey/ \
				-v /etc/localtime:/etc/localtime:ro \
				--network jenkinsnetwork \
				frontend:latest'

				sh 'docker run -d --name backend \
		--network jenkinsnetwork backend:latest'
			}
		}
	}
}

jenkins와 mattermost webhook연동

통합에 들어가서 전체 incommig webhook을 선택합니다.

incoming webkook 추가를 클릭 후, 제목과 설명, 채널을 지정합니다.

생성이 되며, url 부분을 복사합니다.

젠킨스에 접속 후, 젠킨스 관리 -> 플러그인관리에서 mattermost notification plugin을 설치합니다.

다시 메인에서 젠킨스 관리 -> 시스템 설정으로 들어 간 후, Global Mattermost Notifier Settings라는 항목을 찾습니다.

이전에 mm에서 설정한 url과 사용하고 있는 build item을 적고 ,Test connection을 눌렀을 때, 설정한 채널에 메시지가 간다면 성공입니다. 이제 젠킨스파일 파이프라인에 추가하면 됩니다.


Jenkinsfile + mm webhook message (최종본)

pipeline {
	agent none
	options { skipDefaultCheckout(false) }
	stages {
		stage('git pull') {
			agent any
			steps {
				mattermostSend (
                            			color: "#2A42EE", 
                            			message: "Build STARTED: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Link to build>)"
                        		)  
				checkout scm
			}
		}
		stage('Docker build') {
			agent any
			steps {
				script {
                    				try {
						sh 'docker build -t frontend:latest /var/jenkins_home/workspace/thxstore-jenkins-cicd/frontend'
						sh 'docker build -t backend:latest /var/jenkins_home/workspace/thxstore-jenkins-cicd/backend'
					}catch(e) {
                        				mattermostSend (
                                					color: "danger", 
                                					message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Link to build>)"
                            				)
                    				} 
				}
			}
		}
		stage('Docker run') {
			agent any
			steps {
				script {
                    				try {
						sh 'docker ps -f name=frontend -q | xargs --no-run-if-empty docker container stop'
						sh 'docker ps -f name=backend -q | xargs --no-run-if-empty docker container stop'
				
						sh 'docker container ls -a -f name=frontend -q | xargs -r docker container rm'
						sh 'docker container ls -a -f name=backend -q | xargs -r docker container rm'
				
						sh 'docker images -f dangling=true && docker rmi $(docker images -f dangling=true -q)' 

						sh 'docker run -d --name frontend \
						-p 80:80 \
						-p 443:443 \
						-v /home/ubuntu/sslkey/:/var/jenkins_home/workspace/thxstore-jenkins-cicd/sslkey/ \
						-v /etc/localtime:/etc/localtime:ro \
						--network thxstorecicdnetwork \
						frontend:latest'

						sh 'docker run -d --name backend \
						--network thxstorecicdnetwork backend:latest'
					}catch(e) {
						currentBuild.result = "FAILURE"
                    				} finally {
						if(currentBuild.result == "FAILURE"){
							mattermostSend (
                                						color: "danger", 
                                						message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Link to build>)"
                            					)
						}
						else{
							mattermostSend (
                                						color: "good", 
                                						message: "Build SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Link to build>)"
                            					)
						}
					}
				}
			}
		}
	}
}

메시지 관련 코드 (mattermostSend) 는 짧고 간단하기 때문에 따로 설명하지 않겠습니다. 여기서는 started, failed, success 3단계로 나눠서 메세지를 발생했습니다.

NGINX 파일 - homepage.conf(최종 - 사용시, 주석은 제거해주세요)

# homepage.conf
# 포트 80 요청을 모두 잡아서 443으로 리다이렉션
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name k4b202.p.ssafy.io;

        return 301 https://$server_name$request_uri;
        #마지막으로 https요청 된 URI 의 버전으로 301 리디렉션을 반환합니다 . 이 server블록 에 도달하는 모든 요청 http은 포트 80 요청 만 수신하기 때문에입니다. ssl로 리다이렉션

}

server {
        listen 443 ssl;
        listen [::]:443 ssl;

        root /home/ubuntu/s04p31b202/frontend/dist; # index 위치
        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html; # index

        server_name  k4b202.p.ssafy.io;
        client_max_body_size 50M;			# 이미지나 gif 용량 등.

		# SSL 등록
        ssl_certificate /var/jenkins_home/workspace/thxstore-jenkins-cicd/sslkey/fullchain.pem;
        ssl_certificate_key /var/jenkins_home/workspace/thxstore-jenkins-cicd/sslkey/privkey.pem;

		# root 등록
        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                alias /usr/share/nginx/html/homepage/;
                try_files $uri $uri/ /index.html;
        }
        # 백엔드 돌려줄 것 서버가 api 서버로 
        location /api {
                proxy_pass http://backend:8080;
                proxy_http_version 1.1;
                proxy_set_header Connection "";  # 도커 8080으로 만들 것. backend는 jenkins 파일에서 도커 이름으로 부여. 이쪽으로 돌리겠다

				# proxy_set_header Connection : nginx가 proxy로 중계할 때, 중계받은 서버버에 request header를 재정의해서 전달 용도. 밑에와 정의.,
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header X-Forwarded-Host $host;
                proxy_set_header X-Forwarded-Port $server_port;
        }
}

nginx share로 설정., https 설정. front docker 서버에 같이 들어갑니다.

Frontend dockerfile(최종 - 사용시, 주석은 제거해주세요)

FROM node:lts-alpine as build-stage
WORKDIR /homepage
# WORKDIR은 RUN, CMD, ENTRYPOINT의 명령이 실행될 디렉터리를 설정합니다. < 컨테이너 위치
# WORKDIR 뒤에 오는 모든 RUN, CMD, ENTRYPOINT에 적용되며, 중간에 다른 디렉터리를 설정하여 실행 디렉터리를 바꿀 수 있습니다.

# 복사할 파일 경로 : 이미지에서 파일이 위치할 경로
COPY . .
RUN npm install
RUN npm run build

# 가장 기본적인 커맨드이다. 어떤 이미지를 기반으로 새로운 이미지를 생성할 것인지를 나타낸다
FROM nginx:stable-alpine as production-stage

# RUN 커맨드는 정말 간단하게 생각해서, bash 쉘에서 입력하는것과 동일하다고 생각하면 된다.
RUN rm /etc/nginx/conf.d/default.conf
COPY ./homepage.conf /etc/nginx/conf.d/homepage.conf
COPY --from=build-stage ./homepage/dist /usr/share/nginx/html/homepage
# COPY --from=builder를 통해 전 단계 스테이지 빌드에서 생성된 특정 결과물만 새로운 BASE 이미지로 복사해서 이미지를 생성했다.
# '이 도커 이미지는 3000번 포트를 외부에 공개할 예정이다'라고 명시할
#EXPOSE 구문으로 명시한 포트는 'docker run -P' 명령을 이용할 때 호스트 운영체제로 오픈
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]
#4), (5) CMD / ENTRYPOINT
# 컨테이너 시작 시, 실행될 명령어를 정하는 커맨드이기에 build로 이미지가 만들어지고, 그 이미지로 컨테이너를 run할 때 효력을 갖는다. 
# 컨테이너 시작 시 실행될 명령어이기에 한번만 사용 가능한걸로 알고 있다. 사실 두 개의 명령어는 비슷한 면이 있지만, run 시에 조금 달라진다. 
# 자세한 건.. 저도 잘 모릅니다.

# 여기서는 from을 두 개 사용했는데 from 두개 사용하는 것을 멀치 스테이지라고 부릅니다.

Backend dockerfile(최종 - 설명은 frontdocker파일과 비슷해서 적지 않았습니다.)

FROM openjdk:11 AS builder
WORKDIR /backend
COPY . .
RUN chmod +x ./gradlew

RUN rm -rf module-api/src/test
RUN rm -rf module-web/src/test
RUN ./gradlew :module-web:clean build
RUN ls module-web/build/libs

FROM adoptopenjdk:11-jdk
COPY --from=builder /backend/module-web/build/libs/*.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "./app.jar"]

주의사항

env.local이나 백엔드 프론트 환경파일은 관리자가 직접 서버에 배포해야합니다.(레포지토리 주소 : home 디렉터리 docker폴더 안에 있습니다)

충돌이 일어날 경우 서버에 접속해서 직접 도커를 중단 제거 삭제를 해주셔야합니다.(위쪽에 명령어 정리해놨습니다.)