Progateでデータエンジニアをしている穴澤 (id:aaaanwz)です。
本記事は Progate Advent Calendar 2日目の記事になります。
Google Play Musicがサービス終了してしまい「所有している音楽データをアップロードし、インターネット経由で聴く」というサービスでしっくりくるものが無くなってしまいました。
ちょうど昨日の記事でおうちKubernetes環境を構築しているため、これを期におうちKubernetes環境に音楽ストリーミングサーバーを構築してみます。
概要
- 家庭内LANからファイルサーバーとして使える
- ファイルサーバーにアップロードした音楽データをインターネット経由で聴ける
- ファイルサイズが大きい楽曲はサーバーサイドでリアルタイムに圧縮して配信する
という要件から、以下のような構成にしてみます。
- 音楽配信サーバーには Airsonicを使います
- Ingress(L7ロードバランサー)経由でインターネットに接続します
- IngressをTLS終端にします
- ファイルサーバーとしてSambaを構築します
- Airsonicとストレージを共有します
- LoadBalancer Service(L4ロードバランサー)経由で家庭内LANに接続し、インターネットからは遮断します
構築
1. Storage
まず初めに、Podからホストマシンのストレージを使うためのPersistentVolume(PV)とPersistentVolumeClaim(PVC)を作成します。
今回は node1
の /mnt/hdd
に音楽データとメタデータ(設定、アカウント情報など)を永続化するとします。
pv.yaml▼
apiVersion: v1
kind: PersistentVolume
metadata:
name: music
spec:
capacity:
storage: 1000Gi
accessModes:
- ReadWriteOnce
local:
path: /mnt/hdd/music
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node1
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: config
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
local:
path: /mnt/hdd/config
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: music
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1000Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: config
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
サンプルコードでは local volume にしているため、この後にデプロイするAirsonicとsambaはnodeAffinityによって node1
にスケジューリングされる事になります。
物理ストレージが接続されているノードとは別のノードでPodを稼働させたい場合はnfs volumeにすればOKです。
(OS側のNFSマウント設定については割愛します)
... kind: PersistentVolume ... accessModes: - ReadWriteMany nfs: path: /music server: 192.168.0.XXX ...
2. Samba
次のステップではsambaをデプロイし、k8sクラスタをファイルサーバーとして使えるようにします。
Deployment
sambaで一番人気のDocker imageであるdperson/samba をデプロイします。
上で作成したPVCを /music
にマウントします。
samba-deployment.yaml▼
apiVersion: apps/v1
kind: Deployment
metadata:
name: samba
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: samba
template:
metadata:
labels:
app: samba
spec:
containers:
- name: samba
image: dperson/samba
args: [
"-u", "myuser;mypassword", # UPDATE HERE
"-s", "music;/music;yes;no;no;myuser",
"-w", "WORKGROUP"
]
ports:
- containerPort: 139
protocol: TCP
- containerPort: 445
protocol: TCP
resources:
limits:
cpu: "250m"
memory: "500Mi"
livenessProbe:
tcpSocket:
port: 445
volumeMounts:
- name: music
mountPath: /music
volumes:
- name: music
persistentVolumeClaim:
claimName: music
MetalLB
PodをLAN内に公開するため、L4ロードバランサーに相当するLoadBalancer Serviceもデプロイします。
事前準備としてオンプレミス環境用のLoadBalancer実装であるMetalLBを導入します。
microk8sの場合は以下のコマンド一発で利用可能になります。
$ sudo microk8s.enable metallb:192.168.0.AAA-192.168.0.BBB
:
の後はロードバランサーが用いるローカルIPアドレスプールを入力します。
その他の環境では公式ドキュメントを参照してください。
Service
samba Podの139番と445番ポートをローカルIPアドレス192.168.0.YYYとしてクラスタ外に公開します。
(YYYはMetalLBのアドレスプールの範囲で未使用のアドレスを設定します)
samba-svc.yaml▼
apiVersion: v1
kind: Service
metadata:
name: samba
namespace: default
spec:
selector:
app: samba
type: LoadBalancer
loadBalancerIP: 192.168.0.YYY # UPDATE HERE
ports:
- name: netbios
port: 139
targetPort: 139
- name: smb
port: 445
targetPort: 445
EXTERNAL-IPが割り当てられている事を確認します。
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE samba LoadBalancer 10.aaa.bbb.ccc 192.168.0.YYY 139:3xxxx/TCP,445:3xxxx/TCP 1m
Windows PCであればエクスプローラの ネットワークドライブの割り当て
から、¥¥192.168.0.YYY¥music
をネットワークドライブとしてマウントできるようになります。
手持ちの音楽データを {アーティスト名}/{アルバム名}/{曲名}.{ファイル形式}
のディレクトリ構成でアップロードしておきます。
3. Airsonic
本題である音楽ストリーミングサーバーのAirsonicをデプロイします。
Deployment
linuxserver/airsonic のDockerhubドキュメントに沿って実装していきます。
(公式のairsonic/airsonic イメージはメンテナンスされていないようでした)
sanbaでも用いた music PVCを /music
にマウントします。
またメタデータはコンテナ内の /config
に保存されるため、config PVCを /config
にマウントしてこれらのデータが永続化されるようにしておきます。
Airsonicは内蔵DB(HSQLDB)だけでなくPostgresやMySQLなどの外部DBにも対応しています。
外部DBをStatefulSetとしてk8s上に構築すればAirsonicをstatelessにできて良さそうですが、パフォーマンスに問題を抱えているため今回は見送りました。
airsonic-deployment.yaml▼
apiVersion: apps/v1
kind: Deployment
metadata:
name: airsonic
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: airsonic
template:
metadata:
labels:
app: airsonic
spec:
containers:
- name: airsonic
image: lscr.io/linuxserver/airsonic
env:
- name: TZ
value: Asia/Tokyo
- name: PUID
value: "1000"
- name: PGID
value: "1000"
ports:
- containerPort: 4040
protocol: TCP
resources:
limits:
cpu: "2"
memory: "2Gi"
livenessProbe:
httpGet:
path: /login
port: 4040
initialDelaySeconds: 60 # CrashLoopBackOffになる場合はこの値を大きくする
volumeMounts:
- name: config
mountPath: /config
- name: music
mountPath: /music
volumes:
- name: music
persistentVolumeClaim:
claimName: music
- name: config
persistentVolumeClaim:
claimName: config
Service
Sambaとは異なりServiceをクラスタ外に公開する必要は無いため、Headless Serviceとして実装します。
airsonic-svc.yaml▼
apiVersion: v1
kind: Service
metadata:
name: airsonic
namespace: default
spec:
selector:
app: airsonic
type: ClusterIP
clusterIP: None
ports:
- name: http
port: 4040
protocol: TCP
Nginx Ingress Controller
AirsonicはWebアプリケーションのため、L7ロードバランサーに相当するIngressを経由してServiceをクラスタ外に公開します。
事前準備としてオンプレミス環境用のIngress実装であるNginx Ingress Controllerを導入します。
導入手順は公式ドキュメントを参照してください。
(microk8sであれば sudo microk8s.enable ingress
で終わりです)
Ingress
クラスターエンドポイントIPへのhttpアクセスをairsonicのServiceに転送する設定でIngressをデプロイします。
https化は次のステップで実施します。
airsonic-ingress-http.yaml▼
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: airsonic-http
namespace: default
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: airsonic
port:
number: 4040
$ kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE airsonic-http public * 127.0.0.1 80 1m
LAN内PCのブラウザから http://{クラスターエンドポイントIP}
を開くとログイン画面が表示されます。
初期ID/パスワードはadmin/adminなのでログイン後すぐに変更します。
ログインするとsamba経由でアップロードした音楽を聴くことができるようになっています。
4. https化
LAN内からAirsonicを利用できるようになりました。
次は最後のステップとしてAirsonic Podへの接続をhttps化し、インターネットに公開します。
DNS設定
ルータのグローバルIPと自身の所有するドメイン(以降 example.com
と表記)を紐づけるAレコードをDNSに追加します。
筆者は無料のダイナミックDNSサービスを使っています。
ルータの設定
クラスターエンドポイントIP(192.168.0.XXX)の80番と443番ポートを開放します。
この時点で http://example.com としてAirsonicにアクセスできるようになります。
cert-manager
TLS証明書や発行者をk8sリソースとして管理可能にするcert-managerを導入します。
$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.6.1/cert-manager.yaml
Issuer
証明書発行者(Let's Encrypt)をk8sクラスタに追加します。
Let's Encryptの本番環境で試行錯誤しているとレート制限に引っかかる可能性があるため、検証用にステージング環境も追加します。
spec.acme.solvers.http01.ingress.class
の値は環境に応じて設定してください。
issuer.yaml▼
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: myaddress@example.com # UPDATE HERE
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: public # UPDATE HERE
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: myaddress@example.com # UPDATE HERE
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: public # UPDATE HERE
Ingressを更新
Ingressをhttps対応に書き換えます。先ほど作成したIngressを削除し、以下の設定で再作成します。
$ kubectl delete ingress airsonic-http
$ kubectl create -f airsonic-ingress.yaml
airsonic-ingress.yaml▼
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: airsonic
namespace: default
annotations:
cert-manager.io/issuer: "letsencrypt-staging"
spec:
tls:
- hosts:
- example.com
secretName: tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: airsonic
port:
number: 4040
Ingressを作成すると証明書リクエストが実施されます。リクエストは CertificateRequest としてk8sリソースになっています。
$ kubectl get cr NAME APPROVED DENIED READY ISSUER REQUESTOR AGE tls-xxxxx True True letsencrypt-staging system:serviceaccount:cert-manager:cert-manager 1m
APPROVED: True
になると証明書がREADYになります。
$ kubectl get cert NAME READY SECRET AGE tls True tls 1m
しばらく待っても証明書が取得できない場合は
kubectl describe cr
やkubectl describe order
で原因を調査します。
example.com:80
へのチャレンジによって認証されるため、ルーターの設定などが原因になりがちです。
Let's Encryptステージング環境で正しく動作する事を確認したら、issuerを本番に切り替えて再作成します。
... - cert-manager.io/issuer: "letsencrypt-staging" + cert-manager.io/issuer: "letsencrypt-prod" ...
$ kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE airsonic public example.com 127.0.0.1 80, 443 1m
証明書の期限が近づくと自動で更新されます。
Airsonic設定ファイルの編集
ChromeからAirsonic Web UIにhttpsでアクセスすると、一部要素がMixed Contentsでブロックされてしまいます。
AirsonicのPodに入り、X-Forwarded-For
を有効化する設定を追記してPodを再起動します。
$ kubectl exec -it airsonic-xxxxxxx bash # echo "server.use-forward-headers=true" >> /config/airsonic.properties # exit $ kubectl rollout restart deployments/airsonic
以上でインターネットから https://example.com でAirsonicに接続できるようになります。
クライアントアプリ
AirsonicはSubsonicというプロダクトからフォークされたもので、Subsonic APIに対応する様々なクライアントを利用することができます。
筆者はAndroidでは Ultrasonic
、iOSでは iSub
というアプリを利用しています。
おわりに
思いつきでやってみましたが実益とスキルアップを兼ねた遊びができ、今ではおうちKubernetesが実生活に不可欠なものとなってしまいました。
サンプルコードは最小限の実装になっていますので、実運用では環境に応じて冗長化やセキュリティ面の設定などを追加してください。