# [LLM / MLOps] Dify 자체 호스팅

제가 CTO로 있는 디비디랩에서는 좋은 유저 리서치를 할 수 있게 돕기 위해서 여러 LLM 프롬프트와 이를 종합한 에이전트를 개발해서 사용하고 있어요.

그 중에서 **[Dify](https://dify.ai/)**라는 서비스를 활용해 두 개 이상의 프롬프트에 로직과 플로우 제어를 더해서 하나의 목표를 가진 워크플로우를 만드는 데 유용하게 사용하고 있어요.

디파이는 유료 플랜을 사용해서 사용할 수도 있지만, 태생적으로 오픈소스 솔루션([깃허브 리포지토리](https://github.com/langgenius/dify))이기 때문에 직접 호스팅해서 사용하는 것도 당연히 가능합니다.

이 아티클에서는 고유 요구사항을 따라서 디파이를 자체 호스팅한 과정을 소개합니다.

# 1. 요구사항

1. 헬름 차트를 통해서 쿠버네티스 클러스터 위에 배포할 수 있어야 합니다.

2. PostgreSQL은 Amazon RDS를 사용합니다.

3. Redis는 쿠버네티스 클러스터 위에 이미 존재하는 클러스터를 사용합니다.

4. 벡터 스토어는 [Qdrant](https://qdrant.tech/)를 사용하고, 쿠버네티스 위에 자체 호스팅합니다.

5. 인그레스 자원을 사용하지 않고 Gateway API의 HTTPRoute 자원을 사용합니다.

6. VPN을 통해 내부망에 접속한 다음에 디파이에 접속할 수 있습니다.

# 2. 디파이 헬름 차트

디파이는 공식 헬름 차트를 제공하지 않고, docker-compose를 사용한 설치 방법과 소스 코드로부터 설치하는 방법만 제공하고 있어요. 😢 ([공식 문서 참조](https://docs.dify.ai/getting-started/install-self-hosted))

다행히 깃허브 리포지토리에 커뮤니티에서 관리하는 헬름 차트를 소개([깃허브 리포지토리 참조](https://github.com/langgenius/dify?tab=readme-ov-file#advanced-setup))하고 있어서, 아예 바닥부터 차트를 만들어야 하는 수고는 덜 수 있었습니다!

## 2.1. 차트 선정

공식 리포지토리에서 소개한 헬름 차트는 두 개입니다.

- LeoQuote가 관리하는 차트 ([douban/dify](https://github.com/douban/charts/tree/master/charts/dify))

- BorisPolonsky가 관리하는 차트 ([BorisPolonsky/dify-helm](https://github.com/BorisPolonsky/dify-helm))

여기서 첫 번째 차트는 두 번째 차트보다 나중에 시작된 프로젝트인데요. 패스워드와 API 키와 같은 민감정보를 평문으로 넘겨야 하는 한계때문에 만들어진 차트이지만, 현재는 둘 다 시크릿을 사용할 수 있게 되어 있어요.

솔직히 말해서, 어느 쪽도 완성되었거나 완벽하다고 할 수 없지만 첫 번째 차트의 경우가 사용자 입장에서 더 편리하다고 생각합니다.

우선 BorisPolonsky의 차트는 밸류가 너무 많아서 완전히 이해하고 값을 통제하기가 어려워요...

![values.yaml 파일이 3004줄인데, 예시도 없고 밸류에 대한 문서화도 안 되어 있다](https://upload.cafenono.com/image/slashpagePost/20250126/193147_bJ246tFwf7iYXMxCjG?q=80&s=1280x180&t=outside&f=webp)

반면, LeoQuote의 차트는 약 400줄 정도의 간결한 밸류 정의를 가지고 있으면서, 예시도 비교적 많이 제공하고 있어요. 벡터 스토어를 헬름 차트 하나로 해결할 수 없다는 문제가 존재하는데, 이 부분은 벡터 스토어를 따로 배포한 다음 연결하는 식으로 해결하려 해요.

그런 이유로 LeoQuote의 차트를 사용합니다!

## 2.2. 차트 기본 밸류 설정

선정한 헬름 차트의 설치 예시를 기반으로 확장해 보도록 할까요? 아래는 설치 예시를 참조하여 확장한 것입니다. ([헬름 차트 리포지토리 참조](https://github.com/douban/charts/blob/master/charts/dify/README.md#install))

```javascript
global:
  host: "dify.uniglot.com"
  enableTLS: true
  extraBackendEnvs:
  - name: SECRET_KEY
    value: "superconfidentialsecretkey"
```

- `host` : 실제 디파이가 사용할 도메인을 적어 주시면 됩니다.

- `enableTLS` : TLS를 사용할지 결정합니다.

    - 이 값이 `true` 로 설정되면, _templates/_helpers.tpl_의 `dify.baseUrl` 부분이 https 프로토콜을 사용하도록 변경됩니다. ([리포지토리 참조](https://github.com/douban/charts/blob/dify-0.5.1/charts/dify/templates/_helpers.tpl#L73-L75))

    - 또한, `dify.baseUrl` 은 `CONSOLE_API_URL` , `CONSOLE_WEB_URL` , `SERVICE_API_URL` , `APP_API_URL` , `APP_WEB_URL` 의 값을 변경합니다. ([리포지토리 참조](https://github.com/douban/charts/blob/dify-0.5.1/charts/dify/templates/_helpers.tpl#L82-L89), [공식 문서 환경 변수 참조](https://docs.dify.ai/getting-started/install-self-hosted/environments#common-variables))

    - 만약 TLS를 사용하는데 `enableTLS` 의 값이 `false`라면, HTTPS 백엔드에 HTTP로 요청을 하게 되어 Mixed Content 에러가 발생하게 됩니다.

- `image.tag` : 디파이 컨테이너의 이미지를 지정합니다. 지정하지 않을 경우 차트에 지정된 기본값을 사용합니다.

    - 버전 피닝은 강하게 권장됩니다. 다만, 헬름 차트에서 이미지 버전을 피닝하기 때문에 ArgoCD를 사용하면서 헬름 차트의 버전을 피닝하는 것도 가능합니다.

    - 저의 경우 헬름 차트의 버전을 피닝하고, `image.tag` 를 생략하였습니다.

- `extraBackendEnvs` : 디파이 컨테이너의 환경변수([공식 문서 참조](https://docs.dify.ai/getting-started/install-self-hosted/environments))를 직접 넘기는 부분입니다. 이 설정값을 사용해서 이 헬름 차트에서 제공하지 않는 설정을 진행할 수 있어요.

## 2.3. 메타데이터를 위한 PostgreSQL 연결

디파이는 메타데이터의 저장을 위해서 PostgreSQL를 사용합니다.

아래는 이미 만들어서 사용하고 있던 테스트용 Amazon RDS를 연결하는 설정이에요!

필요한 환경변수는 공식 문서를 참조해 주세요. ([환경 변수 공식 문서 링크](https://docs.dify.ai/getting-started/install-self-hosted/environments#database-configuration))

```javascript
global:
  # omit
  extraBackendEnvs:
  # omit
  - name: DB_HOST
    value: dify-uniglot.supersecret.ap-northeast-2.rds.amazonaws.com
  - name: DB_USERNAME
    valueFrom:
      secretKeyRef:
        name: dify-db-credentials
        key: username
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: dify-db-credentials
        key: password
  - name: DB_DATABASE
    value: dify

postgresql:
  embedded: false
```

`postgresql.embedded` 의 기본값은 `true` 인데, 그대로 두면 PostgreSQL을 클러스터 위에 배포하게 되니 비활성화해 줘요.

DB 인증 정보를 위한 시크릿은 적절히 생성해서 사용하시면 되겠습니다 허허헣

## 2.4. 레디스 연결

캐싱, pub/sub, 셀러리 브로커로 레디스를 사용합니다. ([환경 변수 공식 문서 참조](https://docs.dify.ai/getting-started/install-self-hosted/environments#redis-configuration))

레디스의 경우 쿠버네티스 클러스터 위에 있던 레디스를 사용하기로 했어요. 물론 Elasticache를 사용할 수도 있다는 점!

> 운영 환경에서는 레디스에 인증을 활성화하는 것을 추천합니다. `REDIS_USERNAME` 과 `REDIS_PASSWORD` 환경 변수에 인증 정보를 넘길 수 있습니다.

```javascript
global:
  # omit
  extraBackendEnvs:
  # omit
  - name: REDIS_HOST
    value: test-cluster.redis.svc.cluster.local
  - name: CELERY_BROKER_URL
    value: redis://test-cluster.redis.svc.cluster.local:6379/1

redis:
  embedded: false
```

여기서 세션을 위한 레디스 데이터베이스와 셀러리 브로커로 사용하기 위한 데이터베이스는 서로 달라야 해요.

저의 경우 전자는 0(기본), 후자는 1로 설정하였어요.

PostgreSQL과 마찬가지로, 레디스도 `redis.embedded` 가 기본적으로 `true` 라서, 레디스를 새롭게 클러스터 위에 배포하게 됩니다. 필요 없으니 `false` 로 지정하도록 해 볼까요?

# 3. 벡터 스토어 설정

디파이는 다양한 종류의 벡터 스토어를 지원해요.

- weaviate

- qdrant

- milvus

- zilliz

- myscale

- analyticdb

- couchbase

이 중에서 설치와 설정이 비교적 쉬운 Qdrant를 벡터 스토어로 선택했어요.

## 3.1. Qdrant 헬름 차트

이번 아티클에서는 공식 차트를 변경 없이 설치해 봅시다. `ml` 네임스페이스에 설치했어요!

```javascript
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: qdrant
  namespace: argocd
spec:
  source:
    repoURL: https://qdrant.github.io/qdrant-helm
    targetRevision: '1.13.1'
    chart: qdrant
    helm:
      releaseName: qdrant
# omit
```

## 3.2. 디파이 환경 변수 설정

설치한 Qdrant의 도메인을 환경 변수로 넘겨줄게요. 디파이 또한 `ml`  네임스페이스에 설치될 예정이므로, 서비스 이름으로 충분해요.

```javascript
global:
  # omit
  extraBackendEnvs:
  # omit
  - name: VECTOR_STORE
    value: qdrant
  - name: QDRANT_URL
    value: http://qdrant
```

# 4. 여기까지 완성된 헬름 밸류

이제 네트워크를 제외한 모든 기본적인 설정이 완료된 헬름 밸류가 나왔어요!

```javascript
global:
  host: "dify.uniglot.com"
  enableTLS: true
  extraBackendEnvs:
  - name: SECRET_KEY
    value: "superconfidentialsecretkey"
  - name: DB_HOST
    value: dify-uniglot.supersecret.ap-northeast-2.rds.amazonaws.com
  - name: DB_USERNAME
    valueFrom:
      secretKeyRef:
        name: dify-db-credentials
        key: username
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: dify-db-credentials
        key: password
  - name: DB_DATABASE
    value: dify
  - name: REDIS_HOST
    value: test-cluster.redis.svc.cluster.local
  - name: CELERY_BROKER_URL
    value: redis://test-cluster.redis.svc.cluster.local:6379/1
  - name: VECTOR_STORE
    value: qdrant
  - name: QDRANT_URL
    value: http://qdrant

postgresql:
  embedded: false
redis:
  embedded: false
```

# 5. HTTPRoute 설정을 위한 기존 차트 수정

저는 기본적으로 인그레스를 사용하지 않고 [Envoy Gateway](https://gateway.envoyproxy.io/docs/) 기반의 Gateway API 자원을 사용하고 있습니다.

하지만 이번 디파이 차트에서는 인그레스만을 지원하므로, 제가 가지고 있는 차트 중 게이트웨이 차트를 약간 수정하여 HTTPRoute 자원을 별도로 배포해 주었어요.

참고로 디파이 헬름 차트의 `ingress` 는 기본적으로 비활성화되어 있어요.

## 5.1. 게이트웨이 자원 배포를 위한 커스텀 차트 수정

우선 제가 작성해서 사용하고 있던 envoy-gateway라는 차트는 게이트웨이 클래스, 게이트웨이, EnvoyProxy 자원을 배포하기 위해 만들었습니다.

여기에서 `externalHelmRoutes` 라는 밸류를 추가합니다.

```javascript
# envoy-gateway/values.yaml

# omit

envoyProxy:
  # omit

gatewayClass:
  # omit

gateway:
  # omit

externalHelmRoutes: []
```

그리고 외부 헬름 차트가 인그레스만 지원할 경우에 여기에서 HTTPRoute를 정의할 수 있게 할 거예요.

실제 전달할 값의 형태는 아래와 같아요. 라우팅 규칙은 차트에 정의된 인그레스와 동일해요! ([디파이 차트 인그레스 템플릿 참조](https://github.com/douban/charts/blob/dify-0.5.1/charts/dify/templates/ingress.yaml))

```javascript
externalHelmRoutes:
  - name: dify-route
    namespace: ml
    hostnames:
      - dify.uniglot.com
    rules:
      - path: /
        service: dify-frontend
        port: 80
      - path: /console/api
        service: dify-api-svc
        port: 80
      - path: /api
        service: dify-api-svc
        port: 80
      - path: /v1
        service: dify-api-svc
        port: 80
      - path: /files
        service: dify-api-svc
        port: 80
    internalAccess: true  # 내부망에서만 접근하도록 하기
    clientCIDRs:  # 접근 허용 대역 (내부망 대역)
      - 10.0.42.0/10
```

그러면 이 리스트를 통해 HTTPRoute와 Envoy Gateway에서 네트워크 보안 제어를 담당하는 CR인 SecurityPolicy를 생성하도록 해 봅시다.

```javascript
# envoy-gateway/templates/httproute.yaml

{{- range .Values.externalHelmRoutes -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: {{ .name }}
  namespace: {{ .namespace | default "default" }}
spec:
  parentRefs:  # 사정에 맞게 올바른 게이트웨이를 바라보도록 설정
    - name: {{ $.Values.gateway.name }}
      namespace: {{ $.Values.envoyProxy.namespace }}
  hostnames:
    {{- range .hostnames }}
    - {{ . | quote }}
    {{- end }}
  rules:
    {{- range .rules }}
    - matches:
      - path:
         type: PathPrefix
         value: {{ .path | quote }}
      backendRefs:
        - name: {{ .service | quote }}
          port: {{ .port }}
    {{- end }}
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: {{ .name }}-security-policy
  namespace: {{ .namespace | default "default" }}
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: {{ .name }}
  {{- if .internalAccess }}  # 외부 접속 허용하지 않음
  authorization:
    defaultAction: Deny
    rules:
      - action: Allow
        principal:
          clientCIDRs:
            {{- range .clientCIDRs }}
            - {{ . | quote }}
            {{- end }}
  {{- end }}
---
{{- end }}

```

# 6. 결과

지금까지 나온 모든 차트를 다 설치해 줍니다.

ArgoCD 싱크가 모두 마쳐지면 VPN을 켜고, 지정한 도메인에 접속하면-

![Image](https://upload.cafenono.com/image/slashpagePost/20250127/010557_2FQ4pF2EkIvafR3kte?q=80&s=1280x180&t=outside&f=webp)

나만의 작은 디파이가 완성되었습니다!

당연히 실제로 배포에 사용한 값은 이 글과 다르고, 세부 보안 설정도 다르니 이 부분 유념해 주시면 좋겠고요!

감샤합니당 😍

For the site tree, see the [root Markdown](https://slashpage.com/uniglot.md).
