7 ポイント 投稿者 GN⁺ 2023-07-17 | 1件のコメント | WhatsAppで共有
  • 個人サービスは DigitalOcean $5/月 VM と Debian 10 上で直接運用しており、小規模なサービスであれば VM にサーバーソフトウェアを載せる方式でも十分にシンプルで合理的である
  • サーバーアプリは Rust の静的バイナリ で作成し、HTML、CSS、設定、シークレットまで含めてデプロイ成果物を単一ファイルに保っている
  • 実行管理は systemd、HTTPS 処理は nginx のリバースプロキシと Let’s Encrypt/certbot に任せ、アプリが TLS を直接扱わないようにしている
  • データが必要なサービスは SQLite の単一ファイル を使い、Tarsnap の日次バックアップと Litestream の DigitalOcean Spaces へのストリーミングバックアップを併用している
  • 1 台の VM に複数サービスを置くときは サービスごとの Unix ユーザー で分離し、より強いセキュリティが必要なら別 VM や systemd-nspawn、firejail を選ぶ

サーバーとデプロイの基本構成

  • thoughts.page、hanabi.site、cgmserver、phonebridge など複数のサービスがこの構成に近い形で運用されている
  • アプリは DigitalOcean VM で動作し、料金プランは $5/月 ティア、OS は Debian 10 である
    • 一部のサービスは同じ VM を共有し、一部は別の VM に分離している
  • サーバーソフトウェアは Rust で書かれている
    • 静的リンクされている
    • HTML、CSS、設定、シークレットなどがバイナリにコンパイルされる
    • rust-musl-builderrust-embed を使用している
  • この方式では、デプロイはサーバーに 単一ファイル をコピーする作業に単純化される
    • Go、C++ などでも同様の方式を使えると考えている
    • 単一バイナリ配布が難しい言語なら、Docker コンテナをビルド成果物にする代替案がある

実行、プロキシ、データ保全

  • サービス起動の管理は systemd が担当する
    • サーバー起動時にバイナリも一緒に実行されるようにする
    • systemd unit ファイルの大半は 9行のシンプルなファイル である
    • systemd 自体は複雑だが、起動時にサーバーを立ち上げる用途ではその複雑さの大半に触れずに済む
  • デプロイは simple deploy script で処理している
    • バイナリをサーバーへコピーしてサーバーを再起動する
    • ロールバックが可能で、デプロイ中に接続が切れても常に有効なバージョンが動作するようになっている
  • データベースが必要なプログラムは SQLite を使う
    • アプリの状態全体が 単一ファイル に入る
    • 毎日 SQLite .backup コマンド でバックアップを作成し、Tarsnap に保存する
    • バックアップスクリプトは cron で実行される
    • Litestream でデータベースのコピーを秒単位で DigitalOcean Spaces ストレージへストリーミングし、スナップショットは 6 時間ごとに作成する
  • すべてのサーバーは nginx リバースプロキシ の背後で動作する
    • nginx が TLS 終端を処理するため、アプリは HTTPS を気にしなくてよい
    • HTTPS 証明書は Let’s Encryptcertbot で取得し、自動更新される
    • hanabi.site の nginx 設定例は 別の gist にある
    • 静的ファイルは nginx で配信でき、scprsync でサーバーにコピーすればよい

メンテナンスとサービス分離

  • この構成はシンプルで 堅牢な運用経路 を志向している
    • アプリ自体を除いた配信経路のソフトウェアは、何十年も使われてきた実績ある構成要素である
    • DigitalOcean の料金を払い続ける限り、サイト運営に本質的なメンテナンスはほとんどない
    • 監視が検知した問題は、一時的な DigitalOcean のネットワーク障害だけだった
  • OS とセキュリティアップデートは時々必要になる
    • Debian リリースは 5 年のサポートを提供するため、約 2 年半後には Debian 11 へアップグレードする必要がある
    • Heartbleed のような事案が再び起こればパッチが必要になる
    • こうした事案はまれである
  • サービスごとに $5/月 VM を使うコストは負担になりうるが、複数サービスを同じ VM 上で動かすこともできる
    • 分離はサービスごとに 別個の Unix ユーザーアカウント を作ることで実現する
    • この分離方式は Unix の初期から存在する方法なので堅牢に見える
    • より強い分離が必要なら systemd-nspawnfirejail を使える
    • 本当にセキュリティ上重要なら、追加で $5/月 を払って別の VM で実行する
  • 新しいプロジェクトのセットアップ手順は短い
    • 新しいユーザーを作成
    • nginx 仮想ホストを追加し、certbot で HTTPS 証明書を発行
    • systemd unit を追加
    • リポジトリに deploy script をコミットして実行
  • 最初は学ぶことが多いが、このインフラは クラウドインフラよりはるかにゆっくり変化する
    • nginx の設定形式は過去 10 年間、本質的にほとんど変わっていない
    • Debian システム管理における最後の大きな変化は、ほぼ 10 年前の systemd への移行だった
    • クラウドプロバイダーがサービスを廃止したり、動作をひそかに変えたりする状況にさらされにくい
    • 依存先は VPS プロバイダーだけであり、サーバーは汎用品なので他のプロバイダーへ移行できる

1件のコメント

 
GN⁺ 2023-07-17
Hacker Newsの意見
  • 分離のために各サービスを別々の Unix ユーザーで動かすなら、systemd の DynamicUser 機能で時間を節約できる
    UID を割り当て、ログ/状態ディレクトリを適切な権限で作成してくれる
    https://0pointer.net/blog/dynamic-users-with-systemd.html

    • UID を永続化する必要があるなら、systemd-sysusers で新しいシステムユーザーを作るのもとても簡単
      /etc/sysusers.d/ にユーザー名やホームディレクトリなどの情報を書いた小さなテキストファイルを置き、sysusers コマンドまたはサービスを実行すればよい
    • サービス/アプリごとに別々の サービスアカウント を使うのはかなり標準的なやり方で、サーバーも分けたほうがよい
  • 最近いちばん気に入っているのは HTTP トリガーのクラウド関数
    ベンダーごとの落とし穴をうまく避ければ、複雑さを大きく減らせるし、クラウドネイティブな抽象化の中で唯一「最終形」に近いと感じる
    あるアプリだけで 2000 回以上デプロイしたが、実行環境が壊れてサポートに連絡したり、難解なコンソールコマンドを打たなければならなかったことは一度もない
    Azure の分離された App Service プラン基準でも性能は素晴らしく、リクエスト単位課金に対するイデオロギー的な抵抗感も今ではなくなった
    自前でサーバーのある不動産まで所有すればもっと安くできるかもしれないが、単純な HTTP 関数が持つコンプライアンスや監査のような性質を自力で実装しようとすると、実質的に巨大企業を作って大量採用する必要がある
    ベンダーロックインという理屈も、もうあまり腑に落ちない。HTTP リクエストを受けて HTTP レスポンスを返すというインターフェースは自然で、ベンダーごとの差はトリガーメソッドのシグネチャや OIDC/SAML クレームのような付随コンテキスト程度なので、1 週間以内に別ベンダーへリファクタリングできない構造を作るほうがむしろ大変
    個人ブログなら Hetzner VM を買って職人のように磨き込むほうが楽しい、というのは理解できる。規制がまったくない業界なら、サーバーの完全外注よりもマージンと複雑さを細かく見積もるべきだという主張にも、もっと説得力があるだろう

    • 「自前で実装するには 10 億ドル企業を作ってさらに 1000 人雇う必要がある」というくだりでは目を丸くした
      クラウド移行前はみんなどうしていたと思っているのか分からない
      今は大手産業系企業で複数チームを担当し、Azure による革新的なアプリの拡張・デプロイを進めているが、ユーザー数に比べてクラウド費用がばかげて高い
      以前はユーザー数が 10 倍のアプリを、コストは 100 分の 1、しかももっと低い複雑さで運用していた
      この費用はこういう怪しい選択をした別部署に請求されるので個人的にはどうでもいいが、クラウドを盲目的に信じる態度は理解しがたい
    • 極めて規制の厳しい業界で働いているが、最後の一文は理解できない
      私たちにとっては 自社ハードウェア運用 が最善で、オフィス内で写真も撮れないレベルなので、クラウドプロバイダーに何かを任せる可能性は 0% だ
    • 関数はシンプルな API をホストするには悪くない立ち位置だ
      ただ、アプリが実用的になるにはデータベースのような別要素も必要で、そこで追加コストが発生するのではないか?
    • ここで言う HTTP トリガーのクラウド関数 が正確に何を指しているのか分からない
      HTTP サーバーのリクエストハンドラなのか、関数型サービスとしてのクラウドコンピューティングなのか、単なる昔ながらの RPC なのか混乱する
    • 関数が実際に何をするのかが重要だ
      どこかのスクリプトキディが 1 分間に 10 回ずつリクエストし始めたら、関数のコストが VPS より高くなるまでどれくらいかかるのだろう?
  • 個人サイトとプロジェクトサイトに似たような構成を使っている
    Linode VM は月 5 ドルのものを使い、Debian GNU/Linux を利用し、ソフトウェアは Common Lisp で書いている
    個人ウェブサイトやブログは Common Lisp プログラムで静的サイトを生成し、オンラインサービスやウェブアプリは Hunchentoot で HTTP リクエストを処理してレスポンスを返す Common Lisp プログラムとして作っている
    VM の起動・再起動時にサイト/サービスが自動で立ち上がるよう systemd ユニットファイルを使っており、たいてい 10〜15 行程度
    VM の初期設定はシェルスクリプトでコード化してある: https://github.com/susam/dotfiles/blob/main/linode.sh
    プロジェクト別・サービス別の設定はそれぞれ Makefile で管理している: https://github.com/susam/susam.net/blob/main/Makefile, https://github.com/susam/mathb/blob/main/Makefile
    コンテナは使っていない。これらのサイトはコンテナが流行する前から何年も運用されており、今のところ初期化スクリプトと Makefile で十分だった
    Nginx も使っている。静的ファイルを配信し、バックエンドサービスがあるときはリバースプロキシとして機能し、TLS 終端に加えて、リクエストのレート制限やバックエンド保護のための HTTP ヘッダー許可リスト設定といった利点もある
    個人用の小さなプレイブックには curl LINK -o linode.sh && sh linode.sh, git clone LINK && cd PROJECT && sudo make setup https のようなコマンドがいくつか入っている
    make ターゲットは Nginx、certbot、sbcl のようなツールのインストール、Nginx 設定、証明書設定など、サイト稼働に必要な作業を処理し、コマンドが終わればウェブサイトはすぐ公開される

    • どの Common Lisp 実装を使っているのか気になる
      Makefile を見ると SBCL を使っているようだが、メモリ使用量が問題になったことはないのだろうか
      512MB RAM の VPS を使っているが、SBCL インスタンス 1 つでだいたい 100MB 食うので、同時に起動できるサービスは数個しかない
      もっともトラフィックの少ないサービスは CLISP に移そうかとも考えたが、使っている機能の 1 つであるパッケージローカルエイリアスがない
  • 数年にわたって構成を磨き続け、今ではすべてをDockerコンテナで動かす形に落ち着いた
    オーケストレーターは systemd の代わりに docker-compose、プロキシは nginx の代わりに Caddy を使っている
    元記事と同じく、実行する必要があるプロジェクトごとにデプロイスクリプトを書くので、全体としてかなり似ている
    Docker の多くの利点のひとつは、同じ構成でサードパーティー製ソフトウェアも動かせることだ
    この構成を何年も使っているがとても良く、元記事のように堅牢でありつつ、必要なら好きなように変えられる柔軟性もある
    今の唯一の悩みはローリングデプロイだ。ソフトウェアが大きくなるにつれて、デプロイのたびに数秒のダウンタイムが発生するのが問題になっており、まだ単純な解決策はないが、docker swarm が正しい道かもしれない

    • Caddy を使って同じように運用している
      ダウンタイムを減らすには health_uri /health, lb_try_duration 30s を試してみるといい
      たとえば reverse_proxy api:8089 { health_uri /health lb_try_duration 30s } のように設定すると、新バージョンをデプロイしている間、Caddy がリクエストをバッファリングし、新しいサービスが立ち上がるまで 30 秒待ってくれる
      理想的には、新バージョンが正常状態になったあとで Caddy がそれを使い始め、既存のコンテナを停止すべきだ
      https://github.com/Wowu/docker-rollouthttps://github.com/lucaslorentz/caddy-docker-proxy を見てみたが、まだ優先順位を付けられていない
    • Caddy のようなロードバランサーが Pod の前にあるなら、新しい Pod が立ち上がる間リクエストを保持するように設定できる: https://twitter.com/bradleyjkemp/status/1486756361845329927
      完璧ではないが、ブラウザが接続エラーを受け取る代わりに数秒間読み込み中になるだけで済む
      https://mrsk.dev/ も同じ手法を使っている
    • 勤務先のスタートアップのソフトウェアスタックをゼロから作る際に、最初から Docker でアプリケーションをパッケージ化した
      本番環境は compose で始め、のちにスタックを自動アップグレードする継続的デプロイパイプラインへと改善した
      会社とユーザーベースが成長するにつれて、再起動やデプロイ時にダウンタイムが発生する問題が現れ、追加アプリを動かしたいと思うたびに新しいデプロイ環境を用意しなければならなかった
      Kubernetes を使う日が来るのではないかと恐れていたが、その複雑さを実際に見たことがあり、一日の大半をクラスターのご機嫌取りに使いたくなかった
      そこでSwarmモードに進んだのだが、時にジキル、時にハイドのような旅路だった
      誰も直そうとしないバグ、実装されていないのに告知もされない Docker 仕様の一部、髪をかきむしりたくなるような実装上の選択、Docker Inc の社員同士が会話していないか最後まで集中していないように感じることがある
      それでも美しい面は多い。compose スタックがそのまま動き、必要な地点でスケールでき、正しく設定すれば無停止デプロイ、アップグレード、ロードバランシング、ロールバックがうまく機能する
      Raft は他の場所と同様にクラスター維持で信頼でき、少し手をかければ K8s の保守予算の一部だけで、柔軟で安全かつ自動分散されるセルフサービスプラットフォームを作れる
      ただし、デプロイスクリプトをきちんと作る準備は必要だ。有効な Docker 仕様の compose ファイルを Swarm 仕様へ変換し、シークレットを更新・整理し、環境変数を展開する Python ツールを作るのにかなり時間を使った
      VPS プロバイダーによっては、ネットワーク MTU も必ず正しく設定しなければならない。このせいで寿命がかなり縮んだ気がする
    • すでに compose ファイルがあるならSwarmが進む道ではあるが、個人的にはそれだけの価値はないと判断している
      顧客やユーザー数が増えて、拡張の問題が現実に発生するまではそうだ
    • 似たような構成を作ったが、docker savedocker import でイメージを SSH 経由で送り込むやり方は気に入らない
      自前でレジストリを運用しているのか気になる
  • 物理サーバーでは Podman Pod に PostgreSQL データベースとアプリを入れ、すべて localhost の 5000 番超のポートで動かし、Caddy が 443 でリバースプロキシとして動作している
    systemd Timer で毎日午後 4:55 にすべてのデータベースをひとつのディレクトリにダンプしている
    そのあと DejaDup [1] が毎日午後 5 時に $HOME を外付け HDD に自動バックアップし、キャッシュファイルは除外するがデータベースダンプは含めている
    OS は Debian に GNOME Core [2] を載せており、firewalld のルールは 80、443、カスタム SSH ポートだけを許可している
    SSH は鍵ベースで、パスワード認証は無効にしている
    いちばん退屈なやり方だが、ただただうまく動く
    1 - https://flathub.org/apps/org.gnome.DejaDup
    2 - https://packages.debian.org/bookworm/gnome-core

    • サーバーでなぜGNOMEを動かしているのか気になる
    • cron ジョブの代わりにsystemd timersを使う理由があるのか気になる
  • 最近はサーバー1台にDokkuを載せて使っている
    管理が簡単で、開発者は Heroku 方式のように git push するだけでデプロイでき、プラグインも多いのでデータベース追加や HTTPS 設定も最大 10 秒で済む

    • 同じやり方を使っているが、git push でデプロイできるので本当に便利だ
      アプリ追加、データベース追加、環境変数管理、ドメイン管理がどれもとても直感的だ
  • シンプルな構成が好きなので、こういうやり方は作業していて楽しく感じられる
    サービスの99%には十分である可能性が高い
    サーバー設定とデプロイスクリプトには Ansible を使うと思う
    そうすればサーバーが文書化され、デプロイスクリプトもよりシンプルになるかもしれない
    ただし、デバッグや確認のためにサーバーへ直接接続することは避けないと思う

    • Ansible は使っているが、本当に価値があるのか見直しているところだ
      自分のスクリプトはこの7年間ほとんど変わっていない一方で、Ansible は遅く、複数のファイルを継続的に保守しがちな傾向がある
      デバッグに関しては、nginx でアプリのログを パスワード保護されたエンドポイント に公開することもできる
  • クラウド移行以前にも CI/CD と効果的なサーバー管理は一般的な実務だった、ということをときどき忘れがちなようだ

  • Rust でサーバーソフトウェアを書いて静的リンクし、そのうえで HTML、CSS、設定、シークレットなどをすべてバイナリにコンパイルして埋め込むやり方については、最近 Go でこうしているが、静的ファイルに依存するソフトウェアの作成とデプロイが本当に簡単になるのでとても気に入っている

    • コンパイル済みバイナリに シークレット を含めることには、やはり疑問が残る
      シークレットは環境変数や設定を使うのが標準に近く、実行前のステップが1つ増えるとはいえ、バイナリを共有しながら中にシークレットが入っていたことを忘れるような状況を避けられる
      バイナリを展開して文字列を探すのはかなり簡単だ
      静的フロントエンド資産とデフォルト設定をまとめて埋め込めるのは大きな利点だ
    • Go は特別好きではないが、この理由でつい使ってしまう
      Rust で、特に Mac から Linux への クロスコンパイル は比較的つらい一方で、Go では些細なことで、go ツールに組み込まれている
      サイドプロジェクトを仕上げてデプロイするときの摩擦を本当に簡単になくせる
    • https://play.clickhouse.com/play?user=play も同じ方式で動いている
      ただ、1つ質問がある。記事では「Let’s Encrypt の証明書を certbot で取得し、自動更新も処理される」とあるが、私はサーバー2台と geo-DNS を使った クロスリージョン構成 にしているため、certbot が米国サーバーでしか動かず、欧州サーバーへ証明書を手動でコピーしなければならない
      これを解決する方法はあるだろうか?
      ClickHouse Playground の説明はこちら: https://ghe.clickhouse.tech/
    • 自分も同じやり方だ
      バックエンド API を提供する同じ静的コンパイル済みサービスから、Web アプリケーションも配信している
      CI で npm build を実行してから結果を埋め込むと、ローカルテストやデモインスタンスの起動がとても簡単になる