[docker/kubernetes] kompose를 사용하여 docker를 kubernetes로 변환하기

Myoung Jun Ko·2024년 7월 23일
2
post-thumbnail

42seoul 과정 중 공통 과정 마지막 프로젝트인 tail-passenger(과제명은 transcendence)를 마쳤다. 이 프로젝트를 시작할 때 하고 싶었던 DevOps 카테고리 중에 ELK stack을 시간 상 못했기 때문에 프로젝트가 끝난 지금 도전해봐야겠다고 느꼈다.

우리 프로젝트는 프로젝트 명세에 따라 docker compose로 배포한다. 또한 DevOps 카테고리에서 elk stack을 배포하는 방법 역시 docker compose로 하는 것을 전제한다. 하지만 docker compose는 이미 사용해봤고 docker와 함께 자주 거론되는 kubernetes를 사용해보기 위해서 쿠버네티스를 사용하기로 결정했다. 이를 위해 udemy에서 Kubernetes 실습: AWS 클라우드에 마이크로서비스 배포하기라는 강의를 듣고 나서 변환 작업을 시작했다.

docker와 kubernetes의 차이점

도커에 대한 정의는 컨테이너를 다루는 도구 인 반면에, 쿠버네티스에 대한 정의는 컨테이너를 오케스트레이션 하는 도구 라고 되어 있다. 정의에서 나타나듯이 도커는 컨테이너를 쉽게 내려받거나 공유하고 구동할 수 있도록 해주는 도구인 컨테이너 런타임 인 반면에 k8s는 컨테이너 런타임을 통해 컨테이너를 오케스트레이션 하는 도구 라고 할 수 있다.

오케스트레이션은 여러 IT 자동화 태스크 또는 프로세스를 조정하여 실행하는 것이라 정의되어 있는데 이를 쿠버네티스에 적용하여 다시 정의하면 쿠버네티스는 여러 서버(노드)에 컨테이너를 분산해서 배치하거나, 문제가 생긴 컨테이너를 교체하거나, 컨테이너가 사용할 비밀번호나 환경 설정을 관리하고 주입해 주는 일 등 을 한다고 정의할 수 있다.

쿠버네티스에 대한 자세한 설명은 여러 강의나 삼성 SDS 블로그에 올라온 쿠버네티스 알아보기 1편, 2편, 3편을 참고하면 더 이해하기가 쉽다. (3편 말미에 4편에 대한 언급이 있는데 안 올라온건지 찾을 수가 없다...)

docker compose를 kubernetes로 변환하기

먼저 변환할 때 어떻게 시작해야 할 지 막막했다. 배우긴 했지만 직접 하려니 역시 손이 잘 안 떼지는 느낌. 다행이도 쿠버네티스에서 나같은 사람을 위해 이미 도커 컴포즈 파일을 쿠버네티스 리소스로 변환하는 도구가 이미 존재했다. Kompose 라는 도구인데 사용법 역시 간단했다. 자세한 것은 공식 문서를 참고하면 된다.

예를 들어 변환 전 우리 web 컨테이너는 아래와 같이 정의되어 있다.

# docker-compose.yml
web:
    build:
      context: ./backend
    image: web
    container_name: web
    volumes:
      - ./backend/back/:/app
    expose:
      - "443"
    command: sh -c "python manage.py makemigrations && python manage.py migrate && python manage.py loaddata test_user.json && daphne -b 0.0.0.0 -p 443 back.asgi:application"
    env_file:
      - ./.env
    depends_on:
      - db
    networks:
      - ts-network

이 파일이 담긴 위치에서 kompose convert 명령어를 실행하면 아래와 같이 두 개의 파일이 생성된다.

# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.34.0 (HEAD)
  labels:
    io.kompose.service: web
  name: web
spec:
  replicas: 1
  selector:
    matchLabels:
      io.kompose.service: web
  strategy:
    type: Recreate
  template:
    metadata:
      annotations:
        kompose.cmd: kompose convert
        kompose.version: 1.34.0 (HEAD)
      labels:
        io.kompose.service: web
    spec:
      containers:
        - args:
            - sh
            - -c
            - python manage.py makemigrations && python manage.py migrate && python manage.py loaddata test_user.json && daphne -b 0.0.0.0 -p 443 back.asgi:application
          env:
            - name: BASE_IP
              valueFrom:
                configMapKeyRef:
                  key: BASE_IP
                  name: env
            - name: CLIENT_ID
              valueFrom:
                configMapKeyRef:
                  key: CLIENT_ID
                  name: env
            - name: CLIENT_SECRET
              valueFrom:
                configMapKeyRef:
                  key: CLIENT_SECRET
                  name: env
            - name: DJANGO_SECRET_KEY
              valueFrom:
                configMapKeyRef:
                  key: DJANGO_SECRET_KEY
                  name: env
            - name: POSTGRES_NAME
              valueFrom:
                configMapKeyRef:
                  key: POSTGRES_NAME
                  name: env
            - name: POSTGRES_PASSWORD
              valueFrom:
                configMapKeyRef:
                  key: POSTGRES_PASSWORD
                  name: env
            - name: POSTGRES_USER
              valueFrom:
                configMapKeyRef:
                  key: POSTGRES_USER
                  name: env
            - name: REDIRECT_URI
              valueFrom:
                configMapKeyRef:
                  key: REDIRECT_URI
                  name: env
          image: web
          name: web
          ports:
            - containerPort: 443
              protocol: TCP
      restartPolicy: Always
# web-service.yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.34.0 (HEAD)
  labels:
    io.kompose.service: web
  name: web
spec:
  ports:
    - name: "443"
      port: 443
      targetPort: 443
  selector:
    io.kompose.service: web

위에서 kompose 관련 항목들은 app 과 같은 다른 필드로 대체해도 무관하다.

첫 번째 문제: ImagePullBackOff, ErrImagePull

변환된 파일을 바로 배포하면 위와 같은 결과를 확인할 수 있다. 이를 자세히 살펴보기 위해 describe 명령어를 사용하여 살펴보니 다음과 같은 에러가 발생했다.

Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  70s                default-scheduler  Successfully assigned default/web-5f9f5df857-2jkm6 to docker-desktop
  Normal   Pulling    24s (x3 over 68s)  kubelet            Pulling image "web"
  Warning  Failed     21s (x3 over 63s)  kubelet            Failed to pull image "web": Error response from daemon: pull access denied for web, repository does not exist or may require 'docker login'
  Warning  Failed     21s (x3 over 63s)  kubelet            Error: ErrImagePull
  Normal   BackOff    10s (x3 over 63s)  kubelet            Back-off pulling image "web"
  Warning  Failed     10s (x3 over 63s)  kubelet            Error: ImagePullBackOff

web 포드 말고 다른 두 포드 역시 같은 문제였다. 우리 배포 형식은 직접 만든 dockerfile 을 이용해서 이미지를 생성한 뒤, 사용하는데 kompose를 통해 변환된 yaml파일은 docker hub에서 이미지를 가져오려 하기에 가져올 이미지가 없어서 생기는 문제였다.

dockerfile을 직접 사용하여 배포하는 방법을 찾아보았으나 생각보다 자료가 많지 않고 대부분 docker hub에 올린 이미지를 사용하기에 나 역시 이미지를 만들어 docker hub에 올려 사용하기로 결정했다. 이에 따라 이미지를 올리고 deployment 파일을 아래와 같이 수정했다

# web-deployment.yaml
...
          image: kmj951015/tail-passengers_web:1.0.1
          name: web
...

frontendmiddleware 역시 위와 같이 docker hub에 이미지를 올리고 이미지 이름을 적어서 수정을 완료했다.

두 번째 문제: django.db.utils.OperationalError

다시 실행했으나 이번에도 역시 문제가 발생했다.

frontend 는 한 번만 실행되고 종료되어야 하는데 계속해서 재시작되고 있었고 web 은 아래와 같이 db를 찾지 못하고 있었다.

...
django.db.utils.OperationalError: could not translate host name "db" to address: Name does not resolve

먼저 web 포드의 문제부터 살펴보았다. 이미 도커에서 실행되는 것을 확인했기 때문에 백엔드 자체의 문제는 아니다. 그렇기에 현재 web 포드가 db 포드를 못 찾는다고 보는게 더 맞는 접근인 것 같았다.

살펴보니 kompose가 만든 파일 중에 db의 service 파일이 없었다. docker-compose.yml 파일에서 db에 따로 포트를 지정하지 않아서 만들지 않은 것으로 보인다. 그렇기에 아래와 같이 db 포드에 접근할 수 있도록 정의된 서비스 파일을 하나 생성한 뒤, 실행했다.

apiVersion: v1
kind: Service
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.34.0 (HEAD)
  labels:
    io.kompose.service: db
  name: db
spec:
  ports:
  - port: 5432
  selector:
    io.kompose.service: db

세 번째 문제: frontend는 왜 계속 실행되고 에러가 날까?

이제 frontend 포드를 살펴볼 차례이다. 기존 프로젝트 설계는 frontend 컨테이너는 한 번 생성되어 npm build를 마친 뒤, 종료된다. 그렇기에 frontend 포드 역시 마찬가지로 completed 상태에서 멈춰있어야 한다. 하지만 계속해서 CrashLoopBackOff 가 발생하고 다시 실행되어 completed 상태가 되는 것을 반복했다.

그래서 frontend 의 작업은 한 번만 실행되면 되기에 포드에서 Job 으로 변경하고 포드에서만 사용하는 필드들을 삭제했다.

# frontend-deployment.yaml
apiVersion: batch/v1
kind: Job
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.34.0 (HEAD)
  labels:
    io.kompose.service: frontend
  name: frontend
spec:
  template:
    metadata:
      annotations:
        kompose.cmd: kompose convert
        kompose.version: 1.34.0 (HEAD)
      labels:
        io.kompose.service: frontend
    spec:
      containers:
      - image: kmj951015/tail-passengers_frontend:first
        name: frontend
        volumeMounts:
        - mountPath: /app/dist
          name: dist
      restartPolicy: Never
      volumes:
        - name: dist
          persistentVolumeClaim:
            claimName: dist

이러니 더 이상 에러도 발생하지 않고 모두 잘 동작했다. 아직 nodePort를 설정하진 않았기에 middleware 포드에 포드 포워딩을 걸어서 443 포트를 30443으로 접근할 수 있게 하고 접속하였다.

kubectl port-forward service/middleware 30443:443

페이지 자체는 접근이 가능하지만 nginx가 403 에러를 발생시켰다. 아직 에러가 끝나지 않았다.

네 번째 문제: Nginx의 403 에러

이에 대해 고민하고 있을 때, 같은 팀은 아니지만 이 프로젝트를 진행한 다른 친구가 frontend가 끝나는 것 자체가 조금 이상하다며 middleware 부분과 합칠 것을 제안했다. middleware를 통해 접근하는 것 자체가 이상하다고 말이다. 듣고 보니 일리가 있었다. 또한 규모가 커지고 우리처럼 frontend가 종료되지 않는다면 합치는 것이 더 좋지 않겠지만 분리된 상태에서 nginx 조정하는 것의 번거로움과 frontend가 종료되고 끝나는 점, 프로젝트의 규모가 작은 점을 고려하여 두 포드를 합치기로 하였다.

그렇기에 job을 다시 pod로 바꾸고 middleware 포드의 부분을 frontend 포드에 병합하고 도커 이미지도 새로 생성하여 실행하였다. 또한 frontend 의 서비스에서 type으로 NodePort를 지정하여 포트포워딩 작업 없이 바로 접근할 수 있도록 수정하였다.

# frontend-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: frontend
  name: frontend
spec:
  ports:
  - name: "80"
    port: 80
    targetPort: 80
  - name: "443"
    port: 443
    targetPort: 443
    nodePort: 30443
  type: NodePort
  selector:
    app: frontend

결론

변환 과정을 해보면서 정말 설계가 중요함을 다시 한 번 느낀다. docker-compose.yaml 이 좀 더 짜임새있게 만들어졌다면 kompose와 이미지 문제만 해결하여 마무리 될 수도 있었을 것 같다.

그래도 쿠버네티스로 바꾸고 나니 프론트, 백 코드가 바뀌지 않는다는 전제 하에 실행하고 종료하는 것, 그리고 목적인 elk 스택을 추가하기엔 더 쉬워졌다. 다음 포스트에선 kompose 없이 직접 전환하며 쿠버네티스를 더 공부할 예정이다.

위 코드들은 모두 여기에 있다.

참고 문서

0개의 댓글