본문 바로가기

study/CICD

CICD 스터디 1주차 첫번째

올 해 마무리로 '가시다'의 스터디를 참여하게 되었습니다.

바로 cicd스터디 1주차 공유 시작하겠습니다.

 

1. 컨테이너를 활용한 애플리케이션 개발

1.1 python 으로 특정 문자열 출력 → Q. 컨테이너 이미지 Tag 태그 사용 방안

# 코드 작성
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 multistage 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로 자바 컨테이너 빌드 - Docs , Blog1 , Blog2

 

[container] jib 기반의 Java 애플리케이션 컨테이너 이미지

jib 개요 Jib은 Docker 데몬 없이 Java 애플리케이션에 최적화된 Docker 및 OCI 이미지를 빌드하고 Docker Hub와 같은 레지스트리로 저장하는 '플러그인'이다. jib은 gradle, maven과 같은 빌드툴과 함께 사용된

jh-labs.tistory.com

  • Jib는 Dockerfile을 사용하지 않거나 Docker를 설치할 필요 없이 컨테이너를 빌드합니다. → 예를 들어 젠킨스 컨테이너 내부에 도커 설치 필요 X
  • Maven 또는 Gradle용 Jib 플러그인에서 Jib를 사용하거나 Jib 자바 라이브러리를 사용할 수 있습니다.
  • Jib는 애플리케이션을 컨테이너 이미지로 패키징하는 모든 단계를 처리합니다.
  • Dockerfile을 만들거나 Docker를 설치하기 위한 권장사항을 알 필요가 없습니다.
  • 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

 

컨테이너에 접속 후 수정했으나 반영이 안된다 (사진 우측 상단 CloudNet@로 변경)

 

변경된 코드 정보 반영 방안은?

#
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, 코드 내용 동적 반영

파이썬의 경우 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

 

바로 반영가능하다!

 

 

 

2. CI/CD 실습 환경 구성

구성 : 컨테이터 2대(Jenkins, gogs) : 호스트 OS 포트 노출(expose)로 접속 및 사용

# 작업 디렉토리 생성 후 이동
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

 

 

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

 

 

젠킨스의 간단한 소개

  • Jenkins : The open source automation server, support building deploying and automating any project - DockerHub , Github , Docs
    1. 최신 코드 가져오기 : 개발을 위해 중앙 코드 리포지터리에서 로컬 시스템으로 애플리케이션의 최신 코드를 가져옴
    2. 단위 테스트 구현과 실행 : 코드 작성 전 단위 테스트 케이스를 먼저 작성
    3. 코드 개발 : 실패한 테스트 케이스를 성공으로 바꾸면서 코드 개발
    4. 단위 테스트 케이스 재실행 : 단위 테스트 케이스 실행 시 통과(성공!)
    5. 코드 푸시와 병합 : 개발 소스 코드를 중앙 리포지터리로 푸시하고, 코드 병합
    6. 코드 병합 후 컴파일 : 변경 함수 코드가 병함되면 전체 애플리케이션이 컴파일된다
    7. 병합된 코드에서 테스트 실행 : 개별 테스트뿐만 아니라 전체 통합 테스트를 실행하여 문제 없는지 확인
    8. 아티팩트 배포 : 애플리케이션을 빌드하고, 애플리케이션 서버의 프로덕션 환경에 배포
    9. 배포 애플리케이션의 E-E 테스트 실행 : 셀레늄 Selenium과 같은 User Interface 자동화 도구를 통해 애플리케이션의 전체 워크플로가 정상 동작하는지 확인하는 종단간 End-to-End 테스트를 실행.

Jenkins 컨테이너에서 호스트에 도커 데몬 사용 설정 (Docker-out-of-Docker)

# 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

Gogs : Gogs is a painless self-hosted Git service - Github , DockerHub , Docs

 

Introduction - Gogs

 

gogs.io

 

Gogs 컨테이너 초기 설정 : Repo(Private)

# 초기 설정 웹 접속
open "http://127.0.0.1:3000/install" # macOS
웹 브라우저에서 http://127.0.0.1:3000/install 접속 # Windows

 

설치 완료

 

→ Gogs 설치하기 클릭 ⇒ 관리자 계정으로 로그인 후 접속

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

 

 

로그인 후 → Your Settings → Applications : Generate New Token 클릭 - Token Name(devops) ⇒ Generate Token 클릭 : 메모해두기!

 

New Repository

 

 

Gogs 실습을 위한 저장소 설정 : jenkins 컨테이너 bash 내부 진입해서 git 작업 진행 ← 호스트에서 직접 git 작업하셔도 됩니다.

#
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완료

'study > CICD' 카테고리의 다른 글

CICD 스터디 3주차 두번째  (0) 2024.12.22
CICD 스터디 3주차 첫번째  (0) 2024.12.22
CICD 스터디 2주차  (1) 2024.12.15
CICD 스터디 1주차 두번째  (0) 2024.12.07