기본적으로 CI/CD를 왜 사용하는지는 알아야 한다

일반적으로 개발자가 코드 수정사항이 있어 배포를 할 때, 인플레이스 배포를 많이 할 것이다.

war 파일같은 경우에는 scp처럼 파일 전송 프로토콜로 서버에 배포하고 war 파일을 푸는 식으로 코드 업데이트가 이루어지기도 하고

docker 같은 경우에는 파일 수정 후 이미지를 만들고 다시 컨테이너로 올리는 작업을 수행하게 된다.

이번 포스팅에는 수동 배포하는 일련의 과정을 먼저 실습하면서 배포 자동화의 중요성을 깨달아 볼 것이다!

1.1 python 으로 특정 문자열 출력

간단하게 “Hello Docker”를 출력하는 컨테이너 하나를 기동시켜 본다.

image

이 컨테이너 이미지의 현재 태그는 lastest로 되어 있다

image 1

python 코드를 수정한 후, 버전 1로 hello 이미지를 생성하였다.

echo "print ('Hello CloudNet@')" > hello.py
docker build . -t hello:**1**

그리고 버전 1을 lastest 태그로 변경해 준다.

image 2

그럼 버전 1과 lastest는 동일한 최신 이미지로 변경된다

image 3

예전에 컨테이너 마이그레이션 프로젝트를 진행할 때도 항상 최신이미지는 lastest 태그로 관리하였다. 그리고 POD가 불러오는 이미지는 무조건 lastest로 pull 할 수 있게 설정해 두었다. 그래서 별 다른 설정없이 항상 최신 버전으로 배포가 가능했다. 다만 롤백이 필요할 때만 이전 버전으로 명시하였다.

1.2 Compiling code in Docker

이번엔 자바 애플리케이션으로 실행해 보겠다!

간단하게 프린트구문을 출력하고 컨테이너 내부에서 컴파일한 후 실행된다.

image 4

컨테이너 내부에서 컴파일을 실행하기 때문에 java, class 파일과 javac 컴파일러까지 포함되어 있다. 하지만 애플리케이션을 실행하는 데에는 java파일과 컴파일러까지는 필요하지 않는다.

image 5

그래서 이미지를 빌드하는 과정에서만 컴파일을 하고 이미지에 포함되지 않게 하려면 멀티스테이지 빌드를 활용할 수 있다.

1.3 Compiling code with a multistage build

image 6

멀티스테이지 빌드를 사용하게 되면 빌드 과정에서만 컴파일을 실행하고 최종 이미지에는 결국엔 최종 class 파일만 남을 수 있기에 불필요한 컴파일러를 컨테이너 내부에 설치하지 않아도 된다.

image 7

불필요한 것들을 포함하지 않으니 이미지 용량도 반 넘게 줄었다!

image 8

실행된 컨테이너에는 class 파일만 남아 있는 것도 확인할 수 있었다!

1.4 Jib로 자바 컨테이너 빌드

image 9

image 10

Docker 빌드는 Dockerfile을 작성하고 빌드해서 이미지 만드는 과정이 필요하지만

Jib 빌드는 Dockerfile 작성이나 docker 도구를 설치하지 않아도 프로젝트에서 빌드와 동시에 이미지를 만들고 저장소에 푸시까지 해준다.

[참고 링크 ) Jib로 자바 컨테이너 빌드     Google Cloud](https://cloud.google.com/java/getting-started/jib?hl=ko)

1.5 Containerizing an application server

타임서버를 하나 생성해서 컨테이너로 먼저 띄워 보자

*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()*

image 11

1.46기가나 되네..!

image 12

컨테이너를 8080포트로 실행시켜 보면 시간을 잘출력하고 있다

docker exec -it timeserver /bin/bash로 컨테이너 내부에서 직접 server.py 파일을 수정해 보았지만 변경되지 않았다

image 13

UTC를 뺀건데..

image 14

원래대로 출력한다. 반영되지 않았다!

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

image 15

컨테이너를 삭제하고 코드 변경한 이후에 재배포를 해야 변경된 코드가 반영되었다.

1.6 Using Docker Compose for local testing

컨테이너에서 코드 변경이 이루어 지면 즉시 반영이 안되기에 컨테이너를 재기동해야 한다.

하지만 파이썬의 경우 reloading 라이브러리를 사용하여 컨테이너 재기동 없이도 즉각적으로 코드 반영이 이루어 질 수 있다.

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()

image 16

도커파일에서는 평소 그대로 애플리케이션 빌드하고

docker compose 파일에서 현재 호스트 디렉토리를 컨테이너 /app 디렉토리로 마운트하였다

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

그래서 호스트 볼륨에 있는 파일이 변경되면 파이썬 reloading 라이브러리로 코드 변경 시 동적 반영이 될 수 있다. 실제로 내용을 변경해 보았더니 바로 반영되었다.

image 17

마무리

실습을 진행해 보면서 파이썬 라이브러리 reloading 기능이 아니라면 서버를 재기동해야 한다는 점에서 서비스의 순단이 있어 코드 수시로 재반영하기가 어려울 것 같다는 생각이 들었다.

사실 지금 회사에서도 단일서버에 인플레이스 배포를 하는 서비스가 다수라 배포기법을 바꿔서 배포자동화를 세팅하면 좋을 거 같다는 생각을 했다.

이번 실습으로 인해 배포자동화의 용이함을 더욱 느낄 수 있었다.

Categories:

Updated:

Leave a comment