おうちKubernetesクラスタにImmichを入れてGoogle Photoを代替する

Google Photoは割と便利なのだが,容量無制限で始まったサービスにも関わらずいつの間にか15GBの容量制限ができていた.前はPixel 4aを使っていたので,その容量制限も無効化されていたんだけど,iPhoneに戻ってきたらきっちり容量制限されるし,なにかもっと容量のでかいサービスがほしくなった. でも金はあんまり払いたくない,しかし技術力は惜しまないぞ.

というわけでGoogle Photoの代替となるOSSをおうちKubernetesクラスタ上に構築しようと思う.

候補となるOSS

金を払う気がないのでSaaSサービスは対象外にする.無料で大容量を謳っていても,それはGoogle Photoと同じ道を辿るだけだ.というわけでOSSを探した.

PhotoPrismの見た目はすごく良かったのだが,公式でiOS/Androidアプリが存在しないので,モバイルから使うのに不便そう.特にこの種のアプリの場合,写真はたいていスマホで撮るのでモバイルからのアップロードまわりのUXは非常に重要になる. Webブラウザ経由で,とかは嫌だしね.

PiwigoはPHP自宅サーバで動かす気がなかったので却下.

enteはWebもモバイルアプリもあるし,実装はGoでかなりよかったのだけれど,SaaS版はともかくOSS版をどうやって手元で起動するのかドキュメントがあまり見つからなかった.まぁビジネスとしてやっていて,SaaS版を提供している以上そっちを使ってほしいのだろうけど.

というわけでImmichを入れた.機械学習系のマイクロサービスが付属していたりして,若干不要な機能もあるのだが,求めている要件には合致した.

Hemlで入れる

Immichのhelm chartはちょっとむずかしい.

github.com

普通ならvaluesやREADMEに設定できる値が全部書いてあるだろうところ,あんまり書いてない.

https://github.com/immich-app/immich-charts/blob/main/charts/immich/values.yaml

かといってtemplate側を見に行ってもdeployment等の定義が書いてなかったりするので,何を設定すべきなのかわからない. 実はこいつはライブラリとして別のhelm chartを参照している.それも最新版ではなくちょっと古いやつ.

https://github.com/bjw-s/helm-charts/tree/923ef40a39520979c98f354ea23963ee54f54433/charts/library/common

これをライブラリとして流用しているので,こいつのREADMEにあるvaluesが全てそのまま使えるようになっている.これでannotationの設定とかまでhelmから仕込めるようになる.

というわけで出来上がりがこれ.

github.com

面倒なのはDBで,こいつはPostgreSQLを使うのだけれど,pgvecto.rsを使っている.

github.com

そのため,普通のpsqlのhelm chartでは使えない.まぁbitnamiのpostgresqlのhelm chartにimageとしてtensorchord/pgvecto-rsを渡せば割とそのまま使えるのだが.

    primary:
      initdb:
        scripts:
          create-extensions.sql: |
            CREATE EXTENSION cube;
            CREATE EXTENSION earthdistance;
            CREATE EXTENSION vectors;

一応extensionを作っておく必要はある.

ちなみにstorageは全部longhorn

h3poteto.hatenablog.com

できあがり

スマホアプリは初回起動時にドメイン選択画面が表示される.LAN内であればドメイン指定でTLS接続できるようにしてあるので

h3poteto.hatenablog.com

これでそのままつながる.

Immichの公式ページにも書いてあるが,くれぐれもこれを唯一のストレージにしてはいけない.まだProduction Readyではないので,かならずバックアップを用意しておこう.

期待していたとおりだけれど,Immichのアプリの出来は割と良い.

  • バックアップのON/OFF切り替え
  • WiFi接続時のみ自動バックアップを有効化

あたりは普通にできる.

おうちKubernetesにCertManagerを入れてTLS接続できるようにする

宅内LANで動かしているKubernetesクラスタ上のサービスについて,MetalLBingress-nginxを使ってアクセスできるようにしていたんだけど,TLSでアクセスしたくなった.別にLAN内なのでそこまで気にする必要はないんだけど,アプリケーションとしてhttpsを前提にしているものもあるわけで,そういうものを無理やりhttpでアクセスするのは不便だ.

MetalLB + ingress-nginx

そもそもベースになるのはこの構成.まずはMetalLBを入れて,Service type LoadBalancerに任意のIPを振れるようにしておく.自宅のLANのIPアドレスレンジが /24 くらいあるのであれば,例えば 192.168.0.0 - 192.168.0.255 くらいのIPが使えるはずで,適当に使わないやつをServiceに割り振っておけば,LAN内からアクセスできる状態になる.

次に,ingress-nginxを入れるのだが,ingress-nginxが作るServiceをtype LoadBalancerにしておく.そうすることで,このServiceにIPが割り当てられる.するとこのIPにHost付きでアクセスすれば,あとはingress-nginxはHostヘッダーに従って必要なingress -> service -> podにリクエストを流してくれる.

ただし,この方式でやるなら,Hostヘッダー指定が必須になるので /etc/hosts に書き込んでおく必要がある.

Route53を/etc/hostsの代わりに使う

/etc/hosts をいじるのが面倒なので,こいつをRoute53にまかせてみるという案を思いついた.Route53はただのDNSなので,別にローカルのIPを返答していても問題ない.家の外からアクセスできないだけだ.

というわけでexternal-dnsを入れる.これが入った状態でIngressを作れば,そのIP(つまりはingress-nginxのService type LoadBalancerのIPだが)がDNSに登録される.これでLAN内であれば /etc/hosts の変更をしなくても各種サービスにドメインでアクセスできるようになる.

CertManagerで証明書を得る

ここまでやって気づいた.Route53をDNSとして使えるなら,Let's EncryptのDNS-01 challengeが成功するんじゃないか?HTTP-01 challengeは外部サーバからサービスにアクセスできる必要があるけど,DNSは所有権のみを確認するだけなので,サービス自体が外部公開されていなくても成功する.

letsencrypt.org

というわけでCertManagerをインストールして,

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-issuer
spec:
  acme:
    privateKeySecretRef:
      name: letsencrypt-privatekey
    server: https://acme-v02.api.letsencrypt.org/directory
    solvers:
    - selector:
        dnsZones:
          - "home-cluster.mydomain"
      dns01:
        route53:
          region: ap-northeast-1
          accessKeyIDSecretRef:
            name: aws-secret
            key: AWS_ACCESS_KEY_ID
          secretAccessKeySecretRef:
            name: aws-secret
            key: AWS_SECRET_ACCESS_KEY

ClusterIssurerを作って,

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-issuer
  name: my-service
spec:
  ingressClassName: nginx
  rules:
  - host: my-service.home-cluster.mydomain
    http:
      paths:
      - backend:
          service:
            name: my-service
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - my-service.home-cluster.mydomain
    secretName: my-service-tls-certificate

Ingressにcert-managerの設定をちょっと追加すれば,無事証明書が取得できた.これで宅内LANであってもTLSでアクセスできるようになる.

DHT20で湿度計測してprometheus exporterで出力しGrafanaで可視化する

部屋の湿度を測りたくてDHT20を買った.

akizukidenshi.com

さて,こいつで計測した湿度を,最終的にはGrafanaで見られるようにしたい.

アーキテクチャを考える

DHT20はただのセンサーであって,こいつの値を送信・受信するためには別のサーバが必要になる.ただ,センサーから定期的に湿度を取って送るだけなら,大したCPUは必要ないので,Raspberry Pi Zero WHを買った.

akizukidenshi.com

こいつにDHT20を接続してPi Zeroで値を取る.で,Pi Zero上でPrometheus Exporterを動かしておいて,Kubernetes上のPrometheusから取りに行くのはどうだろうか.そうすれば,Prometheusでデータが取れたあとはすべてサーバ側での処理になる.

とりあえず湿度を取れる状態にする

DHT20にGroveコネクタが付属しているので,

https://akizukidenshi.com/goodsaffix/DHT20.pdf

どこに接続するかはデータシートを見る.

Raspberry PiはまぁいつもどおりにOSをセットアップしておく.

で,プログラムをどうするかという問題までくる.DHT20はI2Cというインターフェースを使うが,これを扱うものとして例示されているのはPythonが多い.しかし,Raspberry Pi Zero WH上でPythonを動かしたくない. さらに,Pi Zeroは

https://elinux.org/RPi_Hardware

これによると32bitのARMv6のCPUである.つまりx86_64上でコンパイルするのがめんどくさい.こういときはGoに限る.

というわけで,巷に流れるPythonやCのプログラムを参考にして,I2CをGoで扱ってみる.

package main

import (
    "log"
    "math"
    "time"

    "github.com/d2r2/go-i2c"
)

func main() {
    i2c, err := i2c.NewI2C(0x38, 1)
    if err != nil {
        log.Fatal(err)
    }
    // Free I2C connection on exit
    defer i2c.Close()

    // Need to wait 100ms after launched
    time.Sleep(100 * time.Millisecond)
    var initial byte = 0x71
    ret, err := i2c.ReadRegU8(initial)
    if err != nil {
        log.Fatal(err)
    }
    // ret should be 28(0x1c)
    log.Printf("init code %d", ret)

    time.Sleep(10 * time.Millisecond)
    // Start measure
    _, err = i2c.WriteBytes([]byte{0x00, 0xAC, 0x33, 0x00})
    if err != nil {
        log.Fatal(err)
    }
    // Need to wait after sending ac3300
    time.Sleep(80 * time.Millisecond)
    dat := make([]byte, 7)
    _, err = i2c.ReadBytes(dat)
    if err != nil {
        log.Fatal(err)
    }
    // byte is uint8 (8bits), it is not enough to shift
    // So cast []uint8 to []uint32
    var long_dat []uint32
    for _, d := range dat {
        long_dat = append(long_dat, uint32(d))
    }

    // Get humidity and tempreature data
    hum := long_dat[1]<<12 | long_dat[2]<<4 | ((long_dat[3] & 0xF0) >> 4)
    tmp := ((long_dat[3] & 0x0F) << 16) | long_dat[4]<<8 | long_dat[5]

    // Calcurate real data
    real_hum := float64(hum) / math.Pow(2, 20) * 100
    real_tmp := float64(tmp) / math.Pow(2, 20)*200 - 50

    log.Printf("hum: %f", real_hum)
    log.Printf("tmp: %f", real_tmp)
}

データシートに一応書いてあるんだけど,かなりわかりにくかった.まず,I2Cで新規接続するときには 0x38 を指定する.ここがI2Cデバイスのアドレスになっている.そして初期化は 0x71 を送る.そうすると初期化コードが帰ってくるのだが,ここは正直全然わからん.

If the status word and 0x18 are not equal to 0x18, initialize the 0x1B, 0x1C, 0x1E registers, details Please refer to our official website routine for the initialization process; if they are equal, proceed to the next step

なので, 0x18 が正常なのだが,たまに 0x1c が帰ってくることがある.これが何を意味していて何を必要としているのか,どこを見てもわからんかった.ちなみに 0x1c でもデータは普通に取れてた.

で,測定開始は 0xac, 0x33, 0x00 を送る.

湿度データが入っているのが,2, 3, 4バイトめの上位4ビット.温度データが4バイトめの下位4ビットと5バイト,6バイトめ.ビットシフトするとuint8だとビット数が全然足らないのでuint32にデータを格納している.

このプログラムを

$ GOOS=linux GOARCH=arm GOARM=6 go build

すると,Pi Zeroで動くバイナリが生成できるので,こいつをデバイスに送信して動かす.

Prometheus exporterにする

github.com

github.com/prometheus/client_golang を使えばだいたいかんたんに作れる.これで30秒おきに上記のプログラムを実行して取れたデータをPrometheus exporterに乗せておく.

再起動の管理とかログの管理とかlogrotateとかがめんどくさいので,こいつをsystemdで動かす.

[Unit]
Description=Humidity and Tempreature exporter for Prometheus
After=network.target

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/prometheus-humidity-exporter
ExecStart=/usr/bin/prometheus-humidity-exporter
Restart=always
RestartSec=10
Environment=LANG=en_US.UTF-8
SyslogIdentifier=prometheus-humidity-exporter
RemainAfterExit=no

[Install]
WantedBy=multi-user.target

で,あとはPi ZeroのIPをターゲットにして,Kubernetesクラスタ上のPrometheusから値を読ませる.そしたらもうGrafanaで見えるようになる.

ふはははは.

参考

craft-gogo.com

s-design-tokyo.com

hatakekara.com

RDS Aurora MySQLのPerformance InsightsによるPerformance Schema自動管理はt4g.mediumだけ特殊な動きをするので気をつけろ

言いたいことはタイトルの通り.

まずRDSにはPerformance Ingishtsというものがある.これは今までのRDSのMonitoringに加えて,もっと詳細なクエリ等の情報までが見える分析ツールである.

www.youtube.com

これは AWS RDSによって提供されている機能である

対して,Performance Schemaというのは MySQLで提供されている機能でありMySQLサーバのモニタリングのための機能である. 通常のMySQLサーバであれば,管理者が

[mysqld]
performance_schema=ON

こういう設定を書くことで有効化できる. 名前が似ていてややこしいので,performance_schema と書くことにする.

さて,こいつをAWS RDSでどうやって有効化するのかという話である.

デフォルトではperformance_schemaはPerformance Insightsによって自動管理される

RDS MySQLの設定は,(Cluster|Instance)ParameterGroupによって設定されている. ちなみに,ClusterParameterGroupとInstanceParameterGroupどちらが優先されるかについては以下の記事が詳しい.

dev.classmethod.jp

で,ややこしいのだがParameterGroupをいじってない状態だと,performance_schemaは0 つまり無効化されているように見える.

しかし,こちらのドキュメントでも触れられている通り,performance_schemaの項目がデフォルト値である場合は,Performance Insightsによる自動管理状態になっている.

docs.aws.amazon.com

この場合は,単にPerformance Insightsを有効化してインスタンスを再起動すれば,performance_schemaは有効化される.

dev.classmethod.jp

なお,ここを変更した時点でPerformance Insightsの自動管理から外れるので,1にすると自動管理されなくなるのである.ややこしいわ.

ただしt4g.mediumだけは自動管理対象外になる

https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_PerfInsights.EnableMySQL.html#USER_PerfInsights.EnableMySQL.options

Automatic management of the Performance Schema isn't supported for the t4g.medium instance class.

t4gファミリーとかではなく,なぜかt4g.mediumだけ.一部のインスタンスでこのインスタンスタイプを使っているやつだけハマった.

この場合どういう挙動になるのかを書いていく.

まず,このt4g.mediumでperformance_schemaを有効化するためには,ParameterGroupによる手動管理設定が要る.

performance_schema: 1
performance-schema-consumer-events-waits-current: ON
performance-schema-instrument: wait/%=ON
performance_schema_consumer_global_instrumentation: 1
performance_schema_consumer_thread_instrumentation: 1

というようにParameterGroupを設定しておく.

この状態で再起動する……前に確認すべきなのはPerformance Insightsである.

t4g.mediumはPerformance Insightsによるperformance_schema自動管理外になるので,一見無関係に思えるがそうでもなかった

  1. t4g.mediumをPerformance Insights有効にして再起動すると,どう頑張ってもperformance_schemaは無効化される
  2. t4g.mediumでperformance_schemaを有効化するためには,一度Performance Insightsを無効化して再起動する必要がある
  3. Performance Insightsはインスタンスの再起動無しで有効化できるので,これをやったあとに再度Performance Insightsを有効化する必要がある

面倒すぎるだろう. まじでなぜt4g.mediumだけこうなった?smallとかlargeではこうならないんでしょ?

これは勘だけれど,Performance Insightsが有効な時点でperformance_schema自動管理を動かそうとしているんじゃないだろうか.t4g.mediumなので,その判定で除外されて,以降performance_schemaの設定が確認されないまま起動しているように見える.Performance Insightsが無効化されていれば,そこの確認をスキップしてParameterGroupの設定どおりにperformance_schemaを設定しているように見える.

いや,まじでなんでt4g.mediumだけこうなったのか謎すぎるわ. というかperformance_schemaなしでもPerformance Insightsは使えるわけだし,なんでここを自動管理にしたのか意味不明.普通にParameterGroupの設定どおりに起動してくれたほうが全然明示的じゃん.こんな暗黙的な自動管理をやらないでほしい.

ゼンハイザーHD600を買った

持っていたヘッドホンのヘッドバンド部分,合皮でできている部分が経年劣化でボロボロになってきた.イヤーパッドは結構替えパーツが売っていることが多いのだが,ヘッドバンド部分はどうにもならなくて,結局こうなると捨てるしか無い.

というわけで新しいヘッドホンを買おうと思ってゼンハイザーを探した.今までは密閉型だったのだが,どうにも開放型が欲しくなったので,開放型といえば老舗のゼンハイザーだ.

いくつか視聴してみて,ハイエンド機は今の俺には不要だろう,という結論に至った.残った候補は

  • HD599
  • HD560S
  • HD600

このうち,HD599はヘッドバンドが同じく合皮でできているので,その寿命くらいしか持たないだろうと予想できる.この理由のため,最初からHD599SEも除外した.それに対してHD560Sはベロアのヘッドバンドなので,この点は少しマシなんじゃないかと思っている.また,HD600以上のシリーズになればベロア素材のヘッドバンドの替えパーツが売られているので,ヘタったとしても交換することができる.

となると候補は

  • HD560S
  • HD600

になった.HD650やHD660S2までは手が出ない.

というわけでこの2つを念入りに聴き比べた. ちなみに,デスクトップPCで使う.ゲームはほとんどやらない.音楽を聞くかアニメを見るかのどちらか.

聞く音楽はほぼクラシック.ピアノソロ,ピアノ協奏曲多め.

聴き比べ

第一印象はHD560Sの方が良かった.まず高音の響きが良い.今まで音として鳴っていたが目立っていなかった高音が非常によく響いていて聞こえやすい.全体的にスッキリ全部の音が聞こえる感じがしていて,これは流石に新しいだけはある.また,開放型だけれど低音もしっかり聞こえていて良い. インピーダンス120Ωだけど,スマホにつないでもそこまで音量調節せずにしっかり聞こえている.

これに対し流石にHD600はインピーダンス300Ωだけあって,スマホの音量は最大にしないと聞こえない.それでも音量不足は否めない.これはアンプを通していないから当然のことではあるが.また,音のスッキリ感はHD560Sに一歩劣る.全体的に柔らかくなっている代わりに,スッキリした感じは一段劣る.また,低音はHD560Sよりちょっと軽い感じがする.

ちなみに最終的にMac Book Proと,それにつないだFiio E10Kを持って視聴に行った.

で,一番の問題点は響き方の違いだった.オーケストラ曲だとそこまで違和感はなかったのだが,ピアノソロを聞いてみると一目瞭然の差がある. まず,HD560Sは先程書いたように高音の響きが非常に良い.まるでホールに響いて消えていく残響音が強調されているかのような聞こえ方をする.しかし,これが高音だけなのだ.中音域になるといきなりその響きがなくなる.低音域も同じく.また,音がスッキリ聞こえるのも相まって,中音域のアクセント,sfが非常に強く聞こえる.ベートヴェンのピアノソナタ17番,テンペストの第3楽章みたいに使う音域が真ん中あたりに寄っていればそこまでこれは気にならない.しかし,ショパンスケルツォとか聞くと違和感の塊になる.高音域はホールに響いている音を聞いているようで,中音域になるといきなり目の前で弾いているような音になる.聞いてる音じゃない,弾いてる音になる.本来,こういうときに聞く音源というのは録音された音源であるし,それを聞いているのだから弾いている音になるのはあまりいいことじゃない.

これに対して,HD600は全体的に整っている.高音域はHD560Sより伸びないけど,全体的な響きが統一されていて,音の距離感が変わらない.これだけでかなり違和感がなくなる.あと,アクセント,sfも柔らかくなっているので,こちらのほうが聞きやすい.HD560Sみたいな,目の前で弾いているような音はせずに,全体的にちゃんとホールで聞くような音になっている.

ちなみに店にあるアンプを借りたところ,更に良くなることはわかった.HD560S -> HD600に変えたときの低音の軽さも,アンプによりかなりしっかりと重さがのるようになっている.ただ,値段はそれなりに上がっていくので,値段相応かは人による.

というわけでHD600を買った.

ちなみに公式で買うのが一番安かった.

www.sennheiser-hearing.com

公式で44000円.いくつか店舗を回ったけれど,だいたいHD600は54000円以上で売られていることが多かった.HD560Sは公式と変わらず,

www.sennheiser-hearing.com

むしろ27000円を切るくらいの値段で売っていることもあった.何が違うのかわからないけど,公式で買った.特に問題はない.

視聴に行った場所

だいたいここで全部聞けるし,視聴ブースもあるのでいろいろ試しやすかった.

https://www.e-earphone.jp/

ヨドバシカメラにも行ったのだが,ヨドバシ,お前はダメだ. ノイズキャンセリングヘッドホンを買うのであればよいだろう.しかしオープンエアーのヘッドホンを買う場所じゃない. ところ構わず流れる「まあるい緑の山手線♪」,それが終わったと思えば「ただいま4階にて……」とひたすら売出しセールの呼び込み,そして背後には自動演奏の電子ピアノ.ふざけているとしか思えない. こんな環境なのでeイヤホンと違って誰もヘッドホンの視聴なんかしてないじゃないか.