Cloud Tech

도커 기반 애플리케이션 CI/CD 구성

Hanhorang31 2024. 12. 8. 03:30

CI/CD

CI/CD의 목표는 코드 빌드부터 프로덕션 환경으로의 배포까지 전 과정을 자동화된 프로세스를 구성하는 것입니다. 수동으로 애플리케이션을 업데이트하는 경우 사람의 실수로 인해 구성 드리프트가 발생하거나 보안 취약점이 노출될 가능성이 높아지며, 이는 결국 애플리케이션 배포의 품질 저하로 이어질 수 있습니다.

CI

CI(Continuous Integration)은 코드 변경 분을 버전 관리 리포지터리로 계속 통합하는 프로세스입니다. 변경된 코드가 리포지터리에 커밋되면 빌드가 시작됩니다. 빌드는 애플리케이션 특성에 따라 다른데 빠른 피드백을 원한다면 빌드 과정 중 테스트 단계를 포함하여 빌드를 망친 코드 변경분을 빠르게 찾을 수 있습니다.

또한, 도커 기반의 애플리케이션들은 이미지 빌드 과정이 포함됩니다.

이미지 빌드시 고려사항은 다음과 같습니다.

  • 이미지 사이즈 최적화 : 사이즈가 작을수록 이미지를 가져와 배포하는 시간이 줄고 이미지 보안성이 좋아지기 때문입니다. 최적화 방법으로 멀티스테이지 빌드, 최적화된 베이스 이미지가 있습니다.
  • 컨테이너 이미지 태깅 : 컨테이너 이미지를 빌드할 때 이미지 버전을 쉽게 알아볼 수 있게 이미지 태깅 전략을 신중하게 검토해야 합니다. 기본 최신 태깅인 latest 버전이 아니므로 어떤 코드 변경 내용이 포함됐는 지 알 수가 없습니다. 이에 빌드 ID를 사용하거나, 깃 해시를 이용하여 브랜치 전략과 함께 사용할 수 있습니다.

 

CD

CD(Continuous Delivery)는 CI 파이프라인을 무사히 통고환 변경분을 사람의 개입없이 프로덕션에 매끄럽게 배포하는 프로세스입니다. 앞서 빌드한 컨테이너 이미지는 개발, 스테이징에 걸쳐 프로덕션으로 승격되는 불변의 오브젝트입니다. 컨테이너 이미지 운영를 통해 구성 드리프트 변경없이 일관된 환경을 유지할 수 있습니다.

컨테이너 배포 전략은 쿠버네티스와 addon 툴들을 통해 구성할 수 있습니다.

  • 롤링 업데이트
  • 블루/그린 배포
  • 카나리 배포

 

CI/CD 환경 구성

CloudNet@ 스터디 실습 내용으로 Gogs → Jenkins → DockerHub / DockerEngine(Run) 파이프라인을 구성하여 도커 기반의 애플리케이션 CI/CD 를 구성하겠습니다.

CloudNet@ CI/CD 스터디

 

구성 도구 설치

컨테이너 2대를 구성하여 Jenkins와 gogs를 올리겠습니다.

사전 구성 환경에 docker가 설치되어 있어야합니다.
cat <<EOT > docker-compose.yaml
services:

  jenkins:
    container_name: jenkins
    image: jenkins/jenkins
    restart: unless-stopped
    networks:
      - cicd-network
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - jenkins_home:/var/jenkins_home

  gogs:
    container_name: gogs
    image: gogs/gogs
    restart: unless-stopped
    networks:
      - cicd-network
    ports:
      - "10022:22"
      - "3000:3000"
    volumes:
      - gogs-data:/data

volumes:
  jenkins_home:
  gogs-data:

networks:
  cicd-network:
    driver: bridge
EOT


# 배포
docker compose up -d
docker compose ps
docker compose top

Jenkins 설정

docker compose exec -u root jenkins bash
apt-get update && apt-get install -y tree net-tools iproute2 

ps -ef
tree /var/jenkins_home

8080 포트에는 Jenkins 웹 UI 와 50000 포트는 Jenkins 에이전트 통신됩니다.

초기 비밀번호는 로그에 표시된 경로를 통해 확인할 수 있습니다.

docker compose logs jenkins -f 
-------
...
# 하단 로그 확인
jenkins  | 
jenkins  | *************************************************************
jenkins  | *************************************************************
jenkins  | *************************************************************
jenkins  | 
jenkins  | Jenkins initial setup is required. An admin user has been created and a password generated.
jenkins  | Please use the following password to proceed to installation:
jenkins  | 
jenkins  | 45cd37..bcbab5a64 # 비밀번호 입력 
jenkins  | 
jenkins  | This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
jenkins  | 
jenkins  | *************************************************************
jenkins  | *************************************************************
jenkins  | *************************************************************
...

Jenkins에 접근하면 admin User 설정과 IP Port 등록이 있습니다.

필자는 Admin User에 개인 정보를 입력하였고, IP는 로컬 IP(192.168.1.6) 를 확인하여 입력하였습니다.

ifconfig | grep 192.
        inet 192.168.1.6 netmask 0xffffff00 broadcast 192.168.x.x

추가로 Jenkins 컨테이너에서 호스트 도커 데몬을 사용하기 위해 설정(Docker-out-of-Docker)이 필요합니다.

docker compose exec --privileged -u root jenkins bash 

curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update && apt install docker-ce-cli curl tree jq -y

# 도커 그룹 권한 확인 
ls -l /var/run/docker.sock

# enkins 유저도 docker를 실행할 수 있도록 권한을 부여
groupadd -g 2000 -f daemon
chgrp daemon  /var/run/docker.sock
ls -l /var/run/docker.sock
usermod -aG daemon jenkins
cat /etc/group | grep daemon 

필자의 경우 도커 실행 그룹이 daemon 으로 설정되어 있고, 그룹 권한에 w권한이 빠져있어 추가하였습니다.

# jenkins 컨테이너에서 도커 권한 확인 
ls -l /var/run/docker.sock 

# 그룹에 쓰기 권한 추가
chmod g+rw /var/run/docker.sock

실제 젠킨스에서 도커 동작을 확인하겠습니다.

젠킨스 > Dashboard > 새로운 Item > Freestyle project > Build Steps > Execute shell 에 아래 내용 입력 후 > SAVE

echo "docker check" | tee test.txt
docker ps

젠킨스 왼쪽 메뉴 지금 빌드 > #1 > Console Output 확인

 

 

Gogs 설정

Gogs 초기 설정은 도메인 127.0.0.1:3000/install 에 접근해서 설정할 수 있습니다.

데이터베이스 유형 : SQLite3 (인메모리DB)

애플리케이션 URL : http://<각자 자신의 IP>:3000/

기본 브랜치 : master > main

접근 키 발급 : Your Setting > Application > Generate New Token

레파지토리 생성

아래와 같이 Private Repo 로 설정하여 레파지토리를 생성해주세요.

저장소 설정

Jenkins 에서 gogs에서 생성한 레파지토리를 가져와서 git 작업을 진행하겠습니다.

docker compose exec jenkins bash 

git config --global user.name "hanhorang"
git config --global user.email "tmdgh663@gmail.com"
git config --global init.defaultBranch main

cd /var/jenkins_home/ 

git clone http://192.168.1.6:3000/hanhorang/dev-app.git
-----
Username for 'http://192.168.1.6:3000': hanhorang
Password for 'http://admin@192.168.1.6:3000': gogs 토큰 입력


cd dev-app

# server.py 파일 작성
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime

class RequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        now = datetime.now()
        response_string = now.strftime("The time is %-I:%M:%S %p, CloudNeta Study.\n")
        self.wfile.write(bytes(response_string, "utf-8")) 

def startServer():
    try:
        server = ThreadingHTTPServer(('', 80), RequestHandler)
        print("Listening on " + ":".join(map(str, server.server_address)))
        server.serve_forever()
    except KeyboardInterrupt:
        server.shutdown()

if __name__== "__main__":
    startServer()
EOF


# Dockerfile 생성
cat > Dockerfile <<EOF
FROM python:3.12
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app 
CMD python3 server.py
EOF


# VERSION 파일 생성
echo "0.0.1" > VERSION

# gogs Repo 푸쉬
git add .
git commit -m "Add dev-app"
git push -u origin main

도커 허브 설정

이미지 원격 저장소인 도커 허브를 통해 이미지 저장소를 생성하겠습니다.

동작 확인을 위해 Jenkins 파이프라인을 다음과 같이 생성하겠습니다.

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://192.168.1.6:3000/hanhorang/dev-app.git',  // Git에서 코드 체크아웃
                 credentialsId: 'gogs-dev-app'  // Credentials ID
            }
        }
        stage('Read VERSION') {
            steps {
                script {
                    // VERSION 파일 읽기
                    def version = readFile('VERSION').trim()
                    echo "Version found: ${version}"
                    // 환경 변수 설정
                    env.DOCKER_TAG = version
                }
            }
        }
        stage('Docker Build and Push') {
            steps {
                script {
                    docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-credentials') {
                        // DOCKER_TAG 사용
                        def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
                        appImage.push()
                    }
                }
            }
        }
    }
    post {
        success {
            echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
        }
        failure {
            echo "Pipeline failed. Please check the logs."
        }
    }
}
  • 에러가 난다면 로그를 확인해주세요. 대부분 연동 접근 계정 정보를 잘못 입력해서 발생합니다.

 

도커 애플리케이션 기반 CI/CD 구성

위 설치한 도구들을 연동하여 Gogs → Jenkins → DockerHub / DockerEngine(Run) CI/CD를 구성하겠습니다.

CI/CD를 구성하기 위해 다음과 같은 단계가 필요합니다.

  1. Jenkins 내 gogs 접근 토큰 저장 (상단 Jenkins 설정 참고)
  2. Jenkins 플러그인 설치 (젠킨스 관리 > Plugins > Available plugins)
  3. Jenkins 내 도커 허브 자격증명 설정 (상단 도커 허브 설정 참고)
  4. Gogs Webhooks 설정
  5. SCM 파이프라인 설정

 

4. Gogs Webhooks 설정

젠킨스가 gogs가 코드 변경이 될 경우 자동으로 빌드 트리거를 할 수 있도록 웹 훅을 설정하겠습니다.

gogs repo > setting > Webhooks > gogs

Payload URL 값이 동일 로컬 IP로 등록이 안됩니다.

이를 해결하기 위해 gogs 컨테이너 안 설정 파일에서 로컬 IP 를 허용할 수 있도록 수정이 필요합니다.

# gogs data/gogs/conf/app.ini
..
[security]
INSTALL_LOCK = true
SECRET_KEY   = 3w5FqHaTRpaozbv
LOCAL_NETWORK_ALLOWLIST = 192.168.1.6 # IP 옵션 값 추가 
..

# gogs 재시작
docker compose restart gogs

 

5. Jenkins SCM 파이프라인 구성

SCM 파이프라인은 소스 repo 안에 젠킨스 빌드 파이프라인 정보를 입력한 것을 뜻합니다.

New ITEM > 파이프라인 > SCM-Pipeline (웹훅 생성 매개변수)

SCM 파이프라인 빌드 파일을 생성하고 Repo에 푸쉬하겠습니다.

# /var/jenkins_home/dev-app/JenkinsFile 파일 생성
pipeline {
    agent any
    environment {
        DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://192.168.1.6:3000/devops/dev-app.git',  // Git에서 코드 체크아웃
                 credentialsId: 'gogs-dev-app'  // Credentials ID
            }
        }
        stage('Read VERSION') {
            steps {
                script {
                    // VERSION 파일 읽기
                    def version = readFile('VERSION').trim()
                    echo "Version found: ${version}"
                    // 환경 변수 설정
                    env.DOCKER_TAG = version
                }
            }
        }
        stage('Docker Build and Push') {
            steps {
                script {
                    docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-credentials') {
                        // DOCKER_TAG 사용
                        def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
                        appImage.push()
                    }
                }
            }
        }
    }
    post {
        success {
            echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
        }
        failure {
            echo "Pipeline failed. Please check the logs."
        }
    }
}

docker compose exec jenkins bash


cd /var/jenkins_home/dev-app/
git add . && git commit -m "Jenkinsfile add & VERSION 0.0.1 Changed" && git push -u origin main

Repo 푸쉬시 자동으로 jenkins에서 이미지를 생성하고 배포됨을 알 수 있습니다.

여기서 Repo 내 VERSION을 0.0.2 로 수정하고 다시 푸쉬하면 동일하게 이미지가 생성됨을 확인할 수 있습니다.

cd /var/jenkins_home/dev-app/
git add . && git commit -m "Jenkinsfile add & VERSION 0.0.2 Changed" && git push -u origin main

참고

CloudNet@ CI/CD 스터디

(book) kubernetes-for-developers

(book) 쿠버네티스 창시자에게 배우는 모범 사례