用 K8s 部署一个 Jellyfin


发布于

|

分类

死长死长的配置文件啊!越写越打退堂鼓:人为什么要这么折磨自己啊!

反正基本上是跟着这篇文章走的吧。

起因

买了个 NUC,想学学 K8s。想着之前用 Jellyfin 很爽,就从它下手吧。

先看看原始需求

就,部署一个 jellyfin,正常挂载一些东西,完事儿。

如果用 docker-compose 写的话,大概长这样:

version: '3.5'
services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    network_mode: 'host'
    devices:
      - /dev/dri:/dev/dri
    volumes:
      - /host/path/to/config:/config
      - /host/path/to/media:/media

定义一下使用的镜像,定义两个本地 volume,一个用于存配置,一个用于存媒体文件。最后还需要挂一下/dev/dri 这个东西,我也不知道是干啥的,反正挂上去之后字幕和解码啥啥的就莫名能用了。

这个配置在我的 NAS 上一直工作很好。

Namespace

一般情况下,K8s 有个默认的 Namespace 叫「default」。为了整洁干净,所以我们可以给 Jellyfin 分配个单独的 Namespace,就叫 jellyfin 好了。

这步不是必须的。如果不做的话,后面所有配置里面的 namespace 都改成 default 即可。

apiVersion: v1
kind: Namespace
metadata:
  name: jellyfin

Persistent Volume Claim

这玩意儿是说,「我需要一些存储空间,K8s 你帮忙准备一下啊~」。

原文里是先声明 PersistentVolume,后声明 PersistentVolumeClaim。但是我不太清楚 Persistent Volume 和 Persistent Volume Claim 有啥区别。因为我看到的是,只写 Persistent Volume Claim 似乎也能正常部署。所以就懒了一下,只写了 Persistent Volume Claim。

按照最前面的 docker-compose,我们需要俩存储空间,一个用来存放 Jellyfin 的配置,另一个用来放媒体文件。但其实,我自己的规划是「NFS 共享里面只放配置」,所以只需要一个 PersistentVolumeClaim 就好了。那么数据怎么办呢?按照这篇文章的介绍,我们可以直接在部署那里指定 NFS 就好。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jellyfin
  namespace: jellyfin
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: datapool
  resources:
    requests:
      storage: 2Gi

上面的配置中有个叫 storageClassName 的东西,那是我提前配置好的 NFS 远程存储,具体可见这篇文章。反正这样写会比原文中的写法简洁那么一点点。

Jellyfin 本体

我们要创建一个 deployment,这样万一一个节点挂了,K8s 能自动给我们在其他节点上再启一个实例。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: jellyfin
  name: jellyfin
  namespace: jellyfin
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  selector:
    matchLabels:
      app: jellyfin
  template:
    metadata:
      labels:
        app: jellyfin
    spec:
      volumes:
        - name: jellyfin-config
          persistentVolumeClaim:
            claimName: jellyfin
        - name: jellyfin-media  # Use raw NFS directly
          nfs:
            path: /Media         # NFS of Multimedia
            server: 192.168.1.10 # IP of my NAS
        - name: device-dri
          hostPath:
            path: /dev/dri
      containers:
        - env:
            - name: TZ
              value: Asia/Shanghai
          securityContext:
            privileged: true # Container must run as privileged inside of the pod, required for hardware acceleration
          image: jellyfin/jellyfin
          imagePullPolicy: Always
          name: jellyfin
          ports:
            - containerPort: 8096
              name: http-tcp
              protocol: TCP
            - containerPort: 8920
              name: https-tcp
              protocol: TCP
            - containerPort: 1900
              name: dlna-udp
              protocol: UDP
            - containerPort: 7359
              name: discovery-udp
              protocol: UDP
          resources: {}
          stdin: true
          tty: true
          volumeMounts:
            - mountPath: /config
              name: jellyfin-config
            - mountPath: /media
              name: jellyfin-media
            - mountPath: /dev/dri
              name: device-dri
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      securityContext: # UID and GID, can ignore. Default is root.
        runAsUser: 1000
        runAsGroup: 100

在这个配置里,我们先定义了一堆 K8s 内部「如何滚动升级、挂了怎么办」的东西,然后就像 docker-compose 一样,声明了一些用来存储的卷,我把媒体文件的声明单独拆出来了。最后是配置 Jellyfin 本体:使用啥镜像、开放哪些端口、Volume 挂载到哪儿、用啥 UID 和 GID(可忽略,默认用 root 好像)

Service 设置

这里没太看懂,所以就差不多直接搬上来了。

kind: Service
apiVersion: v1
metadata:
  name: jellyfin # < name of the service
  namespace: jellyfin # < namespace where to place service
spec:
  selector:
    app: jellyfin # < reference to the deployment (connects the service with the deployment)
  ports:
    - port: 1900 # < port to open on the outside on the server
      targetPort: 1900 # < targetport. port on the pod to passthrough
      name: dlna-udp # < reference name for the port in the deployment yaml
      protocol: UDP
    - port: 7359
      targetPort: 7359
      name: discovery-udp
      protocol: UDP
    - port: 8096
      targetPort: 8096
      name: http-tcp
      protocol: TCP
    - port: 8920
      targetPort: 8920
      name: https-tcp
  type: LoadBalancer
  sessionAffinity: ClientIP # This is necessary for multi-replica deployments

大概是说,这个 Jellyfin 在集群里面是如何映射的。反正经过这个配置后,集群上的每个节点都可以将流量转发到 Jellyfin 的工作 Pod(似乎是 LoadBalancer 的作用)。

原文里说 TCP 端口和 UDP 端口需要分别用一个单独的配置文件来配置,我也查到官方文档里面说「所有端口必须具有相同的协议」。但我尝试可以合在一起来搞,所以,就合在一起了。

Ingress 配置

如果没有 Ingress,那么几乎所有东西都是在集群内互通的(访问方式是 http://<service-name>.<namespace>),集群外访问不到(也不绝对,比如可以 ssh port forward 啥的,总之访问会很麻烦)。Ingress,gress 我看不懂,前面的 In 就是开了一个从外部访问到集群内部的口子。

这里用的是域名的方式来搞的。所以配置比较爽,后面在其他平台上设置比较麻烦。由于也看不太懂,所以差不多也是直接抄下来的:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: jellyfin # < name of ingress entry
  namespace: jellyfin # < namespace where place the ingress enty
spec:
  rules:
    - host: jellyfin.myprovatedomain
      http:
        paths:
          - path: /
            pathType: Prefix # pathType no longer has a default value in v1; "Exact", "Prefix", or "ImplementationSpecific" must be specified
            backend:
              service:
                name: jellyfin
                port:
                  name: http-tcp # < same label as the port in the service tcp file

在这里,我告诉 Ingress 说,我的域名是 jellyfin.myprivatedomain,然后从根路径开始都路由到 jellyfin 的 http-tcp 口上。

K3s 有个基于 traefik 的 Ingress,不需要我们自己再去安装基于 nginx 的 Ingress。

域名解析

额,都说了是在内网,解析个毛线啊!

但成也内网,败也内网。因为 jellyfin.myprivatedimain 这个域名是不存在的,所以我们需要想办法 Hack 一下,让自己路由器下能正常解析这个域名。本质上是改 DNS,将这个不存在的域名解析到集群里面的任意一个节点上(因为用了 LoadBalancer)。

第一种方法是改本机的 hosts 文件。如果用了 Surge 之类的代理软件,在配置里有个 DNS,加上对应的解析即可。

第二种方法是改路由器的 DNS。如果是 OpenWRT,在 DHCP/DNS 设置里面,有个地方可以自定义解析,加上去就好了。如果你用了 AdGuard,那么在 AdGuard 里面加也是极好的。不过需要注意的是,如果你电脑上有代理软件,那么你的代理软件需要放过这个域名,用 OpenWRT 或者 AdGuard 发来的解析结果。

好了,访问一下试试

在浏览器里面输入 http://jellyfin.myprivatedomain ,应该就能看到 Jellyfin 的配置界面了。注意一定要输全,不然可能会跳 Google 搜索。

配置代理

额,为啥会有这么个奇怪的茬……

就是说,tvdb 啥的在国内无法访问,所以刮削基本上都会失败。那么为了正常刮削,我们需要配置个代理。

在 Jellyfin 的「设置-控制面-网络-防火墙和代理」里面可以配置代理,不过似乎不咋生效。

另一个方法是在 Docker 里面配置 HTTP_PROXY 这个环境变量。配置文件大概这样改:

apiVersion: apps/v1
kind: Deployment
...
spec:
  ...
  template:
    spec:
      ...
      containers:
        - env:
            - name: HTTP_PROXY
              value: http://192.168.1.1:1111
            - name: HTTPS_PROXY
              value: http://192.168.1.1:1111
            ...

话说 Jellyfin 刮削好慢啊,八季美剧刮削了接近一个小时……怪不得那么多人用 TMM 或者 NasTool……

硬件解码

没搞好。直接看这个官方文档吧。

总结

K8s 的概念真的是太 tm 多了……本来用 docker-compose,12 行就能搞完的东西,K8s 硬生生搞了 150 行。yaml 工程师真的是名不虚传。更可恶的是,还没有 UI 可以直接配置,万一出错了也不知道是哪里错了,只能没头苍蝇乱改一通。

不管怎么说,用上了。

参考资料


评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注