Mastodonのオブジェクトストレージとして利用してみるやつ。

Wasabiは2023年春ごろからPublic accessが原則使えなくなっています。詳細は以下参照。

Wasabiのパブリックアクセスが使えなくなった件(対応の仕方)

上記の@noellaboさんの記事では、nginxとlua(openresty)を用いた対応例が示されていますが、今回は別のアプローチとして表題の通りHAProxyを用いたゲートウェイを作ります。

https://github.com/jouve/haproxy-s3-gateway/blob/main/haproxy/haproxy.cfg

上記の「s3v4」と「upstream」をほぼそのまま利用します。set-var-fmtがHAProxy 2.5以降の実装であるため、標準で新し目のバージョンが入らないOSをお使いの人は適宜対応してください。

万が一アクセスキーなどが漏れても参照しかできないように、必要最低限のキーをWasabiで作成してそちらを利用します。

また、WasabiのEndpointのIPが変わっても動作するよう、resolverの設定を追加します。HAProxyはresolver設定を行わないと、プロセス起動時に名前解決を行ったきりそれ以降DNSへの問い合わせを行わない、という挙動を示します。(2.0等といった昔の版は確実に。最近のはちゃんと調べていないです。)

今回、Proxyを動作させるOSは既存弊Mastodonサーバと合わせてUbuntuとしたので、以下を参考にppaを追加します。
https://haproxy.debian.net/

また、上記の設定例だとアットマークを含む画像(サーバのaboutページなどに使われている)がうまく表示されないため、以下のようなFrontendを追加し、そちらを443待ち受けで使用するように変更。

また、backendも追加し、redispatchオプション+retry-onオプションによりオブジェクトストレージ側で404やリトライ可能エラーが出た場合に旧サーバ(非オブジェクトストレージ)の参照やエラー画像を返すサーバを指定しています。(旧サーバについては、ファイル移行中のフォールバック目的です。ファイル移動が終わったら削除可能です。 ※2024/2/16追記記事を公開したのでリンク)

frontend fr-1<br>  bind :::443 v4v6 ssl crt <pemファイルなど> alpn h2,http/1.1<br>  http-request set-path %[path,regsub(@,%40,g)]<br>  http-request set-var(txn.path) path<br>  acl ismedia var(txn.path) -i -m end png<br>  acl ismedia var(txn.path) -i -m end jpg<br>  acl ismedia var(txn.path) -i -m end jpeg<br>  acl ismedia var(txn.path) -i -m end bmp<br>  acl ismedia var(txn.path) -i -m end gif<br>  acl ismedia var(txn.path) -i -m end jfif<br>  acl ismedia var(txn.path) -i -m end webp<br>  acl ismedia var(txn.path) -i -m end mp4<br>  acl ismedia var(txn.path) -i -m end avi<br>  acl ismedia var(txn.path) -i -m end mpg<br>  acl ismedia var(txn.path) -i -m end mov<br>  acl ismedia var(txn.path) -i -m end mkv<br>  acl ismedia var(txn.path) -i -m end flv<br>  acl ismedia var(txn.path) -i -m end webm<br>  acl ismedia var(txn.path) -i -m end m4v<br><br>  http-response del-header X-Amz-Id-2<br>  http-response del-header X-Amz-Request-Id<br>  http-response del-header X-Wasabi-Cm-Reference-Id<br><br>  http-response add-header Access-Control-Allow-Origin * if ismedia<br>  http-response add-header X-Nsd-IsMedia Yes if ismedia<br>  http-response add-header X-Nsd-IsMedia No if !ismedia<br><br>  default_backend bk-1<br><br>backend bk-1<br>  balance first<br>  option redispatch<br>  retry-on 404 all-retryable-errors<br>  server listen-wasabi localhost:8081 check<br>  server listen-origin-cache localhost:8082 check<br>  server listen-error-bk localhost:8083 check

Mastodon側では以下のように.env.production(など)へWasabiのEndpointや書き込み可能なAccessKey、参照用FQDNの指定などを行います。

S3_ENABLED=true<br>S3_BUCKET=<バケット名><br>AWS_ACCESS_KEY_ID=<アクセスキー><br>AWS_SECRET_ACCESS_KEY=<Secret><br>S3_ENDPOINT=https://s3.ap-northeast-1.wasabisys.com<br>S3_ALIAS_HOST=obj1.oyasumi.dev

うまく行けば上記のように、Wasabi上の画像が取得できるはずです。

参照用FQDNについては、Cloudflareのキャッシュ機能の対象にするなどして、回線に負荷がかからないようにするとより良いでしょう。

最後に、用いている設定を以下に示します。(参考)
尚、実運用ではもう少しダーティなことを行っています。※改善点とかあればこっそり教えて下さい(^o^)

global<br>  log /dev/log  local0<br>  chroot /var/lib/haproxy<br>  user haproxy<br>  group haproxy<br>  daemon<br><br>  # Default SSL material locations<br>  ca-base /etc/ssl/certs<br>  crt-base /etc/ssl/private<br>  <br>  # generated 2024-01-15, Mozilla Guideline v5.7, HAProxy 2.8.5, OpenSSL 1.1.1w, intermediate configuration, no HSTS, no OCSP<br>  # https://ssl-config.mozilla.org/#server=haproxy&version=2.8.5&config=intermediate&openssl=1.1.1w&hsts=false&ocsp=false&guideline=5.7<br>  # intermediate configuration<br>  ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305<br>  ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256<br>  ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets<br><br>  ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305<br>  ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256<br>  ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets<br><br>  # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /etc/haproxy/ffdhe2048.txt<br>  ssl-dh-param-file /etc/haproxy/ffdhe2048.txt<br><br>defaults<br>  log   global<br>  mode  http<br>  option        httplog<br>  option        dontlognull<br>  timeout connect 5000<br>  timeout client  50000<br>  timeout server  50000<br>  errorfile 400 /etc/haproxy/errors/400.http<br>  errorfile 403 /etc/haproxy/errors/403.http<br>  errorfile 408 /etc/haproxy/errors/408.http<br>  errorfile 500 /etc/haproxy/errors/500.http<br>  errorfile 502 /etc/haproxy/errors/502.http<br>  errorfile 503 /etc/haproxy/errors/503.http<br>  errorfile 504 /etc/haproxy/errors/504.http<br><br>frontend fr-1<br>  bind :::443 v4v6 ssl crt <pemファイルなど> alpn h2,http/1.1<br>  http-request set-path %[path,regsub(@,%40,g)]<br>  http-request set-var(txn.path) path<br>  acl ismedia var(txn.path) -i -m end png<br>  acl ismedia var(txn.path) -i -m end jpg<br>  acl ismedia var(txn.path) -i -m end jpeg<br>  acl ismedia var(txn.path) -i -m end bmp<br>  acl ismedia var(txn.path) -i -m end gif<br>  acl ismedia var(txn.path) -i -m end jfif<br>  acl ismedia var(txn.path) -i -m end webp<br>  acl ismedia var(txn.path) -i -m end mp4<br>  acl ismedia var(txn.path) -i -m end avi<br>  acl ismedia var(txn.path) -i -m end mpg<br>  acl ismedia var(txn.path) -i -m end mov<br>  acl ismedia var(txn.path) -i -m end mkv<br>  acl ismedia var(txn.path) -i -m end flv<br>  acl ismedia var(txn.path) -i -m end webm<br>  acl ismedia var(txn.path) -i -m end m4v<br><br>  http-response del-header X-Amz-Id-2<br>  http-response del-header X-Amz-Request-Id<br>  http-response del-header X-Wasabi-Cm-Reference-Id<br><br>  http-response add-header Access-Control-Allow-Origin * if ismedia<br>  http-response add-header X-Nsd-IsMedia Yes if ismedia<br>  http-response add-header X-Nsd-IsMedia No if !ismedia<br><br>  default_backend bk-1<br><br>backend bk-1<br>  balance first<br>  option redispatch<br>  retry-on 404 all-retryable-errors<br>  server listen-wasabi localhost:8081 check<br>  server listen-origin-media localhost:8082 check<br>  server listen-error-srv localhost:8083 check<br><br>listen s3v4<br>  bind :::8081 v4v6<br>  http-request set-var(req.region) str(ap-northeast-1)<br>  http-request set-var(req.endpoint) str(s3.ap-northeast-1.wasabisys.com:443)<br>  http-request set-var(req.bucket) str(<ばけっと>)<br>  http-request set-var(req.access_key) str(<あくせすきー>)<br>  http-request set-var(req.secret_key) str(<Secret>)<br><br>  http-request set-path /%[var(req.bucket)]%[path]<br><br>  http-request set-var(req.date) date<br>  http-request set-var(req.x_amz_date) var(req.date),utime("%Y%m%dT%H%M%SZ")<br>  http-request set-var(req.today) var(req.date),utime("%Y%m%d")<br><br>  http-request set-header Host %[var(req.endpoint)]<br>  http-request set-header x-amz-date %[var(req.x_amz_date)]<br>  http-request set-var(req.x_amz_content_sha256) str(e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855) if { req.body_size eq 0 }<br>  http-request set-var(req.x_amz_content_sha256) var(req.body),sha2(256),hex,lower if { req.body_size gt 0 }<br>  http-request set-header x-amz-content-sha256 %[var(req.x_amz_content_sha256)]<br>  http-request set-var-fmt(req.scope) %[var(req.today)]/%[var(req.region)]/s3/aws4_request<br>  http-request set-var-fmt(req.canonical_request) %[method]\n%[path]\n\nhost:%[var(req.endpoint)]\nx-amz-content-sha256:%[var(req.x_amz_content_sha256)]\nx-amz-date:%[var(req.x_amz_date)]\n\nhost;x-amz-content-sha256;x-amz-date\n%[var(req.x_amz_content_sha256)]<br>  http-request set-var-fmt(req.string_to_sign) AWS4-HMAC-SHA256\n%[var(req.x_amz_date)]\n%[var(req.scope)]\n%[var(req.canonical_request),sha2(256),hex,lower]<br>  http-request set-var(req.key) str(),concat(AWS4,req.secret_key),base64<br>  http-request set-var(req.date_key) var(req.today),hmac("sha256",req.key),base64<br>  http-request set-var(req.date_region_key) var(req.region),hmac("sha256",req.date_key),base64<br>  http-request set-var(req.date_region_service_key) str(s3),hmac("sha256",req.date_region_key),base64<br>  http-request set-var(req.signing_key) str(aws4_request),hmac("sha256",req.date_region_service_key),base64<br>  http-request set-var(req.signature) var(req.string_to_sign),hmac("sha256",req.signing_key),hex,lower<br><br>  http-request set-header Authorization "AWS4-HMAC-SHA256 Credential=%[var(req.access_key)]/%[var(req.scope)],SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=%[var(req.signature)]"<br><br>  server local localhost:8084 check<br><br>listen upstream<br>  bind :::8084 v4v6<br>  capture request header Authorization len 1000<br>  capture request header Date len 1000<br>  capture request header X-Amz-Content-Sha256 len 1000<br>  capture request header X-Amz-Date len 1000<br>  server wasabi-upstream s3.ap-northeast-1.wasabisys.com:443 ssl verify none resolvers dns-resolver alpn h2,http/1.1 check<br><br>resolvers dns-resolver<br>  nameserver ns1 1.1.1.1:53<br>  nameserver ns2 8.8.8.8:53<br><br>listen origin-media<br>  bind :::8082 v4v6<br>  http-request set-header Client-Source-IP %[req.hdr_ip(X-Forwarded-For,1)]<br>  http-request set-header X-Forwarded-Proto https<br>  http-request set-header host <丼さーば><br>  http-request set-path /system%[path]<br>  server mastodon-direct <丼さーば>:443 check ssl verify none resolvers dns-resolver alpn h2,http/1.1<br><br>listen error-srv<br>  bind :::8083 v4v6<br>  http-request set-path /img/404.png if { path -i -m end png }<br>  http-request set-path /img/404.jpg if { path -i -m end jpg }<br>  http-request set-path /img/404.gif if { path -i -m end gif }<br>  http-request set-path /img/404.jpg if { path -i -m end jfif }<br>  http-request set-path /img/404.jpg if { path -i -m end jpeg }<br>  http-request set-path /img/404.webp if { path -i -m end webp }<br>  http-response set-status 503<br>  server http-server <エラー応答用サーバ>:80 check