entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

古くなりすぎた Self-Hosted Sentry を建て直しました

はじめに

こんにちは。インフラエンジニアの lyluck です。

今回は、私たちが社内標準のエラートラッカーとして運用している Self-Hosted Sentry を建て直した話です。

長期の運用の中で最新バージョンとの差分が大きくなっていました。 Self-Hosted Sentry は本体だけでなく周辺ミドルウェアも含めて追従が必要で、継続的に更新し続けるには一定の運用コストがかかります。

旧環境では、継続的なバージョン追従を前提とした運用体制を十分に整えきれず、段階的なアップデートでは最新バージョンへ追いつくことが現実的に難しい状況に直面しました。

そこで、段階的なアップデートではなく新環境の建て直しを選択しました。

この記事では、その判断の背景や、移行に合わせて CI/CD、機微情報の管理まで見直した過程を紹介します。

なぜバージョンの見直しが必要になったか

Sentry は Google Kubernetes Engine (GKE)に Helm Chart でデプロイされており、そのバージョンは18.0.0でした。 このリリースは2023年5月ですから3年ほど昔のことです。

Sentry はアプリケーションのエラーを収集するため、インターネットにエンドポイントを公開しています。 バージョンがとても古いので当然、悪用されうる脆弱性は存在しました。 利用している CSPM からも重大な脆弱性が存在するとの指摘がありました。

Sentry のクライアント(SDK)が Sentry 本体との互換性の問題でバージョンアップできず、ライブラリの互換性、エラー解析やデバッグの効率に支障がある状態になっていました。

こういった事情から Sentry をアップデートすることは社内で重大な課題になりました。

なぜ段階的なアップデートではなく建て直しなのか

段階的なアップデートにかかる工数と Sentry のダウンタイムが大きいためです。

建て直し作業当時の最新のバージョンまで段階的なアップデートを実施する場合、少なくとも6回のメンテナンスが必要なことがわかっていました。

メンテナンス1回ごとに、現在では非推奨となっている古い依存ライブラリやミドルウェアを調査するための大きな工数と、Sentry のダウンタイムが発生します。

この複数のメンテナンスを1回の建て直し作業に置換することで、迅速に、より小さな工数とダウンタイムで最新バージョンの Sentry へ移行できます。

なぜ Self-Hosted をやめないのか

まずコストの問題がありました。

弊社は toC サービスを提供しており、日々の PV やユーザー行動にともなってアプリケーションから送信されるイベント数も多くなりやすい背景があります。 そのため、イベント数に応じて費用が増えやすい SaaS 版 Sentry やそれに類するサービスに移行し、同等の機能を求めると、私たちの利用規模では倍のコストがかかると想定されました。

確かに管理工数が小さくなり安定性が向上しますが、見合わないという結論になりました。

コスト削減の観点だと Self-Hosted の場合は努力次第で様々な手法でコストを圧縮できる可能性があります。 SaaS の場合はほとんど交渉しか手段がありません。

次に機微情報の保存の問題がありました。

機微情報を含むエラーが SaaS 版 Sentry に送信された場合、 SaaS 内部に機微情報が保存されてしまいます。

そのようなエラー情報が発生しないように厳格な運用を徹底していますが、万が一の可能性を完全に排除することは困難です。

どのように移行したか

次のような流れで移行を実施しました。

  1. 新環境を作成する
  2. 新環境へのアクセスを許可する
  3. 並行運用期間を設け、徐々に旧環境から新環境へ移行してもらう
  4. 旧環境へのアクセスを禁止する(新環境への移行完了)
  5. 旧環境を解体する

CI/CD の近代化

既存の CI/CD には以下のような問題がありました。

  1. デプロイのスクリプトが複雑でわかりにくい
  2. 機微情報が平文で CI/CD のログに出現している

それぞれの問題の内容と行った対処について説明します。

問題

1. デプロイのスクリプトが複雑でわかりにくい

GKE には Sentry の Helm Chart 以外にもいくつかの Helm Chart がデプロイされていました。 たとえば Datadog Helm Chart などです。

Helm Chart をデプロイするには Chart のバージョンと values.yaml が必要です。values.yaml は環境ごとに別れていました。 それらを適切に関連づけてデプロイするスクリプトがありました。 これは新しい環境の追加をするには複雑すぎるものでした。

2. 機微情報が平文で CI/CD のログに出現している

Helm では helm-diff を使って現在デプロイされているマニフェストとデプロイ予定のマニフェストはどう違うのかを表示できます。

実際にデプロイする前に dryrun としてそのような処理を走らせますが、そのログに暗号化されているはずの機微情報が平文で出現してしまっていました。

これは helm-secrets を利用して暗号化したもので、 dryrun の時にもデプロイの時と同様に暗号化した機微情報をそのまま渡していたためでした。

機微情報関連では、そのほかにも

  • ローカル環境で開発者がそれぞれ機微情報を暗号化するのが面倒
  • CI/CD の変数に環境ごとに複数の機微情報が設定してあり管理が大変

という難点があり、後述の改善につながりました。

実施した近代化

1. Helmfile の導入

Helmfile は複数の Helm Chart のデプロイ方法を宣言的に記述できるツールです。 Chart のバージョン、使用する values.yaml、デプロイの順番などを環境ごとにすっきりと定義できます。

デプロイスクリプトはほぼ次を実行するだけになり、大幅に簡略化されました。

  • dryrun: helmfile diff
  • デプロイ: helmfile apply

ラベルを利用することでデプロイ対象を部分的に選択できます。 今回はこの Chart だけデプロイするというような、柔軟な運用が可能になりました。

2. External Secrets Operator の導入

External Secrets Operator は Kubernetes クラスター内部に外部の機微情報を Kubernetes Secret として同期する仕組みを提供する Operator です。

今回は外部の機微情報の置き場所として Secret Manager を選びました。

Kubernetes ServiceAccount を Google Cloud Service Account と Workload Identity で連携してあります。

これにより

  • ローカル環境での機微情報の編集と暗号化が面倒
  • CI/CD ログへの機微情報の平文の出現
  • CI/CD 変数の複数の機微情報があって管理が大変

以上をすべて解決できました。

新しい環境のインフラを構築する

既存環境のインフラは Terraform を利用して構築されていました。 既存環境には検証環境と本番環境があり、それぞれの環境に共通のインフラはモジュール化されていました。

そのため新環境のインフラ構築には既存モジュールを活用でき、比較的スムーズに進みました。

一方、アプリケーションやミドルウェアは差分が大きく、多くの調査と対応が必要でした。

新環境アプリケーションの調整

Sentry Helm Chart の values はとても多く、そのままでよいものもあれば環境によって調整が必要なものも多数あります。 新環境の Sentry アプリケーションを起動するにあたって直面した問題とその対処をいくつか説明します。

古い設定(values)の整理

とりあえず新環境で Sentry を起動するため、既存環境の設定(values.yaml)をコピーして使っていました。

新しい Helm Chart ではもう存在しない設定が複数ありました。たとえば

  • 名前が変わったもの
  • 別の設定に取り込まれたもの
  • 本当に消えたもの

のように様々なバリエーションがありました。 古い Helm Chart とそれが依存している Helm Chart を含めて見比べながらひとつずつ対応しました。

こういった設定を放置しておくと管理コストが増えるため対処しました。

ClickHouse の migration 失敗

sentry-snuba-migrate という Job が失敗する問題がありました。

これは ClickHouse のテーブルのスキーマ変更を含む Job です。 このようなエラーが出ていました。

Query: ALTER TABLE eap_items_1_local ADD COLUMN IF NOT EXISTS attributes_array JSON(max_dynamic_paths=128) CODEC (ZSTD(1)) AFTER attributes_float_39;
snuba.clickhouse.errors.ClickhouseError: DB::Exception: Syntax error: failed at position 95 ('='): =128) CODEC (ZSTD(1)) AFTER attributes_float_39;. Expected one of: data type, nested table, identifier, token, Comma.

ClickHouse のバージョンは 23.8.16.16 でした。

失敗したのはこの migration でした

JSON というデータ型は ClickHouse 25.3 から production ready となったようでした。

In ClickHouse Open-Source JSON data type is marked as production ready in version 25.3. It's not recommended to use this type in production in previous versions.

それ以前のバージョンで利用するには allow_experimental_object_type = 1 を設定する必要がありそうでした。

しかし、これはうまくいきませんでした。 default profileに設定して ClickHouse を再起動するとクラッシュし、起動できなくなりました。 この変更しか加えていなかったため、おそらく設定方法の間違いが原因ですが、特にエラーログを確認できなかったため定かではないです。

次に ClickHouse のバージョンを上げることを試しましたが、こちらもうまくいきませんでした。 Helm Chart 側が新しい ClickHouse コンテナイメージに対応していないのが原因でした。 たとえばマニフェスト側で readonly となっているディレクトリにコンテナイメージ側で初期化処理として書き込みがあると起動できません。

Helm Chart にとっては未来のコンテナイメージなのでこれは当然なのですが、かといってこのために ClickHouse コンテナイメージをカスタムして管理するのはコストが大きいです。

なので、問題の JSON がどこで使われているのかを調べたところ一箇所でしか利用されていませんでした。 この変更は取り込まれていなくとも差し当たり問題なかったため、この変更が入る直前のリリースを利用することにしました。

失敗している migration を巻き戻し、

lyluck@cloudshell:~ (pixiv-sentry)$ kubectl exec sentry-snuba-api-768dcdcc74-qqxjz -- snuba migrations reverse-in-progress

Sentry Helm Chart の values で snuba のコンテナイメージのタグを上書きしました。

images:
  snuba:
    tag: 25.10.0

再度デプロイして migration に成功しました。

メモリの割り当て調整

移行前の Sentry と最新の移行後 Sentry では含まれるワークロードに大きな変化がありました。 そのため、各種ワークロードの適切なリソース割り当てを事前に見積もることが困難でした。 新旧環境の並行運用期間において徐々に新環境へのトラフィックが増加し、多数の OOM が発生していました。

resources の requests と limits は一致させていたため、消費メモリ量が request に達した瞬間に OOM になりました。一致させているのは次のメリットがあるためです。

  • 退避(evict)されにくい
  • ワークロードのリソース消費がバーストしない/されない(Noisy Neighborが起きにくい)
  • どれだけのリソースが必要なのか計算しやすい

メモリ消費量のグラフでは見えないが、本当はスパイクしていて上限を突破しているパターンが多くありました。

絶対の指標は特にないのですが、平時の消費量がだいたい 60% くらいとなるように requests/limits を調整しました。

Snuba, RemoteDisconnected

Snuba の consumer (たとえば sentry-snuba-outcomes-consumer)に次のエラーが発生していました。

urllib3.exceptions.ProtocolError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))

ClickHouse 側から接続が切られていました。 原因は ClickHouse の keep_alive_timeout が 3 秒ととても短い設定になっていたためでした。

Sentry Helm Chart の values に次の記述を追加して解決しました(30 秒は ClickHouse のデフォルト値)。

clickhouse:
  clickhouse:
    configmap:
      keep_alive_timeout: 30

Kafka, MSG_SIZE_TOO_LARGE

sentry-process-spans などから次のエラーが発生していました。

arroyo.errors.TransportError: KafkaError{code=MSG_SIZE_TOO_LARGE,val=10,str="Broker: Message size too large"}

consumer, server の両方でより大きなメッセージをサポートさせるため、 Sentry Helm Chart の values に次の記述を追加して解決しました。

kafka:
  controller:
    extraConfig: |
      message.max.bytes=41943040
      replica.fetch.max.bytes=41943040

設定の移行

既存環境から新環境へデータを移行する必要がありました。

量の問題で ClickHouse に格納されているような時系列データは捨てます。 PostgreSQL に格納されている Team や Project などは移行をすることにしました。

移行するといってもバージョンが違いすぎるので DB をコピーすれば済むわけではありません。

そこで Terraform を利用することにしました。やり方はこうです

  1. 既存環境から import ブロック を使って tf ファイルを生成する
  2. 生成された tf ファイルの内容を補う
  3. 新環境に apply する

このやり方は部分的にうまくいきました。

基本的なリソース

  • Organization Member
  • Team
  • Team Member
  • Project

はほとんど生成された内容そのままでよかったのですが、それ以外のリソース(たとえば Alert)などは多くの内容を補う必要がありそうでした。

社内状況の変化から迅速な移行完了が必要なこともあり、基本的なリソース以外の Terraform による移行は諦め、 Sentry を利用する開発者に手動対応してもらうことにしました。

移行ガイドを作る

前述のリソース移行を含め、どうして移行するのか、ログイン方法はどう変わったのかなどのガイドを作成し社内の開発者にアナウンスしました。

別途、Sentry の管理者向けに運用と管理方法を記述したドキュメントを整備しました。 既存 Sentry 向けのドキュメントは存在していましたが、新環境とバージョンの違いが大きいのでほとんど書き直す必要がありました。

用意したドキュメントの読み合わせや、それらを使ったチームでの勉強会を開催することで、知見の共有に努めました。

まとめ

今回の建て直しは単なるバージョンアップではなく、Sentry を継続的に運用・更新できる状態に戻すための取り組みでした。 段階的なアップデートに固執せず、新環境への移行を選んだことで現実的な工数で刷新を進められました。 また、CI/CD や機微情報管理の見直しによって、運用上の負債も同時に整理できました。

今後も継続的にアップデートし、同じ課題を繰り返さない運用を続けていきたいと考えています。

lyluck
2022年にピクシブ株式会社に中途入社。趣味はピアノと音ゲー。オンプレミスのミドルウェアやKubernetesを担当。