Docker를 이용한 Jenkins CI/CD 구현

구본식·2023년 1월 17일
0
post-thumbnail

앞서 공부하고 CODEBOX 프로젝트에 적용해보았던 Jenkins PipelineDocker기술을 적용하여 확장해볼것이다.
Jenkins Pipeline 구축: https://velog.io/@rnqhstlr2297/Jenkins%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-Pipeline-%EA%B5%AC%EC%B6%95

1. 동작 과정

Jenkins server, Deploy(운영) server가 각각의 AWS EC2를 구성한다.

  1. 공유레포지토리(GitHub)로 Push를 한다.
  2. GitHub Webhooks를 이용하여 Jenkins server에 변화를 전달한다.
  3. Jenkins에서 아래 단계를 수행한다.
    1) Github에서 소스코드를 Clone한다.
    2) 테스트 및 빌드를 진행한다.
    3) Dockerfile를 사용하여 Docker 이미지를 만들고 DockerHub에 push한다.
    4) CD를 위해 운영 EC2서버로 deploy.sh실행 명령어를 한다.
    (deploy.sh:DockerHub에서 이미지를 받아오고 컨테이너로 실행하는 쉘)
  4. 운영용 EC2에서 docker로 Spring boot를 실행한다.

2. 프로젝트에 사용될 코드

Spring 프로젝트 코드로는 CODEBOX 프로젝트의 코드를 사용할 것이다.

2.1 Dokerfile

FROM openjdk:11-jre

COPY build/libs/*.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]

프로젝트 파일안에 Dockerfile을 생성한다.

  • FROM openjdk:11-jre : 해당 도커가 컨테이너를 실행할때 Base Container image로 openjdk11-jre를 사용하겠다는 의미이다.
    어플리케이션을 실행할때 jdk까지는 필요없기 때문에 jre를 선언
  • COPY build/libs/*.jar app.jar : build/libs 안에 jar파일을 app.jar라는 이미지 파일로 복사한다는 의미
  • ENTRYPOINT ["java", "-jar", "app.jar"] : 도커가 컨테이너를 실행할때 사용하는 명령어이다. -> "java -jar app.jar" 명령어가 되는것이다.

2.2 deploy.sh

# 가동중인 awsstudy 도커 중단 및 삭제
docker ps -a -q --filter "name=codebox" | grep -q . && docker stop codebox && docker rm codebox | true

# 기존 이미지 삭제
docker rmi bonsik/codebox-project-images:1.0

# 도커허브 이미지 pull
docker pull bonsik/codebox-project-images:1.0

# 도커 run
docker run -d -p 8888:8888 --name codebox bonsik/codebox-project-images:1.0

# 사용하지 않는 불필요한 이미지 삭제 -> 현재 컨테이너가 물고 있는 이미지는 삭제되지 않습니다.
docker rmi -f $(docker images -f "dangling=true" -q) || true

운영 서버에 명시해논 쉘 스크립트이다.
추후 jenkins에서 운영 서버로 ssh를 이용한 실행 명령어로 해당 셀 스크립트를 실행할것이다.


3. Deploy(운영) EC2 세팅

3.1 Docker 설치

운영 EC2 서버에서 Jenkins가 dockerHub에 push한 이미지를 가져와서 컨테이너로 띄울것이다.
그러므로 Docker가 설치되어있어야된다.

또한 운영 EC2 서버의 OS가 Ubuntu 이므로 Ubuntu에 Docker를 설치하는 방식을 사용하였다.

//먼저 기본 중에 기본. 패키징 툴(apt-get)을 업데이트, 업그레이드 시켜주자.
apt update & apt upgrade

//Docker 설치에 필요한 필수 패키지를 설치해주자.
apt-transport-https ca-certificates curl gnupg-agent software-properties-common

//Docker의 GPC Key 인증을 하자.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

// Docker Repository를 등록해보자. 이는 Docker 환경을 구축할 때 필수적인 절차이다.
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"

//apt-get 패키징 툴을 통해 도커를 설치하자.
sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io

//설치된 도커 버전 확인
docker -v

//Docker를 실행해보자.
sudo systemctl enable docker

//Docker 활성화
sudo service docker start

// /var/run/docker.sock 파일의 권한을 666으로 변경하여 그룹 내 다른 사용자도 접근 가능하게 변경
sudo chmod 666 /var/run/docker.sock

//ubuntu 유저를 docker 그룹에 추가 
//-> 추구 운영서버에서 "sudo"명령어 없이 "ubuntu"유저가 docker 명령어를 사용할수 있음
sudo usermod -a -G docker ubuntu

3.2 운영 EC2 인바운드 규칙 편집

추후 jenkins EC2 server에서 운영 EC2서버로 쉘 시작 명령어를 할수 있게 해야한다.

그러니 SSH을 이용한 접근이 가능하도록 jenkins ec2 server ip/port를 보안 규칙에 추가한다.


4. Jenkins EC2 세팅

4.1 프리티어 EC2 메모리 부족 해결

Jenkins를 EC2로 이용해 구축하는걸 찾다보니 EC2로 젠킨스 서버로 프리티어로 구축하게 되면 메모리 부족 문제가 발생한다고 한다.
젠킨스로 도커를 띄우고 실행하는것에는 문제없으나 깃허브 웹훅을 이용해 젠킨스로
Spring 어플리케이션을 clone하여 빌드과정에서 메모리 부족 문제로 EC2가 멈추는 현상이 발생한다고 한다.
그래서 하드디스크를 가상 메모리로 전환시키면 해결할수 있다고 하였다.

//스왑 파일 생성하기
sudo dd if=/dev/zero of=/swapfile bs=128M count=16

//스왑 파일에 대한 일기 쓰기 권한 업데이트하기
sudo chmod 600 /swapfile

//Linux 스왑 영역 설정하기
sudo mkswap /swapfile

//스왑 공간에 스왑 파일을 추가하여 스왑 파일을 즉시 사용할 수 있도록 하기
sudo swapon /swapfile

//확인하기
sudo swapon -s

// /etc/fstab 파일을 편집하여 부팅 시 스왑 파일을 활성화하기
sudo vi /etc/fstab
//파일 가장 마지막에 다음을 추가하고 :wq로 저장하고 종료
/swapfile swap swap defaults 0 0

//메모리 확인하기
free

4.2 Docker 설치

Jenkins를 Docker를 이용하여 띄울것이기 때문에 마찬가지로 Docker를 설치해야된다.

Jenkins EC2 서버는 OS가 Amazon Linux 이고 해당 OS에 Docker를 설치하는 방식을 사용하였다.

//패키지 업데이트
sudo yum -y upgrade

//도커 설치
sudo yum -y install docker

//도커 설치 작업이 잘 되었는지 버전 확인
docker -v

//도커 시작
sudo service docker start

//도커 그룹에 사용자 추가 -> docker가 그룹명, ec2-user가 사용자명
sudo usermod -aG docker ec2-user 

Jenkins EC2 서버에서 이미지를 push 하기전까지의 시나리오를 정리하자면
1. Docker를 이용하여 Jenkins를 실행
2. Jenkins안에서 프로젝트의 Dockerfile를 빌드하여 이미지 생성
3. 생성한 이미지를 DockerHub로 push
이러한 순서로 정리될수 있다.

이러한 순서로 하려면 도커위에서 도커를 실행시켜야된다. 이러한 방식에는 2가지 방식이 있는데 DidD(Docker in Docker, DooD(Docker Out of Docker이 있다.
Docker 측에서는 DooD방식을 권장하는거 같다.

결론적으로는 컨테이너를 띄우기전 -v 옵션으로 호스트의 docker socker을 사용하도록 하면 된다고 한다. 추후 이 2가지의 차이점도 공부해야겠다.🤣

4.3 Docker 이미지를 이용한 Jenkins 설치 및 도커내 도커 설치

Jenkins Docker 이미지를 이용한 Jenkins 설치와 도커내 도커 설치를 Dockerfile로 만들었다. 이부분은 거의 그대로 참고하였다.🤣어렵다ㅠㅠ

//Dockerfile 생성
sudo vim Dockerfile

//Dockerfile 작성 시작 - bash를 Jenkins 이미지로
FROM jenkins/jenkins:jdk11

//도커를 실행하기 위한 root 계정으로 전환
USER root

//도커 설치(도커내 도커)
COPY docker_install.sh /docker_install.sh
RUN chmod +x /docker_install.sh
RUN /docker_install.sh

//도커 그룹에 사용자 추가
RUN usermod -aG docker jenkins
USER jenkins

아래는 도커내 도커를 설치하는 파일이다.

//docker_install.sh 파일 생성
sudo vim docker_install.sh

//docker_install.sh 파일 작성 시작
#!/bin/sh
apt-get update && \
apt-get -y install apt-transport-https \
  ca-certificates \
  curl \
  gnupg2 \
  zip \
  unzip \
  software-properties-common && \
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey; apt-key add /tmp/dkey && \
add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
$(lsb_release -cs) \
stable" && \
apt-get update && \
apt-get -y install docker-ce

두파일을 같은 디렉토리 위치에 두고 Dockerfile를 빌드하기전 해야할일이 있다.
앞서 도커안에 도커는 host의 docker.sock를 빌려서 사용하는데
따라서 host의 docker.sock의 파일 권한을 변경하여 다른 사용자도 접근 가능하도록 해야된다.

//파일 권한 변경
sudo chmod 666 /var/run/docker.sock

//Dockerfile 빌드
docker build -t jenkins .

이제 마지막으로 앞서 생성한 Jenkins 이미지를 컨테이너로 띄우면 된다.

sudo docker run -d --name jenkins \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8080:8080 \
-e TZ=Asia/Seoul \
jenkins

-d: 백그라운드로 실행한다는 의미
--name: 띄운 컨테이너의 이름
-v: 이부분이 호스트의 docker socker을 사용하도록 하는 부분
-p: 포트 매핑 -> -p <호스트 시스템의 포트번호>:<컨테이너 안의 포트>
마지막(jenkins): 컨테이너로 띄울 도커 이미지 이름

4.4 운영 EC2 인바운드 규칙 편집

앞서 포스터인 Jenkins Pipeline 에서도 GitHub Webhooks를 사용했다.
그때는 로컬로 Jenkins 서버를 띄었기 때문에 로컬 서버 ip/port를 외부에 노출시키기 위해 ngrok프로그램을 사용했다.

하지만 지금은 EC2로 Jenkins server를 구축했기 때문에 Github Webhooks
Payload URL 수정해야한다.
또한 Jenkins EC2 서버로Github Webhooks로 부터 오는 api를 받을수 있게
EC2 인바운드 규칙을 편집해야 된다.

4.4.1 Github Webhooks 편집

4.4.2 EC2 인바운드 규칙 편집


위 두 IP가 GitHub 서버의 IP라고 한다.


5. Jenkins 세팅

앞서 포스터인 Jenkins Pipeline에서 했던거와 같이 Jenkins에 필요한 설정, Credentials, 플러그인 설치등을 해주었다.

추가로 Jenkins에서 DockerHub로 이미지를 Push 해주어야하기 때문에 도커 계정을
Credentials에 등록해주었다.


6. Jenkins Pipeline 작성

앞서 포스터인 Jenkins Pipeline에서 작성했던 Pipeline 스크립트를 기반으로 수정하였다.

pipeline {
    environment { 
        repository = "bonsik/codebox-project-images"  //docker hub id와 repository 이름
        DOCKERHUB_CREDENTIALS = credentials('dockerhub') // jenkins에 등록해 놓은 docker hub credentials 이름
    }
    
    agent any
    stages {
        stage('github clone') {
            agent any
            steps {
                git branch: 'main', 
                credentialsId: 'github', 
                url: 'https://github.com/YNCB/backEnd.git'
            }
            post {
                failure{
                    error "Fail Cloned Repository"
                }
            }
        }
        
        stage('Test project') {
            agent any
            steps{
                withGradle{
                    sh  '''
                        echo 'start Test'
                        chmod +x gradlew 
                        ./gradlew test
                        '''
                }
            }
            post{
                failure{
                    error 'Fail Test'
                }
            }
        }
        
        stage('Gradle Project Build') {
            agent any
            steps{
                sh '''
                    echo 'start bootJar'
                    ./gradlew clean bootJar
                    '''
            }
            post{
                failure{
                    error 'Fail Gradle Build'
                }
            }
        }
        
        stage('Docker Image Build') {
            agent any
            steps{
                sh '''
                    echo 'docker image build'
                    docker build -t $repository:1.0 .
                    '''
            }
            post{
                failure{
                    error 'Fail Docker Image Build'
                }
            }
        }
        
        stage("Dockerhub Login") {
            agent any
            steps{
                sh '''
                    echo 'dockerhub login'
                    echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin
                    '''
            }
            post{
                failure{
                    error 'Fail Docker Login'
                }
            }
        }
        
        stage("Upload image to dockerHub"){
            agent any
            steps{
                sh '''
                    echo 'dockerhub image deploy'
                    docker push $repository:1.0
                    '''
            }
            post{
                failure{
                    error 'Fail Docker Image Deploy'
                }
            }
        }
        
        stage('Cleaning up'){
            agent any
            steps{
                sh '''
                    echo 'docker image delete'
                    docker rmi $repository:1.0
                    '''
            }
            post{
                failure{
                    error 'Fail Cleaning up'
                }
            }
        }
        
        stage('Execuate image'){
            agent any
            steps {
                sshPublisher(
                    publishers: [
                        sshPublisherDesc(
                            configName: 'deploy_server', 
                            transfers: [
                                sshTransfer(cleanRemote: false, 
                                            excludes: '', 
                                            execCommand: 
                                '''echo [dockerhub password 작성] | docker login -u [dockerhub 계정 작성] --password-stdin 
                                   sh docker_deploy.sh  ''', 
                                            execTimeout: 120000, 
                                            flatten: false, 
                                            makeEmptyDirs: false, 
                                            noDefaultExcludes: false, 
                                            patternSeparator: '[, ]+', 
                                            remoteDirectory: '', 
                                            remoteDirectorySDF: false, 
                                            removePrefix: '', 
                                            sourceFiles: 'docker_deploy.sh')], 
                                            usePromotionTimestamp: false, 
                                            useWorkspaceInPromotion: false, 
                                            verbose: true
                                            )
                                        ]
                                )
            }
        }
    }
}
  • DockerHub에 이미지를 Push하기 위해서 DockerHub 계정을 환경변수로 정의하였다.

  • Window환경에서 작업시 Gradlew의 기본권한이 644로 설정되다고 하여서 ./gradlew test, ./gradlew build를 위해서 chmod +x gradlew를 통해 권한을 변경하였다.

  • DockerHub에 이미지를 Push하기 위해서는 이미지의 이름이 계정/레포지토리의 이름과 같아야 하기때문에 이미지 빌드시 docker build -t $respository:1.0으로 도커 이미지 이름을 만들었다.

  • DockerHub에 이미지를 Push 하는 저장소가 private 저장소 이므로 DockerHub에 로그인하는 과정을 넣었다.

  • 생성한 도커 이미지를 DockerHub에 Push한 다음 생성한 이미지를 삭제하기 위해 삭제 과정을 넣었다.

  • SSH를 이용하여 운영서버로 실행할 명령어를 작성했다.

    • DockerHub로 부터 이미지를 Pull하기 위해서 로그인 절차
    • 미리 운영서버에 작성해논 deploy.sh(도커허브로 부터 이미지 pull, 컨테이너 시작등) 실행 절차

      SSH란?
      : Secure Shell Protocal 약자로 네트워스 상의 다른 컴퓨터에 로그인하거나 원격 시스템에서 명령을 실행하고 다른 시스템으로 파일을 복사할수 있도록 해주는 프로토콜

      움 sshPublisher에 execCommand에 앞서 정의한 환경변수를 사용못하는건가?..움 선언형 문법을 아직 잘몰라서 나중에 시간이 되면 공부해봐야겠다.🤣


6. 실행결과

github push시 github webhooks로 jenkins에게 알림

profile
백엔드 개발자를 꿈꾸며 기록중💻

1개의 댓글

comment-user-thumbnail
2024년 1월 13일

도움이됐습니다 고마워요

답글 달기