이전 회사에서부터 JuiceFS를 사용해왔고, 여러 소규모 회사에서 이 도구를 사용하며 필수적인 인프라로 자리 잡았습니다. 특히 소규모 팀에 있어 큰 도움이 되었습니다. 최근 글쓰기 활동을 계기로, 소규모 팀이 JuiceFS를 어떻게 사용하고 있는지 소개합니다.
이 글에서 다루는 사례는 "특이한 활용"이라기보다는, JuiceFS가 이미 널리 사용되고 있는 기능들입니다. 하지만 이 글은 내부 프로젝트 문서의 확장판으로, 유지보수 과정에서 얻은 경험을 정리한 것입니다.
특이한 활용: 컨테이너 공유 저장소
CSI 지원이 존재하지만, 우리는 항상 모든 Kubernetes 노드에 /jfs에 JuiceFS를 마운트하여, 컨테이너 애플리케이션이 hostPath 방식으로 호스트 디렉토리를 마운트할 수 있도록 해왔습니다. 이를 통해 공유 저장소를 구현했습니다. 주의해야 할 점은 다음과 같습니다:
jfs.mount은 컨테이너 서비스보다 먼저 시작되어야 합니다. Docker 예시에서는 다음처럼 설정할 수 있습니다:
# /etc/systemd/system/docker.service.d/12-after-jfs.conf
[Unit]
After=jfs.mount
- 마운트할 디렉토리는 미리 생성하고, 권한을 설정해야 합니다(컨테이너 프로세스의 uid와 일치). 이 과정은 번거로울 수 있으므로, 팀이 이 과정을 피하고 싶다면 CSI를 사용하세요.
- 클러스터 확장 시 불편함이 발생합니다. 모든 노드에 JuiceFS를 마운트해야 하기 때문입니다. Kubernetes 클러스터의 복제 수가 충분하지 않을 경우, 새 노드를 추가하려면 JuiceFS를 마운트해야만 클러스터에 참여할 수 있습니다. 이 설계는 극단적인 상황에서 시간을 많이 낭비할 수 있으며, CSI 사용의 또 다른 이유가 됩니다.
이러한 문제점에도 불구하고, hostPath 방식은 관리가 간단하고 직관적이며, 우리의 사용 습관에 따라 /jfs/[appname]-[cluster]와 같은 이름 규칙을 사용해 쉽게 이해할 수 있습니다. Kubernetes PV 개념에 익숙하지 않은 동료에게도 작업이 더 쉬워집니다.
특이한 활용: 웹 기반 파일 공유
파일을 자유롭게 저장하고 공유하기 위해 JuiceFS를 사용할 수 있습니다. JuiceFS는 다양한 플랫폼에서 마운트 가능하며, Windows에서도 잘 작동합니다. 하지만 사용자가 로컬에 직접 마운트하는 것은 운영과 보안 측면에서 어렵습니다. 대신, 간단한 웹 서비스를 만들어 JuiceFS를 마운트하고 파일을 다운로드 가능한 형태로 제공할 수 있습니다.
이 작업은 매우 간단합니다. 저는 이 아이디어를 떠올린 후 5분 만에 설정했습니다. lain을 사용하면 간단한 values.yaml 파일로 Python 기반의 http.server를 실행할 수 있습니다:
appname: jfs-http-server
volumes:
- name: jfs-data
hostPath:
path: "/jfs"
type: Directory
volumeMounts:
- name: jfs-data
mountPath: /jfs
deployments:
web:
replicaCount: 1
image: python:latest
podSecurityContext: {}
resources:
limits:
cpu: 1000m
memory: 80M
requests:
cpu: 10m
memory: 80M
command: ["python", '-m', 'http.server']
workingDir: /jfs
containerPort: 8000
ingresses:
- host: jfs
deployName: web
paths:
- /
개발 경험을 가진 사람이라면 이 코드가 community의 python:latest 이미지를 사용하여 http.server를 실행하고, 호스트의 /jfs 디렉토리를 마운트한다고 이해할 수 있습니다. 서비스가 실행되면 jfs.example.com에 접속해 /jfs 하위의 모든 파일을 탐색하고 다운로드할 수 있습니다.
이 섹션은 라인(Lain)에 대한 광고처럼 보일 수 있지만, DevOps 환경에서는 유용한 도구들이 서로를 끌어당깁니다. DevOps 문화를 따르는 팀이라면 참고하시길 바랍니다.
특이한 활용: JupyterLab에서 실시간 개발
우리 팀은 데이터와 자주打交道합니다. 데이터 리포트 및 시각화 분석 외에도, 데이터에 접근해서 개발 아이디어를 검증하고자 하는 경우가 많습니다. 모두가 원격 데이터베이스에 연결하는 것은 불편하고 보안상 위험할 수 있으며, 모두가 해당 도구를 다룰 수 있는 것도 아닙니다. 따라서 저는 JupyterLab을 배포했으며, 많은 편의성을 추가했습니다. 이는 회사 내부 데이터베이스의 단축키를 내장하고, 개발자 및 데이터 엔지니어가 Python 라이브러리로 분석을 수행하거나 Bokeh를 사용해 시각화를 제공할 수 있도록 구성되었습니다.
Jupyter에서 작성된 코드는 버전 관리 시스템에 저장되어야 하므로, 나는 JupyterLab의 작업 디렉토리를 JuiceFS로 설정했습니다. 모든 노트북은 JuiceFS에 저장됩니다. Lain을 사용해 JupyterLab을 배포하는 것은 매우 간단합니다. 아래는 사용된 values.yaml입니다:
appname: lab
env:
SHELL: zsh
IPYTHONDIR: /lain/app
volumes:
- name: jfs
hostPath:
path: "/jfs/lab"
type: Directory
volumeMounts:
- name: jfs
mountPath: /jfs/lab
deployments:
web:
replicaCount: 1
podSecurityContext: {'runAsUser': 0}
terminationGracePeriodSeconds: 70
resources:
limits:
cpu: 2
memory: 4Gi
requests:
cpu: 100m
memory: 1Gi
command: ['jupyter', 'lab', '--allow-root', '--collaborative', '--no-browser', '--config=/lain/app/jupyter_notebook_config.py']
containerPort: 8888
workingDir: /jfs/lab/notebooks
ingresses:
- host: lab
deployName: web
paths:
- /
build:
base: lain:latest
prepare:
script:
- apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E0C56BD4
- echo "deb https://repo.clickhouse.tech/deb/stable/ main/" | tee /etc/apt/sources.list.d/clickhouse.list
- apt-get update
- apt-get install -y apt-transport-https ca-certificates dirmngr clickhouse-client=20.12.8.5 clickhouse-common-static=20.12.8.5
- apt-get clean
- pip3 install -r requirements.txt
script:
- pip3 install -r requirements.txt
Jupyter 자체만으로는 사용성이 제한적이므로, 저는 편의성을 강화했습니다. 예를 들어, 다양한 데이터베이스 클라이언트를 패키징했습니다:
from os import environ
import pandas as pd
import pymysql
from IPython.core.display import display
class MySQLClient:
def __init__(self, config):
config.update({
'charset': 'utf8mb4',
'cursorclass': pymysql.cursors.DictCursor,
'autocommit': True,
})
self.config = config
def use(self, db):
self.config['database'] = db
return self.execute(f'use {db}')
def fetch(self, sql, *args, **kwargs):
return self.execute(sql, *args, **kwargs)
def fetchone(self, sql, *args, **kwargs):
kwargs.update({'fetchone': True})
return self.execute(sql, *args, **kwargs)
def executemany(self, sql, *args, **kwargs):
con = pymysql.connect(**self.config)
with con.cursor() as cur:
cur.executemany(sql, *args, **kwargs)
res = cur.fetchall()
con.close()
return res
def execute(self, sql, *args, **kwargs):
con = pymysql.connect(**self.config)
with con.cursor() as cur:
fetchone = kwargs.pop('fetchone', None)
as_pandas = kwargs.pop('as_pandas', None)
cur.execute(sql, *args, **kwargs)
if fetchone:
res = cur.fetchone()
else:
res = cur.fetchall()
con.close()
if as_pandas:
return pd.DataFrame(res)
return res
x = execute
def preview(self, table_name=None, n=2):
if not table_name:
return self.execute('show tables', as_pandas=True)
if '.' in table_name:
n = max([n, 20])
table_name, column_name = table_name.split('.')
part = self.execute(
f'''
SELECT DISTINCT {column_name}, count(*) AS count
FROM {table_name}
GROUP BY {column_name}
ORDER BY count DESC
LIMIT {n}
''', as_pandas=True
)
return part
part1 = self.execute(f'''
SELECT `column_name`,
`column_type`,
`column_comment`
FROM `information_schema`.`COLUMNS`
WHERE `table_name` = "{table_name}"
''', as_pandas=True)
display(part1)
part2 = self.execute(
f'''
SELECT *
FROM {table_name}
ORDER BY RAND()
LIMIT {n}
''', as_pandas=True
)
return part2
MYSQL_CONFIG = jalo(environ['MYSQL_CONFIG'])
mysql_client = mysql = my = MySQLClient(MYSQL_CONFIG)
mysql_client.use('mydatabase')
이런 패키징을 통해 얼마나 편리한지 감을 잡을 수 있습니다:
이와 같은 편리한 호출을 통해, 저는 근무했던 팀의 백엔드 엔지니어, 데이터 분석가, 그리고 제품 매니저까지 JupyterLab을 활용해 업무를 수행할 수 있게 했습니다.
Jupyter에 대해 너무 많은 내용을 다루었지만, 사실 이 프로젝트는 JuiceFS와 깊은 연관이 있습니다:
- 생성된 데이터 리포트나 기타 ad hoc 프로세스 결과물은 JuiceFS에 저장되어, 쉽게 공유할 수 있습니다(참조: "웹 기반 파일 공유" 섹션).
- 모든 코드(노트북이라고 불리는 Jupyter의 코드)는 JuiceFS에 저장되며,
juicefs snapshot을 통해 정기적으로 백업됩니다.
특이한 활용: GitLab, ClickHouse, Elasticsearch 등
이론적으로, 모든 애플리케이션이 데이터를 저장할 때 JuiceFS에 저장할 수 있습니다. 다만 적절한 성능 범위를 고려해야 합니다. 이 섹션에서는 우리가 시도한 몇 가지 방법을 소개합니다:
- GitLab은 I/O 요구사항이 높아, MR 시 코드베이스가 큰 경우 SSD로 이전하는 것이 좋습니다. 그러나 소규모 팀이라면 프로젝트가 적고 크기가 작다면 JuiceFS에 저장하면 여러 추가 이점을 누릴 수 있습니다. 예를 들어,
juicefs grep을 통해 전체 코드베이스에서 코드를 검색하거나,juicefs snapshot을 통해 모든 리포지토리 데이터를 백업할 수 있습니다. - ClickHouse와 JuiceFS CSI를 결합해 CH 클러스터를 쉽게 구성할 수 있습니다. 이에 대한 세부 사항은 "소규모 팀이 Sentry를 유지보수하는 방법"에서 더 자세히 설명합니다.
특이한 활용: CI
GitLab CI를 예로 들면, Runner에 마운트 디렉토리를 설정할 수 있습니다:
[runners.docker]
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/jfs:/jfs:rw", "/cache"]
JuiceFS를 CI Runner에 마운트하면 다양한 가능성과 편리함이 생깁니다. 아래의 사례들은 모두 특별한 기술이 아니지만, JuiceFS 덕분에 편리하고 유지보수가 쉬워졌습니다.
빌드 결과물 배포 (Artifacts)
JFS는 파일 저장 용도로 사용되기 때문에, 빌드 결과물을(Java 안드로이드 패키지 등) JFS에 올리고, 앞서 언급한 "웹 기반 파일 공유" 섹션의 파일 공유 기능을 활용하면 다운로드 링크를 쉽게 만들 수 있습니다. 팀 내 비기술 직원에게 빌드 결과물을 공유해야 한다면, JFS + Python http.server가 좋은 조합이 될 수 있습니다.
지속적 배포
모든 서비스 배포가 컨테이너 업데이트가 필요한 것은 아닙니다. 예를 들어, 일부 프론트엔드 애플리케이션 업데이트는 정적 파일 배포만으로 이루어집니다. 이 과정은 일반적으로 CI에서 처리되며, 프론트엔드 애플리케이션 배포와 JFS 간에 훌륭한 협력이 가능합니다:
- CI Job은 프론트엔드 애플리케이션의 정적 파일을 컴파일하고, 버전 번호가 포함된 JFS 경로에 배포합니다.
- Nginx 설정을 업데이트하여 최신 버전의 경로로 사이트를 전환하면 배포가 완료됩니다. 필요시 CDN 예열을 트리거할 수도 있습니다.
- 롤백이 필요할 경우, 해당 버전의 CI Job을 다시 실행하면 이전 버전이 다시 배포됩니다.
또한, 일부 프로젝트는 특정 서버에 배포되는 경우가 있습니다. 이 서버는 데이터센터나 사무실에 있을 수 있습니다. 이러한 서버에 회사 내부 네트워크 VPN을 설정하고, 각각의 서버에 정기적으로 git clone을 설정하는 것은 번거로운 작업입니다. 하지만 JFS가 있다면, 이와 같은 방식은 필요 없습니다. 다음과 같이 진행합니다:
- 모든 서버는 JFS를 마운트하고, 초기 설정 시 자동으로 마운트됩니다.
- 프로젝트 코드는 CI를 통해 JFS에 배포됩니다. 예를 들어, 코드 변경 시
/jfs/[appname]의 내용을 덮어씌웁니다. - 서버는
/jfs/[appname]의 파일 변화를 감시하거나, 매일 밤 정기적으로 재시작하도록 설정합니다.
전역 캐시
GitLab CI 또는 기타 CI 시스템은 다양한 캐시 메커니즘을 가지고 있지만, 일부 CI 도구는 프로젝트별이 아닌 전역 캐시를 사용할 수 있습니다. 이 경우, JuiceFS를 사용해 전역 캐시를 저장할 수 있습니다. 예를 들어:
Trivy
Trivy를 사용해 컨테이너 이미지 보안 스캔을 수행합니다. Trivy는 보안 취약점에 대한 특성 데이터를 필요로 하며, 모든 이미지에서 동일한 DB를 사용합니다. 따라서 정기적으로 JuiceFS에 데이터를 업데이트하는 CI Job을 설정했습니다:
refresh_trivy_db:
stage: schedule
variables:
TRIVY_CACHE_DIR: /jfs/trivycache
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
script:
- trivy --cache-dir $TRIVY_CACHE_DIR image --download-db-only
이후 모든 프로젝트는 JFS에 있는 데이터를 사용해 이미지 취약점 스캔을 수행할 수 있습니다.
container_scanning:
stage: release
rules:
- if: '$CI_PIPELINE_SOURCE != "schedule"'
variables:
GIT_STRATEGY: none
TRIVY_CACHE_DIR: /jfs/trivycache
script:
- trivy --cache-dir $TRIVY_CACHE_DIR image --skip-db-update=true --exit-code 0 --no-progress --severity HIGH "${IMAGE}:latest"
- trivy --cache-dir $TRIVY_CACHE_DIR image --skip-db-update=true --exit-code 1 --severity CRITICAL --no-progress "${IMAGE}:latest"
Semgrep
Trivy는 이미지 스캔을 담당하고, Semgrep은 코드 스캔을 담당합니다. Semgrep은 정기적으로 업데이트된 규칙 파일을 필요로 합니다. 일반적으로 현지에서 다운로드하는 방식이지만, 네트워크 문제로 인해 국내에서는 미리 다운로드한 파일을 사용하는 경우가 많습니다. 이때 JuiceFS가 유용합니다:
# 참조: https://semgrep.dev/docs/semgrep-ci/sample-ci-configs/#gitlab-ci
semgrep:
image: semgrep-agent:v1
script:
- semgrep-agent
variables:
SEMGREP_RULES: >- # more at semgrep.dev/explore
/jfs/semgrep/security-audit.yaml
/jfs/semgrep/secrets.yaml
/jfs/semgrep/ci.yaml
/jfs/semgrep/python.yaml
/jfs/semgrep/bandit.yaml
rules:
- if: $CI_MERGE_REQUEST_IID
규칙 파일 업데이트 프로세스는 별도로 설명하지 않습니다.
도움이 되셨다면 Juicedata/JuiceFS 프로젝트를 팔로우해주세요! (0ᴗ0✿)