본문 바로가기
👾개발지식/DevOps

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

by 서버요정 호토론 2022. 9. 2.

진행 상황

백엔드와 프론트 서버가 배포되어 있는 상태입니다. 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폴더 안에 있습니다)

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

댓글