Recofashion - docker-compose와 kubernetes를 이용해 배포하기

백근영·2020년 2월 7일
1
post-thumbnail

개요

2019년 11월에 진행했던 recofashion 프로젝트를 docker-compose와 쿠버네티스로 각각 배포해보는 실습을 진행했다. 간략한 배포 전략과 배포 과정에서 마주친 문제들을 적어보려 한다. 도커와 쿠버네티스에 관한 학습 자료는 여기에 있다.

docker-compose를 이용한 배포

docker-compose.yaml

version: "3"

services:
    mysql:
        container_name: mysql
        image: mysql:5.7
        ports:
          - 3306:3306
        environment:
          MYSQL_DATABASE: recofashion
          MYSQL_ROOT_PASSWORD: dkdltm123
          MYSQL_ROOT_HOST: '%'
        volumes:
          - /recofashion/db:/var/lib/mysql
    server:
        container_name: recofashion_server
        image: recofashion/server
        ports:
          - 5000:8080
        depends_on:
          - mysql
        environment:
          SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/recofashion?serverTimezone=UTC&characterEncoding=UTF-8
          DEPLOY_HOST: http://192.168.137.120
          DEPLOY_PORT: 81
    client:
        container_name: recofashion_client_runtime
        image: recofashion/client-runtime
        ports:
          - 81:3000
        depends_on:
          - server
        environment:
          NODE_ENV: production

k8s & GKE를 이용한 배포

GKE structure

Api Server는 어떤 Service로 만들어야 할까?

api server는 쿠버네티스 클러스터 내부에서 client와 소통하기를 기대했기 때문에, 클러스터 외부로 노출하지 않고 Cluster-IP 타입으로 서비스를 만들고, 서비스 디스커버리 기능을 이용해 환경변수를 설정할 생각이었다. 하지만 실제로 그렇게 클러스터를 구축해본 결과 브라우저 상에서 api 서버로 날리는 리퀘스트는 DNS가 제대로 동작하지 않는다는 것을 깨달았다.

의도한대로 작동하지 않는 이유는 브라우저 상에서 날리는 요청은 클러스터 내부에서 날리는 요청이 아니기 때문인 것 같다. 브라우저를 띄우고 있는 각 사용자들의 호스트는 클러스터 외부에 있기 때문에 DNS가 제대로 작동하지 않는 것이다. 그러므로 api server 또한 client와 마찬가지로 NodePort 타입의 서비스로 만들고, ingress를 통해 외부로 노출시키기로 했다.

ingress.yaml

kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: ingress-client
spec:
  rules:
  - http:
      paths:
      - path: /*
        backend:
          serviceName: recofashion-client
          servicePort: 80
          
---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: ingress-server
spec:
  rules:
  - http:
      paths:
      - path: /*
        backend:
          serviceName: recofashion-api
          servicePort: 80

인그레스를 하나로 만들어서, 외부로 노출시킬 recofashion-api 서비스와 recofashion-client 서비스를 path를 다르게 하여 L7 로드밸런싱을 활용하는 방법도 생각해볼 수 있을 것 같다.

Nodejs로 환경변수 전달하기

보통 nodejs에서 환경변수는 process.env.<환경변수 이름>과 같은 방식으로 접근한다. 환경변수를 .env와 같은 정적 파일로 관리한다면 문제가 없겠지만, 컨테이너 개발 환경에서 docker-compose 등에서 env 값을 동적으로 넣어주기를 원한다면 아래와 같은 문제가 생길 수 있다.

정적 파일 빌드 방식의 문제

기존에 내가 리액트 애플리케이션을 배포하려고 했던 방식은 리액트 코드를 정적 파일로 빌드하고 그 앞단에 nginx를 둬서 정적 파일을 서빙하는 방식이었다. 이렇게 했을 시에 생기는 문제점은 파일을 빌드하는 시점에 모든 환경변수를 적절히 주입해주어야 한다는 것이었다. 도커 이미지를 빌드한 후에 docker-compose에서 설정해주는 env 값들은 정적 파일이 인식할 수 없게 된다.

상주 애플리케이션 형태로 배포하기

도커 이미지를 빌드한 후에 동적으로 환경 변수를 주입해주기 위해서 생각한 방식은 리액트 코드를 정적 파일로 빌드하지 않고 상주하는 애플리케이션으로 배포하는 것이었다(yarn start, npm run start 등). 이렇게 하면 docker-compose나 쿠버네티스의 매니페스트 파일에서 주입해주는 환경 변수를 실시간으로 전달받을 수 있게 된다. 하지만 이렇게 배포를 하게 되면 메모리 사용률이 높아진다는 점과 npm이나 yarn 등의 패키지 관리자가 컨테이너에 포함되어 있어야 하기 때문에 멀티 스테이지 빌드를 적용할 수 없다는 점, 따라서 도커 이미지의 크기가 불필요하게 커지는 등의 단점이 존재한다.

Ingress의 health check

쿠버네티스의 인그레스는 지정된 백엔드 서비스를 외부로 노출시키기 전에 서비스가 정상적인 상태인지 확인하기 위해 간단한 헬스체크를 거친다. 구체적인 헬스체크 방법을 매니페스트 파일에서 readinessProbe 등의 필드를 선언해 지정해줄 수 있겠지만, 기본적인 헬스체크는 백엔드 서비스의 기본 라우트(/)에 보내는 Get 요청에 대한 응답이 정상적인지(status가 200 이상 400 미만인지)를 판단하는 방식으로 이루어진다.

healthCheckController 만들기

인그레스로 api 서버를 노출시킨 후 계속 헬스 체크가 실패했었는데, 그 이유가 바로 기본 라우트 설정이 안되어있기 때문이었다. 아래와 같이 healthCheckController를 만들어주니 헬스 체크가 성공하고 정상적으로 서비스가 인그레스를 통해 노출되는 것을 확인할 수 있었다.

@RestController
class HealthCheckController
{
    @GetMapping("/")
    fun healthCheck() : Map<String, String> {
        val ret: MutableMap<String, String> = HashMap()
        ret["message"] = "ok"

        return ret
    }
}

배운 점

  • 가벼운 도커 이미지를 만들기 위한 멀티 스테이지 빌드
  • 이식성을 높이기 위한 환경변수 관리 방법(nodejs, spring-boot)
  • GKE 스택 드라이버를 이용한 로그 추척, 디버깅
  • 파드의 헬스 체크 전략 세우기
  • 서비스 타입별 차이(Cluster-IP, NodePort, LoadBalancer) 학습
profile
서울대학교 컴퓨터공학부 github.com/BaekGeunYoung

0개의 댓글