들어가며
안녕하세요 여러분 벌써 12월이네요!
한 해 마무리되어 가면서 조금 더 학습 성취를 이루고자 가시다 님의 스터디에 참여했답니다 :,)
이번 포스팅은 CloudNet@ 커뮤니티에서 주최하는 CI/CD 스터디 1주 차 주제인 "Jenkins CI/CD + Docker"에 대해서 정리한 내용입니다.
실습환경 구축
실습 환경은 Mac OS 기준 docker와 vscode 위에서 진행하였습니다.
Docker Desktop (brew install --cask docker)
VSCODE (Docker 플러그인 확장)
1. 컨테이너를 활용한 애플리케이션 개발
1. 컴파일 없는 애플리케이션을 컨테이너로 배포
# 코드 작성
mkdir 1.1 && cd 1.1
echo "print ('Hello Docker')" > hello.py
cat > Dockerfile <<EOF
FROM python:3
COPY . /app
WORKDIR /app
CMD python3 hello.py
EOF
# 컨테이너 이미지 빌드
docker pull python:3
docker build . -t hello
docker image ls -f reference=hello #필터링
# 컨테이너 실행
docker run --rm hello
# 코드 수정
echo "print ('Hello CloudNet@')" > hello.py
# 컨테이너 이미지 빌드 : latest 활용 해보자!
docker build . -t hello:1
docker image ls -f reference=hello
docker tag hello:1 hello:latest
docker image ls -f reference=hello
# 컨테이너 실행
docker run --rm hello:1
docker run --rm hello
2. 컨테이너에서 컴파일을 통해 애플리케이션을 배포
원리를 이해하는 차원에서 진행한 것. 실제로 이런 형태로 배포가 일어나게 되면 컨테이너 탈취 시 코드 변경을 통해 애플리케이션 변조가 가능하기 때문에 운영 환경에서는 조심해야 할 것으로 생각된다.
# 코드 작성
mkdir 1.2 && cd 1.2
cat > **Hello.java** <<EOF
class Hello {
public static void main(String[] args) {
System.out.println("**Hello Docker**");
}
}
EOF
cat > **Dockerfile** <<EOF
FROM openjdk
COPY . /app
WORKDIR /app
RUN **javac Hello.java** # The complie command
CMD java Hello
EOF
# 컨테이너 이미지 빌드
docker pull openjdk
docker build . -t hello:2
docker tag hello:2 hello:latest
docker image ls -f reference=hello
# 컨테이너 실행
docker run --rm hello:2
docker run --rm hello
# 컨테이너 이미지 내부에 파일 목록을 보면 어떤가요? 꼭 필요한 파일만 있는가요? 보안적으로 어떨까요?
docker run --rm hello ls -l
# RUN 컴파일 시 소스코드와 java 컴파일러(javac)가 포함되어 있음. 실제 애플리케이션 실행에 필요 없음.
docker run --rm hello javac --help
docker run --rm hello ls -l
3. Compileing code with a multistage build
앞서 보았던 docker 내부에서 빌드하는 방식은 로컬 환경에서 빌드를 하지 않아도 되기 때문에 편리하지만, 컨테이너 탈취 시 코드정보를 모두 노출시킬 수 있는 문제가 있습니다.
Multistage build 기법은 앞서보았던 빌드 후 나온 App binary를 이용하여 Runtime container에서 App을 동작시키는 방법으로써, 자유도와 보안성 및 편의성을 모두 갖춘 스마트한 방법입니다.
mkdir 1.3 && cd 1.3
cat > Hello.java <<EOF
class Hello {
public static void main(String[] args) {
System.out.println("Hello Multistage container build");
}
}
EOF
cat > Dockerfile <<EOF
FROM openjdk:11 AS buildstage
COPY . /app
WORKDIR /app
RUN javac Hello.java
FROM openjdk:11-jre-slim
COPY --from=buildstage /app/Hello.class /app/
WORKDIR /app
CMD java Hello
EOF
# 컨테이너 이미지 빌드 : 용량 비교 해보자!
docker build . -t hello:3
docker tag hello:3 hello:latest
docker image ls -f reference=hello
# 컨테이너 실행
docker run --rm hello:3
docker run --rm hello
# 컨테이너 이미지 내부에 파일 목록을 보면 어떤가요?
docker run --rm hello ls -l
docker run --rm hello javac --help
실습을 해보면 multistage build를 수행했을 때 2 버전에 비해 이미지의 사이즈가 현저히 줄어드는 것을 확인할 수 있습니다. 또한 컨테이너 내부의 파일을 보면 docker 파일이나 코드 없이 class 파일만 있는 것도 장점입니다.
4. Jib로 자바 컨테이너 빌드
Docker를 통해 컨테이너를 빌드하는 기본적인 방식은 다음과 같습니다.
JIB 플러그인을 사용하면 다음과 같이 프로젝트에서 빌드와 동시에 이미지가 만들어지고 저장소에 Push까지 처리합니다.
저희 팀에서는 spring boot application 개발시 gradle용 jib 플러그인을 사용하여 빌드합니다.
plugins {
id 'java'
id 'org.springframework.boot' version '2.5.8'
id 'io.spring.dependency-management' version '1.1.3'
id 'com.google.cloud.tools.jib' version '2.8.0'
}
group = 'com.winitech'
version = '0.0.1-SNAPSHOT'
dependencies {
....
}
jib {
from {
image = "eclipse-temurin:17-jre-alpine"
}
container {
extraClasspath ['/app.jar']
# -Xms128m: JVM의 초기 힙 메모리를 128MB로 설정
# -Xmx128m: JVM의 최대 힙 메모리를 128MB로 설정.
jvmFlags = ["-Xms128m", "-Xmx128m"]
ports = ['8092']
}
}
test {
useJUnitPlatform()
}
5. Containerizing an application server
파이썬으로 현재시간을 리턴하는 웹 애플리케이션 서버를 만드는 실습입니다.
# 코드 작성
mkdir 1.5 && cd 1.5
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 %p, UTC.\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
cat > Dockerfile <<EOF
FROM python:3.12
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
CMD python3 server.py
EOF
# 컨테이너 이미지 빌드 : 용량 비교 해보자!
docker pull python:3.12
docker build . -t timeserver:1 && docker tag timeserver:1 timeserver:latest
docker image ls -f reference=timeserver
# 컨테이너 실행
docker run -d -p 8000:80 --name=timeserver timeserver
# 컨테이너 접속 및 로그 확인
curl http://localhost:8000
docker logs timeserver
# 컨테이너 이미지 내부에 파일 확인
docker exec -it timeserver ls -l
# 컨테이너 이미지 내부에 server.py 파일 수정 후 반영 확인 : VSCODE 경우 docker 확장프로그램 활용
docker exec -it timeserver cat server.py
# 컨테이너 접속 후 확인
curl http://localhost:8080
# 컨테이너 삭제
docker rm -f timeserver
하지만, 소스코드를 변경해도 내용 반영이 되지 않습니다. 그 이유는, 컨테이너가 run 상태일 때 내부적으로 Python 실행 프로세스가 이미 동작 중이기 때문입니다. Python 프로세스는 시작 시 로드된 소스 파일을 기반으로 실행되며, 실행 도중에 소스 코드가 변경되더라도 이를 자동으로 반영하지 않습니다.
개발 편의성을 위해 코드를 업데이트하기 위해선 아래와 같이 코드 내용을 동적으로 변경하는 라이브러리를 사용하면 됩니다.
6. Using Docker Compose for local testing
각각의 언어별로 reroad를 지원하는 라이브러리가 다르지만, 파이썬의 경우 reloading 라이브러리를 사용하여 GET 기능으로 Disk로부터 reload로 할 수 있습니다.
#
# 코드 작성
mkdir 1.6 && cd 1.6
cat > server.py <<EOF
from reloading import reloading
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class RequestHandler(BaseHTTPRequestHandler):
@reloading # By adding the @reloading tag to our method, it will be reloaded from disk every time it runs so we can change our do_GET function while it’s running.
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 %H:%M:%S, Docker End.")
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
# reloading 라이브러리 설치 필요
cat > Dockerfile <<EOF
FROM python:3
RUN pip install reloading
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
CMD python3 server.py
EOF
cat > docker-compose.yaml <<EOF
services:
frontend:
build: .
command: python3 server.py
volumes:
- type: bind
source: .
target: /app
environment:
PYTHONDONTWRITEBYTECODE: 1 # Sets a new environment variable so that Python can be made to reload our source
ports:
- "8080:80"
EOF
#
docker compose build; docker compose up -d
# 컴포즈로 실행 시 이미지와 컨테이너 네이밍 규칙을 알아보자!
docker compose ps
docker compose images
#
curl http://localhost:8080
docker compose logs
실행 후 다음과 같이 소스코드를 변경해 봅니다.
# VSCODE 에서 호스트에서 server.py 코드 수정(볼륨 공유 상태)
cat server.py
...
response_string = now.strftime("The time is %H:%M:%S, Docker EndEndEnd!")
self.wfile.write(bytes(response_string,"utf-8"))
...
#
curl http://localhost:8080
#
docker compose down
실행 결과를 살펴보면 다음과 같이 컨테이너를 다시 빌드하지 않고도 로컬 소스코드의 환경이 컨테이너에 반영된 것을 확인할 수 있습니다. (아주 유용하네요! 바로 현업에 적용 적용.. 물론 개발계에서만.. *_*)
참고: Docker Compose에서 컨테이너 네이밍은 기본적으로 프로젝트 이름, 서비스 이름, 인스턴스 번호를 기반으로 이루어집니다. 이를 통해 Docker Compose가 생성하는 컨테이너 이름은 <프로젝트 이름>_<서비스 이름>_<인스턴스 번호> 형식을 따르기 때문에 실행 중인 컨테이너 이름은 16-frontend-1이 됩니다.
2. CI/CD 실습 환경 구성
개발자들이 여러 명 프로젝트에 참여하게 되면, 소스코드 수정 및 컨테이너화가 매우 번거로워집니다. 그래서 CI/CD 파이프라인 기술이 도입 되게 됩니다.
실습에서는 Jenkins와 Gogs 오픈소스 툴을 사용하여 CI/CD 환경을 구성합니다.
주요 내용 정리
항목 | Jenkins | Gogs |
툴 종류 | CI/CD 도구 |
Git 저장소 관리 도구
|
주요 기능 | - 소프트웨어 개발 프로세스의 다양한 단계를 자동화 - 테스트 및 배포 - 플러그인 확장성 지원 - 파이프라인 워크플로우 관리 |
- Git 저장소 호스팅
- 경량화된 설치 및 사용 - SSH/HTTP를 통한 코드 관리 지원 |
특징 | - 오픈소스 - 다양한 프로그래밍 언어 지원 - 수많은 플러그인으로 확장 가능 - 멀티플랫폼 호환성 |
- 오픈소스
- 경량화 및 간단한 설정 - 자체 서버에서 실행 가능한 독립형 애플리케이션 |
설치 및 배포 | - 로컬 또는 클라우드 환경에서 설치 가능 - Java 기반 (JAR 파일 실행 또는 WAR 파일 배포) |
- 로컬 서버에 설치 가능
- Go 언어로 개발되어 경량화 및 빠른 실행 지원 |
사용 사례 | - 소프트웨어 빌드 및 테스트 자동화 - CI/CD 파이프라인 구현 - DevOps 워크플로우 통합 |
- 소규모 프로젝트의 Git 코드 관리
- 경량화된 Git 서버가 필요한 환경 - 자체 호스팅 환경 |
운영 환경 | - Windows, macOS, Linux - Docker 컨테이너 및 Kubernetes 환경에서 사용 가능 |
- Windows, macOS, Linux
- Docker로 배포 가능 |
장점 | - 다양한 플러그인으로 기능 확장 가능 - 대규모 프로젝트 지원 - 커뮤니티 지원 및 문서화 우수 |
- 경량화되어 리소스 소모가 적음
- 설치와 사용이 간단함 - 소규모 프로젝트와 개인용에 적합 |
단점 | - 설정이 복잡할 수 있음 - 리소스 소모가 많을 수 있음 |
- 대규모 프로젝트 관리에는 적합하지 않을 수 있음
- 기능이 제한적 |
공식 웹사이트 | https://www.jenkins.io | https://gogs.io |
Jenkins와 Gogs 설치는 다음과 같이 진행합니다.
# 작업 디렉토리 생성 후 이동
mkdir cicd-labs
cd cicd-labs
#
cat <<EOT > docker-compose.yaml
services:
jenkins:
container_name: jenkins
image: jenkins/jenkins
restart: unless-stopped
networks:
- cicd-network
ports:
- "8079: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
# 기본 정보 확인
for i in gogs jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "whoami && pwd"; echo; done
# 도커를 이용하여 각 컨테이너로 접속
docker compose exec jenkins bash
exit
docker compose exec gogs bash
exit
# 초기 설정
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
Jenkins URL 설정 : 각자 자신의 PC의 IP를 입력
다음으로 DooD 방식을 사용하기 위한 추가 설정을 적용합니다.
잠깐 스터디에서 언급된 DinD 방식과 DooD 방식에 대해 살펴보도록 하겠습니다.
DinD 방식은 Jenkins Container 안에 Docker 데몬이 별도로 존재하며, 이 Docker 데몬에서 컨테이너를 실행합니다 Jenkins와 컨테이너 실행 환경이 완전히 분리된 상태로 동작합니다.
반면 DooD 방식을 사용하면 Jenkins는 Docker 클라이언트를 통해 호스트의 Docker 데몬에 명령을 내립니다. 즉 모든 Docker 명령은 호스트 Docker 데몬에서 실행되며, 컨테이너를 직접 제어합니다.
항목 | DinD 방식 | DooD 방식 |
동작 방식 | 컨테이너 내에서 Docker 데몬 실행 |
호스트의 Docker 데몬 공유
|
격리성 | 완전한 격리 환경 제공 |
호스트와 공유, 격리성이 낮음
|
성능 | 상대적으로 낮음 | 상대적으로 높음 |
설정 난이도 | 설정이 복잡 | 설정이 간단 |
보안 | 컨테이너 간 독립적, 상대적으로 안전 |
호스트와 연결되어 보안 취약 가능성 있음
|
실습에서는 컨테이너를 더 편하게 구성할 수 있는 DooD 방식을 통해 구축했습니다.
# Jenkins 컨테이너 내부에 도커 실행 파일 설치
docker compose exec --privileged -u root jenkins bash
-----------------------------------------------------
id
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
docker info
docker ps
which docker
# Jenkins 컨테이너 내부에서 root가 아닌 jenkins 유저도 docker를 실행할 수 있도록 권한을 부여
groupadd -g 2000 -f docker
chgrp docker /var/run/docker.sock
ls -l /var/run/docker.sock
usermod -aG docker jenkins
cat /etc/group | grep docker
exit
--------------------------------------------
# jenkins item 실행 시 docker 명령 실행 권한 에러 발생 : Jenkins 컨테이너 재기동으로 위 설정 내용을 Jenkins app 에도 적용 필요
docker compose restart jenkins
sudo docker compose restart jenkins # Windows 경우 이후부터 sudo 붙여서 실행하자
# jenkins user로 docker 명령 실행 확인
docker compose exec jenkins id
docker compose exec jenkins docker info
docker compose exec jenkins docker ps
Gogs 환경 설정은 다음과 같이 진행합니다.
다음으로는 젠킨스에서 레포지토리 API에 접근할 수 있도록 토큰을 발행합니다.
다음으로는 레포지토리를 만듭니다.
다음으로는 레포지토리에 코드를 커밋할 수 있도록 Jenkins 내부에서 소스코드 업로드 환경을 구성합니다.
(로컬 PC에서 할 수 도 있지만, 편의 성을 위해 구성한 환경입니다.)
docker compose exec jenkins bash
-----------------------------------
whoami
pwd
cd /var/jenkins_home/
tree
#
git config --global user.name "<Gogs 계정명>"
git config --global user.name "devops"
git config --global user.email "a@a.com"
git config --global init.defaultBranch main
#
git clone <각자 Gogs dev-app repo 주소>
git clone http://192.168.254.124:3000/devops/dev-app.git
Cloning into 'dev-app'...
Username for 'http://192.168.254.124:3000': devops # Gogs 계정명
Password for 'http://devops@192.168.254.124:3000': <토큰> # 혹은 계정암호
...
#
tree dev-app
cd dev-app
git branch
git remote -v
# 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
#
git add .
git commit -m "Add dev-app"
git push -u origin main
...
내부에서 코드를 추가한 후 Push 하게 되면 Gogs의 dev-app 레포지토리에 잘 Push 되는 것을 확인할 수 있습니다.
마지막으로 이미지를 업로드를 할 수 있도록 도커 허브에서 dev-app이라는 Private repository를 생성합니다.
3. Jenins 기본 사용
Jenkins에서 작업(프로젝트, Job, Item)은 다음 세 가지 주요 유형의 지시 사항으로 구성됩니다.
Trigger (작업 실행 시점 정의)
Trigger는 작업을 언제 시작할지 정의합니다.
작업 수행 조건을 지정하며, Jenkins는 조건에 따라 작업을 실행합니다.
Build Step (작업 단계 정의)
Build Step은 작업을 구성하는 단계별 수행 태스크입니다.
특정 목표를 달성하기 위해 여러 단계로 나누어 설정할 수 있습니다.
Post-Build Action (작업 완료 후 수행할 명령 정의)
Post-Build Action은 작업이 완료된 후에 수행할 추가 동작을 정의합니다.
작업 결과(성공 또는 실패)에 따라 후속 조치를 설정할 수 있습니다.
(참고) 젠킨스의 빌드 : 젠킨스 작업의 특정 실행 버전
- 사용자는 젠킨스 작업을 여러 번 실행할 수 있는데, 실행될 때마다 고유 빌드 번호가 부여됩니다.
- 작업 실행 중에 생성된 아티팩트, 콘솔 로드 등 특정 실행 버전과 관련된 모든 세부 정보가 해당 빌드 번호로 저장됩니다.
이제 Jenkins로 파이프라인 설정을 진행해 봅니다.
빌드를 해보면 다음과 같이 콘솔 출력을 확인할 수 있습니다.
작업공간을 살펴보면 test.txt라는 파일이 생성되어 있습니다.
이는 컨테이너에 직접 접속하여 확인할 수 있습니다. 빌드를 하면 산출물들이 workspace 안에 저장됩니다.
다음으로 gogs 레포지토리에서 코드를 가져올 수 있도록 하기 위해 자격증명을 설정합니다.
두 번째 아이템을 만듭니다.
두번째 아이템에서는 FirstPara라는 매개변수를 사용하여 파이프라인이 실행되도록 합니다.
매개변수 출력 및 소스코드의 VERSION 파일을 출력하도록 설정합니다.
다음으로 파이프라인 구성을 위해 Plugin을 설치합니다.
- Jenkins Plugin 설치
설치하고 나면 다음과 같이 설치가 된 것을 확인할 수 있습니다.
다음으로 Docker레포지토리에 푸시를 하기 위해 도커 계정 정보를 연결합니다.
일반적인 파이프라인의 구성은 다음과 같습니다.
출처 - https://www.jenkins.io/doc/book/pipeline/
파이프라인을 구성하면 다음과 같은 장점을 얻을 수 있습니다.
- 코드 : 애플리케이션 CI/CD 프로세스를 코드 형식으로 작성할 수 있고, 해당 코드를 중앙 리포지터리에 저장하여 팀원과 공유 및 작업 가능
- 내구성 : 젠킨스 서비스가 의도적으로 또는 우발적으로 재시작되더라도 문제없이 유지됨
- 일시 중지 가능 : 파이프라인을 실행하는 도중 사람의 승인이나 입력을 기다리기 위해 중단하거나 기다리는 것이 가능
- 다양성 : 분기나 반복, 병렬 처리와 같은 다양한 CI/CD 요구 사항을 지원
파이프라인은 다음과 같이 구성합니다.
- 파이프라인 : 전체 빌드 프로세스를 정의하는 코드.
- 노드 node = Agent : 파이프라인을 실행하는 시스템
- Stages : 순차 작업 명세인 stage 들의 묶음
- stage : 특정 단계에서 수행되는 작업들의 정의. (옵션) agents 설정
- steps : 파이프라인의 특정 단계에서 수행되는 단일 작업을 의미.
- post : 빌드 후 조치, 일반적으로 stages 작업이 끝난 후 추가적인 steps/step
- Directive : environment, parameters, triggers, input, when - Docs
- environment (key=value) : 파이프라인 내부에서 사용할 환경변수
- parameters : 입력받아야 할 변수를 정의 - Type(string, text, choice, password …)
- when : stage를 실행할 조건 설정
파이프라인의 구문은 선언형과 스크립트형이 있습니다.
선언형 파이프라인은 최신 문법으로써 젠킨스에서 권장하는 쉬운 방법입니다. 스크립트 형은 커스텀 작업에 용이한데, 복잡하고 난도가 높습니다.
첫 번째 파이프라인을 구성합니다.
pipeline {
agent any
stages {
stage('Hello') {
steps {
echo 'Hello World'
}
}
stage('Deploy') {
steps {
echo "Deployed successfully!";
}
}
}
}
주요 콘솔 출력값은 다음과 같습니다.
Started by user DEVLOS
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/jenkins_home/workspace/pipeline-ci
[Pipeline] {
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Checkout)
[Pipeline] git
The recommended git tool is: NONE
using credential gogs-dev-app
> git rev-parse --resolve-git-dir /var/jenkins_home/workspace/pipeline-ci/.git # timeout=10
Fetching changes from the remote Git repository
> git config remote.origin.url http://192.168.219.112:3000/devlos/dev-app.git # timeout=10
Fetching upstream changes from http://192.168.219.112:3000/devlos/dev-app.git
> git --version # timeout=10
> git --version # 'git version 2.39.5'
using GIT_ASKPASS to set credentials
> git fetch --tags --force --progress -- http://192.168.219.112:3000/devlos/dev-app.git +refs/heads/*:refs/remotes/origin/* # timeout=10
> git rev-parse refs/remotes/origin/main^{commit} # timeout=10
Checking out Revision cf056d9800ed2442d42b79235149091f61cf07f2 (refs/remotes/origin/main)
> git config core.sparsecheckout # timeout=10
> git checkout -f cf056d9800ed2442d42b79235149091f61cf07f2 # timeout=10
> git branch -a -v --no-abbrev # timeout=10
> git checkout -b main cf056d9800ed2442d42b79235149091f61cf07f2 # timeout=10
Commit message: "Add dev-app"
First time build. Skipping changelog.
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Read VERSION)
[Pipeline] script
[Pipeline] {
[Pipeline] readFile
[Pipeline] echo
Version found: 0.0.1
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Docker Build and Push)
[Pipeline] script
[Pipeline] {
[Pipeline] withEnv
[Pipeline] {
[Pipeline] withDockerRegistry
$ docker login -u devlos0322 -p ******** https://index.docker.io/v1/
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /var/jenkins_home/workspace/pipeline-ci@tmp/df995e04-41ef-4971-a2ce-5a4e27f9da21/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores
Login Succeeded
[Pipeline] {
[Pipeline] isUnix
[Pipeline] withEnv
[Pipeline] {
[Pipeline] sh
+ docker build -t devlos0322/dev-app:0.0.1 .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 125B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/python:3.12
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [internal] load build context
#4 transferring context: 46.55kB done
#4 DONE 0.0s
#5 [1/3] FROM docker.io/library/python:3.12
#5 CACHED
#6 [2/3] COPY . /app
#6 DONE 0.0s
#7 [3/3] WORKDIR /app
#7 DONE 0.0s
#8 exporting to image
#8 exporting layers done
#8 writing image sha256:7f99fe8d74a7389698a9320cb602b7fd919a3b46668c4cbaa0c3a70aec6d9b52 done
#8 naming to docker.io/devlos0322/dev-app:0.0.1 done
#8 DONE 0.0s
[Pipeline] sh
+ docker tag devlos0322/dev-app:0.0.1 index.docker.io/devlos0322/dev-app:0.0.1
+ docker push index.docker.io/devlos0322/dev-app:0.0.1
The push refers to repository [docker.io/devlos0322/dev-app]
31c72bb4adf1: Mounted from library/python
0.0.1: digest: sha256:6c7905ca1739ccf23b419361d8ee07ea9455010e51ea20f15e28120e8cce6b14 size: 2211
Docker image devlos0322/dev-app:0.0.1 has been built and pushed successfully!
Finished: SUCCESS
4. 도커 기반 애플리케이션 CI/CD 구성
마지막 실습은 SCM을 통해서 소스코드로 파이프라인을 관리하는 것입니다.
- 사용자 코드 푸시:
- 사용자가 Git 저장소에 코드를 푸시하면, CI/CD 서버가 이를 감지하고 작업을 트리거합니다.
- CI/CD 서버 작업:
- Git 저장소에서 코드를 가져옵니다.
- Dockerfile을 기반으로 애플리케이션을 빌드하고 Docker 이미지를 생성합니다.
- 생성된 이미지를 컨테이너 이미지 저장소에 업로드합니다.
- 이미지 배포:
- 대상 서버의 도커 엔진이 컨테이너 이미지 저장소에서 이미지를 가져옵니다.
- 도커 엔진은 가져온 이미지를 기반으로 컨테이너를 실행하거나 업데이트합니다.
gogs webhook에서 local IP를 허용하도록 Gogs 컨테이너에 app.ini 파일을 수정합니다.
- Payload URL : http://192.168.219.112:8080/gogs-webhook/?job=SCM-Pipeline/
- Content Type : `application/json`
- Secret : `qwe123`
Jenkins 서버에 gogs 플러그인을 깔면 자동으로 해당 url을 인지할 수 있도록 합니다.
Secret은 Jenkins가 웹훅을 체크하기 위한 용도로 사용합니다.
정상적으로 등록이 되면 다음과 같이 webhook 리스트에 gogs가 표시됩니다.
다음으로 slack 연동을 위한 webhooks을 등록해 줍니다.
이제 SCM 기반 파이프라인을 구성하기 위한 SCM-Pipeline 세팅을 합니다.
- GitHub project : http://***<mac IP>***:3000/***<Gogs 계정명>***/dev-app ←. git 은 제거
- Use Gogs secret : qwe123
- Build Triggers : Build when a change is pushed to Gogs 체크
- Pipeline script from SCM
-SCM : Git
- Repo URL(http://***<mac IP>***:3000/***<Gogs 계정명>***/dev-app)
- Credentials(devops/***)
- Branch(*/main)
- Script Path : Jenkinsfile
다음으로 SCM으로 가져올 jenkinsfile을 만듭니다.
# Jenkinsfile 빈 파일 작성
docker compose exec jenkins touch /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.254.124: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."
}
}
}
마지막 실습으로는 실행 중인 컨테이너를 중지하고 재배포까지 되도록 파이프라인을 구성합니다.
pipeline {
agent any
environment {
DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
CONTAINER_NAME = 'dev-app' // 컨테이너 이름
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://192.168.254.124: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()
appImage.push("latest") // 빌드 이미지 push 할 때, 2개의 버전(현재 버전, latest 버전)을 업로드
}
}
}
}
stage('Check, Stop and Run Docker Container') {
steps {
script {
// 실행 중인 컨테이너 확인
def isRunning = sh(
script: "docker ps -q -f name=${CONTAINER_NAME}",
returnStdout: true
).trim()
if (isRunning) {
echo "Container '${CONTAINER_NAME}' is already running. Stopping it..."
// 실행 중인 컨테이너 중지
sh "docker stop ${CONTAINER_NAME}"
// 컨테이너 제거
sh "docker rm ${CONTAINER_NAME}"
echo "Container '${CONTAINER_NAME}' stopped and removed."
} else {
echo "Container '${CONTAINER_NAME}' is not running."
}
// 5초 대기
echo "Waiting for 5 seconds before starting the new container..."
sleep(5)
// 신규 컨테이너 실행
echo "Starting a new container '${CONTAINER_NAME}'..."
sh """
//매개 변수를 받도록 구성
docker run -d --name ${CONTAINER_NAME} -p "${DEPLOY_PORT}":80 ${DOCKER_IMAGE}:${DOCKER_TAG}
"""
}
}
}
}
post {
success {
echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
}
failure {
echo "Pipeline failed. Please check the logs."
}
}
}
다음과 같이 슬랙에도 푸시 알림이 잘 오는 것을 확인할 수 있습니다.
하지만 현재까지의 파이프라인 문제점은 컨테이너가 하나 실행되고 있기 때문에 재배포 시 서비스가 중단됩니다. 이를 확인하기 위해 다음과 같이 반복적으로 서비스를 호출해 봅니다.
# server.py 수정
response_string = now.strftime("The time is %-I:%M:%S %p, Study 1week END!!!\n")
# VERSION 수정
# Jenkins 컨테이너 내부에서 git push
jenkins@5c1ba7016f9e:~/dev-app$ git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main
# 호스트 PC에서 반복 접속 실행 : 서비스 중단 시간 체크!
while true; do curl -s --connect-timeout 1 http://127.0.0.1:4000 ; date; sleep 1 ; done
다음과 같이 6초 정도의 서비스 중단이 발생합니다. 이를 해결하기 위해서는 쿠버네티스와 같은 컨테이너 오케스트레이션을 적용하여 다수의 Replica로 구성한 후 업데이트 전략을 가져가는 것이 좋을 것으로 보입니다.
마치며
이번 시간에는 Gogs와 Jenkins를 이용하여 Automation 파이프라인을 구축하는 내용으로 스터디를 진행했습니다. 요즘 저희 회사에서는 Github Action을 이용하여 대부분의 파이프라인을 구성하고 있습니다. 그래서 스터디를 통해 Jenkins 사용법을 복기하는 좋은 경험이 되었습니다. 하지만 Github Action이나 Jenkins나 기본적인 파이프라인 구성 및 흐름은 일맥상통한 것 같습니다. 둘 다 다양한 플러그인도 제공하니까요..
이번 스터디를 통해 Jenkins를 한번 둘러보고 차년도 공공사업에 적용할 파이프라인 구성요소를 정리해야겠습니다. 공공쪽에서는 아무래도 역사가 상태적으로 긴 Jenkins가 많이 사용되는 것 같더라고요 ㅎㅎ
이상으로 이번 스터디 정리를 마치도록 하겠습니다.
긴 글 읽어주셔서 감사합니다 :)
'클라우드 컴퓨팅 & NoSQL > [CICD] CICD 맛보기 스터디' 카테고리의 다른 글
[3주차 - Jenkins CI/ArgoCD + K8S] CI/CD 스터디 (24.12.15) (2) | 2024.12.22 |
---|---|
[2주차 - Github Action] CI/CD 스터디 (24.12.08) (3) | 2024.12.15 |