[docker/kubernetes] docker-compose를 kubernetes로 변환하기(without kompose)

Myoung Jun Ko·2024년 10월 17일
0

전체 프로젝트는 Retro-pong-with-k8s-and-aws에서 확인할 수 있습니다.

이전 글에서 kompose 를 이용해 docker-compose를 kubernetes로 변환했다. 하지만 공부하는 입장에서 너무 간단하게 넘어간 점과 저번에 활용한 프로젝트가 42 oauth로만 접근이 가능하다는 점에서 변환할 (같이 공부한 다른 팀의) 프로젝트로 변경하고 직접 kubernetes로 변환해보기로 했다.

변환을 시작하기 전에

이번 프로젝트는 내가 만든 프로젝트가 아니기에 프론트, 백의 소스코드를 직접 건들기가 어렵다. 그렇기에 현재 구조를 그대로 이어가기 위해 한 부분만 제외하고 노력을 많이 했다. 변경한 파트는 저번 글과 같은 이유로 nginx와 frontend를 합친 부분이다. 이번 프로젝트 역시 npm build 후, dist 폴더만 만들고 종료되는데 이 구조를 수정했다.

또한 배포되는 모든 deployment , service , persistent volume claimapp 이란 namespace에 배포한다. persistent volume 은 namespace에 국한되지 않는 전역 속성이기에 따로 namespace를 지정하지 않았다. replicas 설정은 로컬에서도 가볍게 돌아가도록 1로 설정했다.

docker-compose에서 kubernetes로

위와 같이 변경했기 때문에 변환할 부분은 db , frontend(+nginx) , backend 3개이다. 파트 별로 변환과정을 작성하고 볼륨은 따로 빼서 작성할 예정이다.

db

# docker-compose.yml 중 db 부분
db:
  build:
    context: ./postgres
  container_name: db
  env_file:
    - ./postgres/.env
  volumes:
    - postgres-data:/var/lib/postgresql/data
  networks:
    - network

우선 기존 db 부분이다. postgres 이미지를 그대로 가져왔기에 크게 환경변수와 볼륨만 신경쓰면 된다. 그래서 간단하게 작성했다.

먼저 configMap.yaml 부분이다.

# configMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: db-env
  name: db-env
  namespace: app
data:  # 환경 변수의 키와 값
  POSTGRES_DB: "database"
  POSTGRES_USER: "user"
  POSTGRES_PASSWORD: "password"

kubernetes는 환경 변수를 deployment 에서 적용할 수 있다. 이때 .env 파일처럼 키-값의 데이터를 configMap 형식으로 저장할 수 있다. 그래서 우선 아래와 같이 configMap.yaml 파일을 만들었다.

그 후, deployment.yaml 는 아래와 같다.

# deployment.yaml 중 db 부분
apiVersion: apps/v1
kind: Deployment
metadata:
  name: db
  namespace: app  # 이름공간을 설정하면 배포할 때 이름공간을 명시하지 않아도 자동으로 배포
spec:
  replicas: 1
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      volumes: # pod 레벨
        - name: db-data
          persistentVolumeClaim:
            claimName: db-data
      containers:
      - env:  # 환경변수 설정
        - name: POSTGRES_DB
          valueFrom:
            configMapKeyRef:
              key: POSTGRES_DB
              name: db-env
        - name: POSTGRES_USER
          valueFrom:
            configMapKeyRef:
              key: POSTGRES_USER
              name: db-env
        - name: POSTGRES_PASSWORD
          valueFrom:
            configMapKeyRef:
              key: POSTGRES_PASSWORD
              name: db-env
        image: postgres
        name: db
        volumeMounts: # container 레벨
          - mountPath: /var/lib/postgresql/data
            name: db-data
      restartPolicy: Always

deployment 이름과 namespace를 각각 db와 app으로 설정하고 이 값을 식별하기 위해 app: db 레이블을 설정한다. 그리고 먼저 작성한 configMap 의 값을 사용하여 환경 변수를 설정한다.

service.yaml 은 다른 컨테이너와 통신할 수 있도록 postgres의 기본 port만 설정했다.

# service.yaml 중 db 부분
apiVersion: v1
kind: Service
metadata: # 서비스 자체 이름
  name: db
  namespace: app
spec:
  selector: # 연결될 pod 식별
    app: db
  ports:
    - port: 5432 # postgres default port

backend

docker-compose.yml 의 backend 부분은 아래와 같다.

# docker-compose.yml 중 backend 부분
backend:
 image: backend:transcendence
 container_name: backend
 build: ./backend
 ports:
   - '8000:8000'
 networks:
   - network
 volumes:
   - backend-media:/app/media

backend 역시 환경변수가 있다. 하지만 기존 프로젝트에서 환경변수를 docker build 시, 사용하기에 docker 이미지를 사용하는 이 프로젝트에서는 환경변수를 지정해도 의미가 없었다. 그렇기에 환경변수 중 url 관련 부분만 변경하여 docker image를 다시 build하고 이를 docker hub에 올려서 사용했다.

먼저 deployment.yaml 은 아래와 같다.

# deployment.yaml 중 backend 부분
apiVersion: apps/v1
kind: Deployment
metadata:
 name: backend
 namespace: app
spec:
 selector:
   matchLabels:
     app: backend
 template:
   metadata:
     labels:
       app: backend
   spec:
     volumes:
       - name: back-data
         persistentVolumeClaim:
           claimName: back-data
     containers:
     - image: kmj951015/retro-pong-backend:1.0.1
       name: backend
       volumeMounts:
         - mountPath: /app/media
           name: back-data
       ports:
       - containerPort: 8000  # pprt 명시
         protocol: TCP  # 통신 방법 명시(기본이 TCP라 생략 가능)
     restartPolicy: Always

살펴보면 전반적으로 db에서 사용한 설정들을 사용한 것을 볼 수 있다. 차이는 container 부분에 ports가 추가된 것뿐이다. 사실 db는 기본 포트가 정해져 있기에 설정을 생략해도 된다. 하지만 명시해주는 것이 함께 개발하는 개발자들이 쉽게 이해할 수 있기에 명시하는 것을 권장한다. 또한 backend 이미지의 경우 커스텀 이미지이기에 포트를 명시하는 것이 개발의 혼란을 막을 수 있다.

service.yaml 역시 다른 부분은 ports 부분 밖에 없다. 또한 다른 값도 생략이 가능하지만 커스텀 이미지를 사용하기에 명시적으로 표시했다.

# service.yaml 중 backend 부분
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: app
spec:
  selector:
    app: backend
  ports:
    - name: "8000" # 여러 포트를 구분하기 위해 사용하는 이름(선택 사항)
      port: 8000  # 서비스가 클러스터 내부에 노출하는 포트
      targetPort: 8000  # 실제 파드/컨테이너 포트(생략 시, port와 동일값 설정)

외부 요청에 따른 트래픽 흐름 예시

외부 요청 → Service (port: 8000) → Pod (targetPort: 8000) → Container (containerPort: 8000)

frontend

docker-compose.yml 중 frontend와 nginx 부분은 아래와 같다.

# docker-compose.yml 중 frontend와 nginx 부분
frontend:
  image: frontend:transcendence
  container_name: frontend
  build: ./frontend
  ports:
    - '3000'
  volumes:
    - frontend-data:/app/dist
  networks:
    - network

nginx:
  image: nginx:transcendence
  container_name: nginx
  build: ./nginx
  ports:
    - '443:443'
  networks:
    - network
  volumes:
    - frontend-data:/usr/share/nginx/html
    - backend-media:/app/media

'전환을 시작하기 전에' 에서 이야기했듯이 frontend는 nginx와 합쳐서 전환을 진행했다. 컨테이너를 합친 부분은 생략한다. 또한 frontend 역시 환경변수를 build 과정에서 사용하기에 url 부분만 수정하여 docker image를 다시 build하여 사용하였다.

frontend를 job으로 만들지 않고 nginx와 합친 이유

기존 프로젝트에선 frontend가 실행되고 바로 종료되기에 이를 job 으로 처리하는 시도도 했었다. 하지만 frontend 컨테이너에서 생성되는 dist 폴더를 로컬에 마운트 시키는 등의 방법을 도입해야 했는데 frontend와 nginx 간 강한 의존성과 추후 aws 배포 과정에서 구조를 더욱 복잡하게 만든는 것을 방지하기 위해 두 컨테이너를 합치게 되었다.

deployment.yaml 는 설정하는 필드가 backend와 동일하다.

# deployment.yaml 중 frontend 부분
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: app
spec:
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      volumes:
        - name: back-data
          persistentVolumeClaim:
            claimName: back-data
      containers:
      - image: kmj951015/retro-pong-frontend:1.0.1
        name: frontend
        volumeMounts:
          - mountPath: /app/media
            name: back-data
        ports:
        - containerPort: 443
          protocol: TCP
      restartPolicy: Always

service.yaml 역시 backend와 거의 동일하고 ports의 type만 다르다.

# service.yaml 중 frontend 부분
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: app
spec:
  selector:
    app: frontend
  ports:
    - name: "443"  # port를 식별하는 이름
      port: 443  # 서비스가 클러스터 내부에 노출하는 포트
      targetPort: 443  # 실제 파드/컨테이너의 포트
      nodePort: 30443  # 노드의 포트
  type: NodePort

기본적으로 외부에서 파드 내부 컨테이너에 직접 접근할 수 없기 때문에 port-forward를 해주거나 NodePort로 접근 가능한 포트를 열어주어야 한다. 나는 열리는 포트를 고정하기 위해 ports의 타입을 NodePort로 설정하고 기억하기 쉽도록 30443(NodePort는 30000~32767 범위 내의 값이어야 한다)으로 설정했다.

볼륨

마지막으로 볼륨을 설정할 차례다. 쿠버네티스는 도커와 다르게 볼륨을 설정하기 위해 PersistentVolume(pv)PersistentVolumeClaim(pvc) 그리고 StorageClass(sc) 을 기본적으로 사용한다. 간단하게 pv는 클러스터의 물리적 스토리지, sc는 pv를 동적으로 프로비저닝하기 위한 템플릿, pvc는 스토리지 요구사항을 정의한 것이다. 이 개념이 쉽게 와닿지 않기에 공식 문서와 여러 다른 글의 개념을 참고하면 좋다.

아래는 docker-compose.yml 에서 볼륨과 관련한 설정만 적은 것이다.

services:
  db:
    volumes:
      - postgres-data:/var/lib/postgresql/data

  backend:
    volumes:
      - backend-media:/app/media

  frontend:
    volumes:
      - frontend-data:/app/dist

  nginx:
    volumes:
      - frontend-data:/usr/share/nginx/html
      - backend-media:/app/media

volumes:
  frontend-data:
  backend-media:
  postgres-data:

postgres-data는 db만 사용하고 frontend-data는 frontend와 nginx가, backend-data는 backend와 nginx가 사용하는 것을 볼 수 있다. 이때 앞서 말했듯 frontend의 볼륨은 npm build의 결과물인 dist 폴더를 nginx에 옮기는 것이 주된 목적이기에 frontend와 nginx를 합치면서 볼륨의 필요성이 사라졌다. 따라서 쿠버네티스에서는 backend-data와 postgres-data만 정의한다.

먼저 persistentVolume.yaml 중 pv와 sc 정의 부분을 먼저 살펴보면 다음과 같다.

# persistentVolume.yaml 중 pv와 sc 부분
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: back-shared-storage
provisioner: kubernetes.io/no-provisioner # 동적 프로비저닝 안 함. 주로 로컬 볼륨에서 사용
volumeBindingMode: WaitForFirstConsumer # pvc가 파드에 의해 실행 될 때까지 바인딩 보류

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: back-pv
spec:
  capacity:
    storage: 2Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany  # 여러 파드에서 동시에 읽기, 쓰기 가능
  persistentVolumeReclaimPolicy: Retain  # pvc가 삭제되어도 데이터 보존
  storageClassName: back-shared-storage
  local:
    path: DIR/data/media
  nodeAffinity:  # 특정 노드만 사용 가능
    required:  # 조건을 만족하는 노드에서만 스케줄링 되도록 설정
      nodeSelectorTerms:  # 여러 조건을 Or 그룹으로 묶어놓은 것(즉 하나만 만족하면 스케줄링 됨)
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - docker-desktop

sc는 로컬에서 사용하기 위해 동적 프로비저닝을 안 하도록 만들었다. back-pv는 frontend와 backend가 모두 접근할 수 있어야 하므로 동시에 읽고 쓸 수 있는 ReadWriteMany 옵션을 설정했으며 DIR 경로는 Makefile에서 실행환경에 맞춰서 수정되도록 설정했다.

pvc는 아래와 같이 db-data와 back-data를 설정했다.

# persistentVolume.yaml 중 pvc 부분
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
  namespace: app
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: back-data
  namespace: app
spec:
  accessModes:
    - ReadWriteMany # backend and frontend
  resources:
    requests:
      storage: 1Gi
  storageClassName: back-shared-storage

db-data의 pv를 따로 설정하지 않은 것은 db만 접근하면 되기에 자동으로 생성되는 pv를 사용해도 되기 때문이다. 하지만 실제 배포 시, db 데이터 역시 백업이 필요하기에 그에 맞춰서 설정한 pv가 필요할 수도 있다.

back-data는 생성한 pv가 있기에 같은 sc를 사용하게 해서 back-pv를 사용하도록 만들었다. 또한 accessModes 역시 pv와 마찬가지로 여러 파드에서 접근할 수 있도록 설정했다. storageClassName을 설정하지 않으면 기본 StroageClass를 선택하며 쿠버네티스가 여러 옵션을 확인해서 알맞다고 판단되는 pv에 자동 할당시킨다.

용량은 돌아가는 것을 확인할 수 있을정도로 작게 설정했다. 실제는 동적으로 설정하거나 훨씬 큰 용량을 주는 것이 안전하다.

배포

편하게 명령어를 사용하기 위해 Makefile을 사용했다.

PROJECT_DIR := $(PWD)
NAMESPACE = app
FLAG = -f

.PHONY: local create_namespacess clean_local fclean_local

local: create_namespace
	kubectl apply $(FLAG) k8s/local/configMap.yaml
	sed "s|DIR|$(PROJECT_DIR)|g" k8s/local/persistentVolume.yaml | kubectl apply $(FLAG) -
	kubectl apply $(FLAG) k8s/local/deployment.yaml
	kubectl apply $(FLAG) k8s/local/service.yaml

create_namespace:
	@if ! kubectl get namespace $(NAMESPACE) > /dev/null 2>&1; then \
        	kubectl create namespace $(NAMESPACE); \
    	fi

clean_local:
	kubectl delete $(FLAG) k8s/local/configMap.yaml
	kubectl delete $(FLAG) k8s/local/deployment.yaml
	kubectl delete $(FLAG) k8s/local/service.yaml

fclean_local: clean_local
	kubectl delete $(FLAG) k8s/local/persistentVolume.yaml
	kubectl delete namespace $(NAMESPACE)
    # 프로젝트에서 필요한 초기 데이터
	rm -rf data/media
	cp -r media data/

배포 부분을 보면 DIR을 현재 경로로 바꾸는 것을 볼 수 있다. 또한 create_namespace 명령으로 app namespace를 먼저 생성하고 배포하도록 만들었다.

docker-desktop의 kubernets 설정을 키고 위 사진과 같이 make local 을 실행하면 된다. pod가 모두 Running 상태가 되면 https://127.0.0.1:30443 으로 접속할 수 있다. 이때 인증서 문제로 주의 페이지가 나오는데 무시하고 들어가면 아래와 같은 페이지를 볼 수 있다.

후기

전반적으로 docker-compose 파일을 보고 kubernetes에 특성에 맞춰서 설정해주면 어렵지 않게 진행할 수 있지만 문제는 볼륨이었다. 볼륨을 사용하는 방식이 차이가 있기에 이를 적절하게 바꿔주는 것이 어려웠다. 특히 db 볼륨 같은 경우 로컬에 완전 마운트 된 것이 아니기에 완전하게 했다고 할 수는 없다고 생각한다. 웹 사이트가 동작하는 최소 목표만 달성했다.

또한 backend, frontend 모두 내가 참여한 것이 아니기 때문에 문제가 발생했을 때 어떤 파트에서 발생한 것인지, 찾지 못한 에러인지 내가 잘못 설정한 것인지 구분하는 것이 처음 접해보는 문제라 신선했다.

다음 글에서는 전환한 파일을 일부 수정하고 eks를 사용하여 aws에 배포하는 과정을 정리할 예정이다.

0개의 댓글