🚨

2024年記憶に残ったインシデント2選

2024/12/04に公開

こんにちは、モニクルでエンジニアリングマネージャーをやっているka2nです。
この記事は モニクル Advent Calendar 2024の4日目の記事です。 昨日は、@sugoi-wadaの 禅と暮らす一週間。一日一食断食と仏教体験レポート 準備編 2024 年秋 - wada-blog でした。 修行と言えば私もやったことがあります。またやりたいな。

https://scrapbox.io/wada-blog/禅と暮らす一週間。一日一食断食と仏教体験レポート_準備編_2024年秋

はじめに

モニクルでは、マネイロ をはじめ、「金融の専門知識がなくても、正しい意思決定ができる人を増やす。」をグループのパーパスとして、様々なサービスを開発・運用しています。
システムの運用と障害は切っても切り離せないものではありますが、弊社では昨年よりSREチームを結成し、全エンジニアがシステム運用技術を磨き、サービスの安定稼働を支える組織として成長を続けています。
本年はシステムの障害に対する対処(= インシデント対応フロー)の一環としてポストモーテムを書く文化が根付いてきました。
その中でも印象に残ったインシデントをピックアップしてご紹介します。

Terraform: (Google Cloud)サービスを複数モジュールで有効化した後、意図せず無効化しサービス停止

TL;DR

  • TerraformでGoogle Cloudを管理している。
  • Google Cloud Serviceの有効/無効や、特定のサービスアカウントへのIAM Role Bindingをモジュール(Terraform module)で管理していた。
  • 同一モジュールを複数回使用することでリソースの多重管理状態になってしまった。
  • モジュール使用箇所を一部削除する時に意図せず当該リソースが無効化されサービス停止してしまった。
  • 対策として lintnet を使用して安全な記述を推奨するようにした。

前提

社内のTerraformリポジトリはモノレポとし、以下の構成になっています。

.
├── .github
├── CODEOWNERS
├── ...
├── modules
│   └── common-a
└── products
    ├── bar
    └── foo
        ├── env
        │   ├── nonprod
        │   │   └── main.tf
        │   ├── prod
        │   │   └── main.tf
        │   └── shared
        │       └── main.tf
        └── modules
            └── product-specific
                └── main.tf

  • ./modules : 組織全体で共有可能なモジュール
  • products/<product>: 各プロダクト用のディレクトリ
  • products/<product>/env/<env>: プロダクトの各環境用のTerraform root module
  • products/<product>/modules: プロダクトに閉じた再利用可能なTerraform module

各プロダクトのディレクトリ単位でチームによるレビューを必須とし、チームごとに自己管理しています。

障害の内容

あるプロダクトで不要なCloud Runサービスを含むモジュールを削除したところ、Google Cloud Projectの run.googleapis.comサービスが停止されてしまいました。
その結果、プロジェクト内のすべてのサービスが削除状態になってしまい、当該プロジェクトのサービスが一時的に停止してしまいました。

復旧

インシデント発生直後、担当者が問題に気付き、War roomを作って対応にあたりました。
問題の操作を切り戻すPRを作成して、最新イメージを再デプロイすることで復旧しました。

原因と対策

かねてより、 Terraform moduleはterraform-google-modules程度の粒度で作成できる場合に限って使い、できるだけフラットなプロジェクト構成にした方が良いかもな。 と思っていたのですがその思いを強くしました。

原因

下記のTerraform moduleがあり、プロダクトのTerraform root moduleから使用していました。

modules/cloud-run/main.tf
locals {
  used_services = [
    "run.googleapis.com",
    ...
  ]
}

resource "google_project_service" "services" {
  project            = data.google_project.run.project_id
  for_each           = toset(local.used_services)
  service            = each.key
}

resource "google_cloud_run_service" "service" {
  ...
  depends_on = [
    google_project_service.admin-services["run.googleapis.com"],
  ]
}

トリガーは下記の変更でした。

env/prod/main.tf
module "service-1" {
    source = "../../modules/cloud-run"
}

- module "service-2" {
-     source = "../../modules/cloud-run"
- }

弊社のTerraformリポジトリは tfaction を使用してワークフローを管理しており、削除されるリソースは一覧で確認できます。
しかしながらこの差分を見過してしまったことで当該インシデントが発生することとなりました。


意図せず google_project_service が削除されようとしている。(今回は言及していませんが、実は IAM Role でも同様の事が発生しています)

教訓

今回原因となったモジュールは、プロジェクト単位で管理されるモジュールでした。
プロダクト側の事前準備の期待は可能であるものの、極力あるモジュール自身が自己完結していることを意図したリソース定義が仇となりました。
共有リソースの存在を確認する記述は同一リソースの多重管理につながるので避けるという教訓を得ることができました。

対策

「モジュールで google_project_service リソースを定義する場合は disable_on_destroy = false を必須とする」としました。
lintnet を使用して以下のルールを作成しました。

lint/google_project_service/main.jsonnet
// A lint file of lintnet.
// https://lintnet.github.io/
function(param)
    std.sort([
        {
            name: "google_project_service-disable_on_destroy",
            message: "google_project_service must have disable_on_destroy = false",
            location: {
                address: resource.address,
                url: "https://github.com/<org>/<repo>/tree/main/lint/google_project_service#google_project_service-disable_on_destroy",
            },
        }
        for module in std.get(param.data.value.planned_values.root_module, 'child_modules', [])
        for resource in module.resources
        if resource.type == "google_project_service" &&
        std.member([null, '', true], std.get(resource.values, 'disable_on_destroy', true))
    ], function (elem) elem.location.address)

Google Cloud CDN: 一部リージョンでファイルが破損して一部サービス停止

TL;DR

  • Google Cloud CDNを使用しているシステムで、一部利用者のみブラウザ側で SyntaxError でSPAが動かなくなっていた。
  • 東日本のユーザーは問題無いが、西日本のユーザーは影響を受けた。
  • JSファイルに同じコードが2回出現するなどファイル破損が起きていた。コンテナイメージには問題がなかった。ログに記載されるファイルサイズ等からCDNを疑った。
  • たまたまCloud Load BalancingでCDNによるコンテンツの圧縮が無効になっていたことに気付いて有効化することで結果的に解決した(オマケで体感パフォーマンスも向上)。

前提

Google CloudのCloud Load BalancingとCloud CDNを使用し、バックエンドはCloudRunを使用してサービス提供を行っていました。
アプリケーションはSPAで作られており、大きなJSと、一部初期データをSSRしたHTMLを配信しながらGraphQLで各種APIを提供しています。


引用: グローバル HTTP(S) 負荷分散と CDN がサーバーレス コンピューティングをサポート | Google Cloud 公式ブログ

障害の内容

一部の利用者からシステムが利用できないとの報告を受けました。 障害の対象プロダクトと影響範囲により即時War roomが作成されました。
該当プロダクトの開発チームでCloud Runのコンテナイメージの再作成とデプロイ、問題がありそうなコンテナイメージの調査が行なわれましたが復旧できず、原因も判明しませんでした。

そこで、たまたま沖縄でワーケーションをしていた開発メンバーが検証した所、再現でき、問題はSPAで使用しているJavaScriptのファイルにあることを特定、またそのファイルを回収できました。

復旧

沖縄の開発メンバーの目grepによって、システムから配信しているJSファイルが破損しているらしいことが判明しました。同時に、コンテナに含まれるファイルは正常であることが判明。
また、Cloud CDNのオリジンがNRT(東京)では再現せず、KIX(大阪)では起きていることから、一旦Cloud CDNのキャッシュパージを実施しました。

調査を進める中で、 Cloud Load BalancingでCloud CDNを有効化していたものの、コンテンツを圧縮していないことが判明。
問題が「Cloud Load BalancingからCloud CDN間のデータ転送」もしくは、「Cloud CDNのキャッシュストレージへの保存処理」であればコンテンツを圧縮することで何らかの影響を及ぼすことが可能ではないか。と予想しました。
compression_mode を有効化しました。

有効化後、利用者側でも問題なくシステムが動作することを確認でき、当該インシデントは収束できました。
コンテンツ圧縮をしたことで、JSファイルの転送量が減ったため「画面の表示が速くなりました!」とのフィードバックも受けました。

原因・教訓

調査中に、Issue TrackerCloud Forums を確認しましたがそれらしい情報はありませんでした。

教訓

この件に関する具体的な対策というのは現時点ではありません。
しかしながら、サービスの運用においてSPOFになっている箇所を意識し、(コストが見合えばそれを無くし)PaaSやSaaSでの障害に対しても機動的に対処できる準備と対策をしていきたいと思っています。

まとめ

本記事では、モニクルで2024年に発生した印象深い2つのインシデント事例について紹介しました。
これらの事例は実際に起きたインシデントのポストモーテムを元にしています。
モニクルではこれらのインシデントと対応・対策を通じて、ノウハウを貯め、システムの運用技術を磨き、サービスの安定稼働を支える組織として成長を続けたいと考えています。

株式会社モニクル

Discussion