안녕하세요 늑대양 입니다.
이번에 CloudNet@ 에서 진행하는 CI/CD Study 에 참여하게 되어 관련 내용을 정리하여 게시합니다.
CloudNet@ URL: https://gasidaseo.notion.site/CloudNet-Blog-c9dfa44a27ff431dafdd2edacc8a1863
스터디는 3주간 진행되며, 1주차 주제는 Jenkins CI/CD + Docker 입니다.
1주차. Jenkins CI/CD + Docker
1주차에서는 아래의 내용에 대해 학습합니다.
- 실습 환경 구성 → 테스트를 진행하는 macOS 기준으로 작성
- 컨테이너를 활용한 애플리케이션 개발
- Python으로 특정 문자열 출력
- Compiling code in Docker
- Compiling code with a multistage build
- Jib 으로 자바 컨테이너 빌드
- Containerizing an application server
- Using Docker Compose for local testing
- CI/CD 실습 환경 구성
- Docker Hub를 원격 이미지 저장소로 활용하며, Jenkins 와 gogs 컨테이너를 활용
- Jenins 기본 활용
- 작업 소개 (프로젝트, Job, Item)
- 작업 구성 (Job Configuration)
- Jenkins Item
- Gogs repo 자격증명 설정
- 파이프라인구성
- 도커 기반 애플리케이션 CI/CD 구성
- Gogs → Jenkins → Docker Hub / Docker Engine (Run)
Overview
아래의 내용은 스터디 시작 전, 관련 내용에 대해 간단하게 정리한 내용입니다.
+ 스터디 내용 중, 이론적인 부분을 추가하였습니다.
Jenkins 란?
The open source automation server, support building deploying and automating any project
Main URL: https://www.jenkins.io/
- Jenkins는 오픈소스 CI/CD 자동화 서버로, 소프트웨어 개발 주기에서 코드 빌드, 테스트, 배포 작업을 자동화하는 데 사용
- 2004년 Sun Microsystems의 Hudson 프로젝트로 시작되었으며, 2011년 프로젝트가 분리되며 Jenkins라는 이름으로 독립적인 커뮤니티 주도 하에 발전
- 젠킨스는 아파치 톰캣처럼 서블릿 컨테이너 내부에서 실행되는 서버 시스템
- 자바로 작성되었으며, 소프트웨어 개발과 관련된 다양한 도구를 지원
CI/CD의 등장
- 1990년대 후반~2000년대 초반, 소프트웨어 개발에서 Agile이 대두되며 지속적인 통합(Continuous Integration)이 중요해짐
- 이후 DevOps 철학의 등장과 함께 CI에서 한 단계 더 나아간 지속적 배포/전개(Continuous Deployment/Delivery)가 강조
- Jenkins는 이러한 변화를 수용하는 대표적인 도구로 사용
Jenkins의 발전
- 초기에는 CI를 위한 빌드 및 테스트 도구로 주로 사용
- 플러그인 아키텍처와 파이프라인 DSL의 도입으로 현재는 복잡한 CD 워크플로우까지 지원
- Git, Docker, Kubernetes 등 최신 기술과의 강력한 통합 지원
Jenkins의 핵심 기능
- 소프트웨어 개발 프로세스의 다양한 단계를 자동화하는 도구로서 중앙 소스 코드 리포지터리에서 최신 코드 가져오기, 소스 코드 컴파일, 단위 테스트 실행, 산출물을 다양한 유형으로 패키징, 산출물을 여러 종류의 환경으로 배포하기 등의 기능을 제공
- 분산 빌드 시스템: 마스터-에이전트 구조를 통해 빌드 부하 분산 가능
- 플러그인 확장성: 1800개 이상의 플러그인을 통해 다양한 도구 및 기술과 통합
- Build Plugins : Maven, Ant, Gradle …
- VCS Plugins : Git, SVN …
- Languages Plugins : Java, Python, Node.js …
- 파이프라인 DSL(Domain Specific Languange): 코드 기반으로 복잡한 배포 워크플로우를 작성 가능
- 파이프라인 DSL은 Groovy 언어를 기반으로 작성되었으며, 복잡한 로직을 단순하고 읽기 쉬운 방식으로 표현 가능하여 작업의 가독성이 높아지고 협업과 관리가 용이
- 순차적이고 종속적인 단계가 시작부터 끝까지 실행되면 최종적으로 사용자가 실행할 수 있는 빌드가 생성
- 만약 빌드 프로세스를 진행하는 중에 특정 단계에서 실패가 발생하며, 이 단계의 출력 결과를 사용하는 다음 단계는 실행되지 않으며 빌드 프로세스 전체가 실패
CI/CD 워크플로우 예제
- Continuous Integration Server + Continuous Development, Build, Test, Deploy
- 최신 코드 가져오기 : 개발을 위해 중앙 코드 리포지터리에서 로컬 시스템으로 애플리케이션의 최신 코드를 가져옴
- 단위 테스트 구현과 실행 : 코드 작성 전 단위 테스트 케이스를 먼저 작성
- 코드 개발 : 실패한 테스트 케이스를 성공으로 바꾸면서 코드 개발
- 단위 테스트 케이스 재실행 : 단위 테스트 케이스 실행 시 통과(성공!)
- 코드 푸시와 병합 : 개발 소스 코드를 중앙 리포지터리로 푸시하고, 코드 병합
- 코드 병합 후 컴파일 : 변경 함수 코드가 병함되면 전체 애플리케이션이 컴파일된다
- 병합된 코드에서 테스트 실행 : 개별 테스트뿐만 아니라 전체 통합 테스트를 실행하여 문제 없는지 확인
- 아티팩트 배포 : 애플리케이션을 빌드하고, 애플리케이션 서버의 프로덕션 환경에 배포
- 배포 애플리케이션의 E-E 테스트 실행 : 셀레늄 Selenium과 같은 User Interface 자동화 도구를 통해 애플리케이션의 전체 워크플로가 정상 동작하는지 확인하는 종단간 End-to-End 테스트를 실행.
Docker 란?
- Docker는 2013년 Solomon Hykes가 창립한 dotCloud의 내부 프로젝트에서 시작된 컨테이너화 플랫폼으로, 애플리케이션과 필요한 라이브러리를 컨테이너로 패키징하여 실행 환경의 일관성을 제공
- 기존의 가상화 기술(VMware, Hyper-V)보다 경량화된 방식으로 리소스를 효율적으로 활용
컨테이너 기술의 역사
- 2000년대 초반: 리눅스 커널의 chroot 및 cgroups 기술이 컨테이너화의 초석을 마련
- 2008년: LXC(Linux Containers)의 등장으로 컨테이너화 기술이 현실화
- 2013년: Docker의 등장으로 컨테이너가 개발자와 운영자 모두에게 사용 가능한 표준으로 자리 잡음
Docker의 주요 구성 요소
- 이미지(Image): 애플리케이션 실행 환경을 캡슐화한 불변의 템플릿
- 컨테이너(Container): 이미지를 실행한 상태로, 독립된 프로세스 및 자원 격리 제공
- Docker Hub: 공유 가능한 이미지 저장소로, 커뮤니티와 표준 이미지를 제공
- Docker Compose: 멀티 컨테이너 애플리케이션을 정의하고 실행하기 위한 도구
Docker의 혁신과 장점
- 애플리케이션과 의존성의 패키징을 표준화하여 "Works on My Machine" 문제를 해결
- 지속적 통합/배포 과정에서 일관된 빌드 및 배포 환경 제공
- 클라우드 네이티브와 마이크로서비스 아키텍처의 기반 기술로 발전
Jenkins와 Docker의 결합
- Jenkins와 Docker의 시너지
- Docker 컨테이너를 사용하여 Jenkins 빌드 에이전트를 격리하고 확장성 강화
- Jenkins 파이프라인에서 Docker를 사용해 컨테이너 기반 빌드 및 배포 작업을 표준화
- 운영 환경과 동일한 컨테이너에서 테스트를 수행하여 배포 신뢰성 향상
사용 사례
- CI/CD 파이프라인: Jenkins는 Docker 이미지를 빌드, 저장, 배포하며 배포 환경의 통일성을 보장
- 분산 빌드: Docker 에이전트를 통해 Jenkins의 빌드 성능을 확장
0. 실습 환경 구성
- Docker Desktop
- 설치:
- brew install --cask docker (or)
- URL: https://www.docker.com/products/docker-desktop/
- 설치:
- Visual Studio Code
- URL:https://code.visualstudio.com/
- Extension 으로 Docker 활용
1. 컨테이너를 활용한 애플리케이션 개발
1.1 Python 으로 특정 문자열 출력
Dockerfile Reference URL: https://docs.docker.com/reference/dockerfile/
# 코드 작성
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
1.2 Compiling code in Docker
# 코드 작성
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
1.3 Compiling code with a multi-stage build
최종 빌드 전에 컴파일 등을 실행하는 임시 컨테이너를 사용하는 방식
# 코드 작성
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
1.4 Jib 로 자바 컨테이너 빌드
Jib는 Dockerfile을 사용하지 않거나 Docker를 설치할 필요 없이 컨테이너를 빌드합니다.
Jib는 애플리케이션을 컨테이너 이미지로 패키징하는 모든 단계를 처리합니다.
Dockerfile을 만들거나 Docker를 설치하기 위한 권장사항을 알 필요가 없습니다.
Docs & Image URL: https://cloud.google.com/java/getting-started/jib?hl=ko
- Jib는 애플리케이션을 종속 항목, 리소스, 클래스 등 별개의 레이어로 구성하고 Docker 이미지 레이어 캐싱을 활용하여 변경사항만 다시 빌드함으로써 빌드를 빠르게 유지합니다.
- Jib 레이어 구성과 작은 기본 이미지는 전체 이미지 크기를 작게 유지하여 성능과 휴대성을 향상시킵니다.
1.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 8080:80 --name=timeserver timeserver
# 컨테이너 접속 및 로그 확인
curl http://localhost:8080
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
# 변경된 코드 정보 반영 방법
#
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
#
# 컨테이너 이미지 빌드 : 용량 비교 해보자!
docker build . -t timeserver:2 && docker tag timeserver:2 timeserver:latest
docker image ls -f reference=timeserver
# 컨테이너 실행
docker run -d -p 8080:80 --name=timeserver timeserver
# 컨테이너 접속 및 로그 확인
curl http://localhost:8080
# 컨테이너 삭제
docker rm -f timeserver
1.6 Using Docker Compose for local testing
- 개발 편리 방안
- Mapping folders locally
- 코드 내용 동적 반영
- 호스트 현재 디렉토리를 컨테이너의 특정 경로로 볼륨 마운트 진행
- 호스트 파일 업데이트 시 변경 사항 재구성 없이 컨테이너 내부에서 즉시 읽을 수 있음
- Disk 에 Python 코드가 변경이 되더라도 reload 되지 않으므로 테스트 코드를 업데이트하여 코드 변경 시 reload 되게 설정 진행
- 파이썬의 경우 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
# 코드 내용 변경 후 확인:
# 호스트에서 server.py 코드 수정(볼륨 공유 상태) 후 curl 접속 시 자동 반영 확인
# 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
[참고사항] 도커 이미지 레이어 분석 도구 - dive
Github URL: https://github.com/wagoodman/dive
# dive 설치
brew install dive
#
dive -h
# 'nginx:alpine-slim' 이미지 레이어 분석 실행
dive nginx:alpine-slim
Image Source: docker://nginx:alpine-slim
Fetching image... (this can take a while for large images)
Analyzing image...
Building cache...
# 1.6 실습 이미지 레이어 확인
dive 16-frontend
Image Source: docker://16-frontend
Fetching image... (this can take a while for large images)
Analyzing image...
Building cache..
2. CI/CD 실습 환경 구성
2.1 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:
- "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
# 기본 정보 확인
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
2.2 Jenkins 관련 초기 설정 진행
# Jenkins 초기 암호 확인
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
09a21116f3ce4f27a0ede79372febfb1
# Jenkins 웹 접속 주소 확인 : 계정 / 암호 입력 >> admin / qwe123
open "http://127.0.0.1:8080" # macOS
웹 브라우저에서 http://127.0.0.1:8080 접속 # Windows
# (참고) 로그 확인 : 플러그인 설치 과정 확인
docker compose logs jenkins -f
# (참고) 실습 완료 후 해당 컨테이너 중지 상태로 둘 경우 → 재부팅 및 이후에 다시 실습을 위해 컨테이너 시작 시
# 실습 완료 후 해당 컨테이너 중지 상태로 둘 경우
docker compose stop
docker compose ps
docker compose ps -a
# mac 재부팅 및 이후에 다시 실습을 위해 컨테이너 시작 시
docker compose start
docker compose ps
# (참고) 특정 컨테이너만 삭제 후 다시 초기화 상태로 기동 시
# jenkins : 볼륨까지 삭제
docker compose down jenkins -v
docker compose up jenkins -d
# gogs : 볼륨까지 삭제
docker compose down gogs -v
docker compose up gogs -d
2.3 Jenkins 컨테이너에서 호스트에 도커 데몬 사용 설정 (Docker-out-of-Docker)
# Jenkins 컨테이너 내부에 도커 실행 파일 설치
docker compose exec --privileged -u root jenkins bash
-----------------------------------------------------
# root user 로 로그인 확인 후, 아래의 명령어 입력
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
chmod g+rw /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
2.4 Gogs
Gogs is a painless self-hosted Git service
This project aims to build a simple, stable and extensible self-hosted Git service that can be setup in the most painless way.
With Go, this can be done with an independent binary distribution across ALL platforms that Go supports, including Linux, macOS, Windows and ARM.
Gogs Github URL: https://github.com/gogs/gogs
Gogs Docs URL: https://gogs.io/docs
2.5 Gogs 컨테이너 초기 설정
# 초기 설정 웹 접속
open "http://127.0.0.1:3000/install" # macOS
웹 브라우저에서 http://127.0.0.1:3000/install 접속 # Windows
#
docker compose exec gogs ls -l /data
docker compose exec gogs ls -l /data/gogs
docker compose exec gogs ls -l /data/gogs/conf
docker compose exec gogs cat /data/gogs/conf/app.ini
2.6 Gogs 실습을 위한 저장소 설정
#
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://172.29.98.14: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
...
2.7 Docker Hub quickstart
- Step 1 : Sign up for a free Docker account
- A Docker ID grants you access to Docker Hub repositories and lets you explore available images from the community and verified publishers. You also need a Docker ID to share images on Docker Hub.
- Explore Docker's core subscriptions to see what else Docker can offer you. → Personal(무료, 6시간 당 200개 Image pulls)
- Step 2 : Create your first repository
- On the Repositories page, select Create repository.
- Name it <your-username>/dev-test-app
- Set the visibility to Private.
- Select Create.
- Step 3 : Build and push a container image to Docker Hub from your computer
1. Start by creating a Dockerfile to specify your application as shown below:
# syntax=docker/dockerfile:1
FROM busybox
CMD echo "Hello world! This is my first Docker image."
2. Run docker build -t <your_username>/my-private-repo . to build your Docker image.
3. Run docker run <your_username>/my-private-repo to test your Docker image locally.
4. Run docker push <your_username>/my-private-repo to push your Docker image to Docker Hub. You should see output similar to:
# Dockerfile 파일 작성
cat > Dockerfile <<EOF
FROM busybox
CMD echo "Hello world! This is my first Docker image."
EOF
cat Dockerfile
# 빌드
DOCKERID=<자신의 도커 계정명>
DOCKERREPO=<자신의 도커 저장소>
DOCKERID=gasida
DOCKERREPO=dev-test-app
docker build -t $DOCKERID/$DOCKERREPO .
docker images | grep $DOCKERID/$DOCKERREPO
# 실행 확인
docker run $DOCKERID/$DOCKERREPO
# 로그인 확인
docker login
# 푸시
docker push $DOCKERID/$DOCKERREPO
3. Jenkins 기본 사용
3.1 Jenkins 작업 소개
- 작업을 수행하는 시점 Trigger
- 작업 수행 태스크 task가 언제 시작될지를 지시
- 작업을 구성하는 단계별 태스크 Built step
- 특정 목표를 수행하기 위한 태스크를 단계별 step로 구성 가능
- 이것을 젠킨스에서는 빌드 스텝 build step이라고 부름
- 태스크가 완료 후 수행할 명령 Post-build action
- 예를 들어 작업의 결과(성공 or 실패)를 사용자에게 알려주는 후속 동작이나, 자바 코드를 컴파일한 후 생성된 클래스 파일을 특정 위치로 복사 등
- (참고) 젠킨스의 빌드 : 젠킨스 작업의 특정 실행 버전
- 사용자는 젠킨스 작업을 여러번 실행할 수 있는데, 실행될 때마다 고유 빌드 번호가 부여
- 작업 실행 중에 생성된 아티팩트, 콘솔 로드 등 특정 실행 버전과 관련된 모든 세부 정보가 해당 빌드 번호로 저장
3.2 Jenkins Item 생성 - 1
3.3 Gogs repo 자격증명 설정
3.3 Jenkins Item 생성 - 2
- First item 과 마찬가지로 Freestyle 생성 진행
3.4 Jenkins 파이프라인
3.5 Jenkins Item 생성
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."
}
}
}
4. 도커 기반 애플리케이션 CI/CD 구성
4.1 목표 Architecture
4.2 Gogs Webhooks 설정
# gogs 에 app.ini 파일 수정 후 컨테이너 재기동
[security]
INSTALL_LOCK = true
SECRET_KEY = j2xaUPQcbAEwpIu
LOCAL_NETWORK_ALLOWLIST = 192.168.254.124 # 각자 자신의 PC IP
- Payload URL : http://192.168.254.124:8080/gogs-webhook/?job=SCM-Pipeline/
- Content Type : application/json
- Secret : qwe123
- When should this webhook be triggered? : Just the push event
- Active : Check
4.3 Jenkins Item 생성 (Pipeline)
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."
}
}
}
# 작성된 파일 push : jenkins 컨테이너 bash 내부 진입해서 작업 진행
#
docker compose exec jenkins bash
---------------------------------
cd /var/jenkins_home/dev-app/
git add . && git commit -m "Jenkinsfile add & VERSION 0.0.2 Changed" && git push -u origin main
# server.py 와 VERSION 변경 후 Jenkins 트리거 작업 한번 더 확인하기
# server.py
# 버전 0.0.3 수정 : 아래 입력 잘 안될 경우 VSCODE(Docker플러그인)에서 직접 수정
docker compose exec jenkins sh -c 'echo "0.0.3" > /var/jenkins_home/dev-app/VERSION'
#
docker compose exec jenkins bash
---------------------------------
cd /var/jenkins_home/dev-app/
git add . && git commit -m "VERSION $(cat VERSION) Changed" && git push -u origin main
Final
# Jenkinsfile 수정 후 git push → tcp (기본값:4000) 는 파라미터로 입력 받게 설정하게 해볼것
pipeline {
agent any
environment {
DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
CONTAINER_NAME = 'dev-app' // 컨테이너 이름
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://<자신의 Private IP>/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 4000: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."
}
}
}
# 생성된 컨테이너 접속 확인
docker image
docker ps
curl http://127.0.0.1:4000
# server.py 수정 후 VERSION 수정 후 push 후 생성된 컨테이너 접속 후 반영 확인
# 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
이것으로 CloudNet@ CI/CD Study - 1주차 - Jenkins CI/CD + Docker 내용을 마무리 합니다.
블로그 내용을 작성하는 중, 인터넷 단절 등 수많은 시련이 있었네요..😭
긴 글 읽어주셔서 감사합니다
다음 시간에 뵙겠습니다
'k8s > CloudNet@' 카테고리의 다른 글
CI/CD Study - 2주차 - GitHub Actions CI/CD (0) | 2024.12.15 |
---|---|
[CloudNet@] AWS EKS Workshop Study - 5주차. (0) | 2023.05.28 |
[CloudNet@] AWS EKS Workshop Study - 4주차. (0) | 2023.05.21 |
[CloudNet@] AWS EKS Workshop Study - 3주차. (0) | 2023.05.14 |
[CloudNet@] AWS EKS Workshop Study - 2주차. (0) | 2023.05.07 |