Skip to content
art of loving
Go back

도커 이미지 빌드부터 쿠버네티스 배포까지

Updated:
Edit page

1: 실습 목표와 순서 (이미지 → compose → yaml)

간단한 실습을 진행하고자 했다. 일단 프로젝트 구조는 다음과 같았다.

.
└── work
    └── docker
        ├── app
        │   ├── Dockerfile
        │   ├── app.py
        │   ├── requirements.txt
        │   └── templates
        │       ├── dashboard.html
        │       ├── login.html
        │       └── register.html
        └── db
            ├── Dockerfile
            └── init.sql

나의 목표는

  1. docker-compose 파일 구성
  2. 빌드한 이미지를 구축해둔 kubernetes를 이용해 배포하기

를 진행하면서 이론으로 배운 사항들을 적용하는 실습해보기였다.

2: Dockerfile부터 다시 읽기

먼저 appdb 폴더 안에 각각 위치한 Dockerfile 소스를 파악해보고자 했다.

2.1: 베이스 이미지 / 작업 디렉터리 / 실행 명령, 최소 세 가지

이미지를 생성하는 Dockerfile 에는 필수로 들어가야 하는 내용들이 있다. 예를 들면, 이 앱이 무슨 언어 또는 런타임에서 도는지, 뭘 설치해야 하는지 등등이다. 이 프로젝트의 app 폴더에 위치한 Dockerfile을 보면

# 베이스 이미지로 Python 3.8을 사용
FROM python:3.8

# 작업 디렉터리를 설정
WORKDIR /app

# 시스템 업데이트 및 필수 패키지 설치
RUN apt-get update && \
    apt-get install -y \
    build-essential \
    default-libmysqlclient-dev && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 애플리케이션의 종속성 파일을 복사
COPY requirements.txt requirements.txt

# 종속성 설치
RUN pip install --no-cache-dir -r requirements.txt

# 애플리케이션의 소스 코드 복사
COPY . .

# Flask 애플리케이션을 환경 변수로 설정
ENV FLASK_APP=app.py

# 컨테이너 실행할 때 Flask 애플리케이션을 시작
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]

\

2.2: requirements.txt 먼저 COPY하는 이유 (레이어 캐시)

도커의 대표적인 작동 습관 중 하나인데, Dockerfile 각 줄마다 레이어를 쌓으면서 캐시가 걸려있어서 위에서부터 어느 줄까지는 그대로 사용하고 바꾼 줄부터 새로 실행함. 만약에 COPY . . 를 먼저 실행하면 코드 한줄만 고쳐도 requirements.txt는 그대로인데도 그 COPY레이어가 통째로 바뀐걸로 인식 되고 pip install까지 다시 실행됨. 패키지 수십 개가 매번 재설치 되는 셈이라서, requirement.txt 만 먼저 복사해서 설치해두면 코드만 고쳤을 땐 그 위 레이어가 그대로 캐시됨. 그러면 pip install은 스킵되고 코드 복사만 다시 하게 됨. 빌드 속도가 꽤 크기 때문에!

2.3: mysql 공식 이미지의 docker-entrypoint-initdb.d

db쪽 dockerfile은 아래와 같이 구성되어있는데,

# Dockerfile
FROM mysql:5.7

# 환경 변수 설정
ENV MYSQL_ROOT_PASSWORD=
ENV MYSQL_DATABASE=
ENV MYSQL_USER=
ENV MYSQL_PASSWORD=

# 초기화 스크립트 복사
COPY init.sql /docker-entrypoint-initdb.d/

mysql:5.7 베이스에 ENV로 root 비밀번호, 데이터베이스 이름, 유저, 비밀번호를 박아두고, init.sql/docker-entrypoint-initdb.d/라는 특정 경로에 복사하고 있음

mysql 공식 이미지의 entrypoint 스크립트는 컨테이너가 처음 켜질 때(데이터 디렉터리가 비어있을 때만) 두 가지 일을 순서대로 함. 먼저 ENV로 받은 MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER 값으로 초기 계정과 데이터베이스를 만들고, 그다음에 /docker-entrypoint-initdb.d/ 폴더 안에 있는 .sql 파일들을 자동으로 순서대로 실행하는 거임. init.sql은 계정 만든 다음 단계, 테이블이나 초기 데이터를 넣는 역할이라고 보면 됨. 데이터 디렉터리가 비어있을 때 딱 한 번만 실행되는 거라서 나중에 컨테이너를 지웠다 다시 만들어도 볼륨에 데이터가 남아있으면 init.sql이 다시 안 돌아감 .

3: 로컬에서 먼저 검증하기: docker compose

\

3.1: 서비스 이름이 곧 내부 DNS라는 것 (app.py가 하드코딩한 db-svc)

compose를 짜려면 먼저 app.py 안에서 MySQL(db)에 접속하는 코드 먼저 확인 해야함. 호스트 이름, 포트, 유저, 비밀번호, 데이터베이스 이름을 어디서 가져오는지 먼저 보는 거임. 하드코딩인지 환경변수로 받아오게 되어있는지부터 확인.

# MySQL 설정
app.config['MYSQL_HOST'] = 'db-svc'
app.config['MYSQL_USER'] =  
app.config['MYSQL_PASSWORD'] =  
app.config['MYSQL_DB'] =  

이렇게 되어있었음. 그러니까 app.py 설정에 따라서 app 단에서는 무조건 db-svc 라고 하드코딩된 이름을 찾아가게 되어있음.

3.2: 포트 매핑 이해하기 (컨테이너 내부 포트 vs 호스트 포트)

4: 쿠버네티스 언어로 번역하기

\

4.1: Pod 대신 Deployment를 쓰는 이유

pod 자체를 직접 만들 수도 있긴 한데 이 pod가 어떤 이유로 (메모리 초과나 노드 문제 등) 죽어버리면 이걸 관리해줄 수 있는 도구가 없음. 그래서 replicas라는 개수를 설정하는 오브젝트인 deployment를 사용할 거임. 여기서는 간단한 구조라 app 레플리카는 2개로 설정함. db 레플리카는 당연히 데이터니까 1개!

apiVersion: apps/v1
kind: Deployment
metadata:
  name: (이름)
spec:
  replicas: (개수)
  selector:
    matchLabels:
      app: (라벨값)
  template:
    metadata:
      labels:
        app: (라벨값,  selector랑 반드시 똑같아야 함)
    spec:
      containers:
        - name: (컨테이너 이름)
          image: (이미지 이름:태그)
          ports:
            - containerPort: (포트)

\

4.2: Service selector와 Pod label는 틀리지 않게 조심

containerPort는 이 컨테이너 안에서 앱이 실제로 어느 포트를 쓰고 있는지 알려주는 값. 이 값을 바꾼다고 해서 앱이 실제로 그 포트로 옮겨가는 게 아님. 앱은 여전히 Dockerfile의 CMD가 정한 대로 5000번에서만 듣고 있음. 여기 적는 숫자는 진짜 사실을 그대로 적어주는 자리지 아까 compose에서 했던 호스트포트:컨테이너포트처럼 다른 숫자로 바꿔치기(리매핑)하는 자리가 아님. 여기 6000 같은 엉뚱한 숫자를 적으면 Service의 targetPort가 실제 5000이 아닌 엉뚱한 곳을 가리키게 돼서 연결이 끊김.

\

4.3: ClusterIP / NodePort / LoadBalancer 용도

\

5: apply 이후 상태 읽기

위 명령어로 apply 이후 확인하기

5.1: kubectl get pods / svc / endpoints 순서

kubectl get pods

여기서 app이랑 db 파드 둘 다 Running으로 뜨는지 봐야함. 혹시 CrashLoopBackOffPending으로 떠 있으면 다시 확인.

kubectl get svc

여기서 app-svc의 EXTERNAL-IP 칸에 MetalLB가 준 실제 IP가 찍히는지 확인

kubectl get endpoints

app-svc랑 db-svc 둘 다 IP:포트 값이 채워져 있어야 함. 만약 여기가 텅 비어 있으면, 파드는 떠 있는데 Service가 못 찾고 있는 거라서 라벨이나 셀렉터를 다시 봐야 함

→ 실시간으로 지켜보면 천천히 만들어짐

kubectl get pods -w

6: 확인

\

6.1: 새로고침마다 바뀌는 Server Name, Service의 로드밸런싱

\

6.2: Deployment의 자기 치유

pod를 의도적으로 kill 해도 새로운 파드가 생성됨. 그동안 남은 하나의 파드가 실행되면서 새로고침도 정상 실행(서버 네임은 남은 하나 파드로 고정)

7. 회고

다 알고 있다고 생각한 내용이었는데 소스코드에서 이미지 빌드부터 배포까지 쭉 완성하려니 헷갈리는 부분들이 있어서 좀 당황했다. 구성하는 연습을 많이 해봐야겠구나…


Edit page
Share this post on:


Next Post
컨테이너를 이루는 커널 기술 3가지