Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

運用改善チームとして、月30時間の運用工数削減に取り組んだ話

初めまして。Sansan Engineering Unit Master Dataグループの上野です。

私たちMaster Dataグループでは、Sansanの各プロダクトで活用されるさまざまなデータの収集・提供を担うサービスを、複数のサブチームに分かれて開発・運用してきました。
しかし、活動を続ける中で、次第に次のような課題が顕在化してきました。

  • サービスに関する知識や運用ノウハウが各チームに分散しており、サービス横断的な活動をできる人がいない
  • 開発初期にスピードを優先した設計が、運用フェーズで負債となっている

こうした状況を打破するため、私たちは「One Master Data Group」という方針を掲げ、従来のチーム制を廃止しました。
そしてグループ全体での運用改善とナレッジ統合を目指すチーム、T.O.P.(Team of Operations & Productivity)*1を新たに立ち上げました。
私はその初期メンバーの一人として、参画しています。

T.O.P.で最初に掲げたOKRは、「運用にかかる工数を月30時間削減する」です。
今回は、その一環として私が取り組んだプロジェクトを2つご紹介します。

目次

背景

マスターデータグループは、その名の通りマスターデータの製造と品質管理を担うグループです。正確で信頼性の高いデータはサービスの基盤であり、その維持と向上が私たちの使命です。

日々の業務において、マスターデータの品質向上は主にデータ修正作業によって実現されています。この作業とは、社内外から寄せられるフィードバックを基に、不備や改善点のあるデータを適切に修正するプロセスを指します。これにより、最新かつ正確な情報を維持しています。

さらに、品質管理の一環として、私たちは月次でKPI(重要業績評価指標)を測定し、データ品質の状態を定量的に確認しています。これにより、修正作業の効果や品質の変化を継続的に把握できます。

今回ご紹介するのは、これらの「データ修正作業」と「KPI測定」という2つの運用における工数削減の取り組みです。

データ修正作業の運用工数削減

まず取り組んだのは、データ修正作業にかかる運用工数の削減です。

このデータ修正作業では、処理の中で他部署が提供するAPIを利用しており、そのデータに副作用があります。 トランザクション処理の実装は難しく、誤った修正のロールバックには大きな工数がかかります。 そのため、事前にステージング環境で検証し、問題なければ本番環境でデータを修正するプロセスを採用しています。

従来の作業フローは、次のようなものでした。

従来の作業フロー

このフローには次の2つの課題がありました。

  • 不要なコミュニケーションコスト: 修正アプリはGCP上でのみ実行可能であり、権限管理が煩雑なことから、開発メンバーに都度実行を依頼する運用となっていた。その結果、実行のたびにやり取りが発生し、不要なコミュニケーションコストを招いていた。
  • 手動作業の多さ: 修正アプリの実行はすべて手動で行われていたため、待機や確認に要する時間が無駄になっていた。

これらの課題を解消するために、Slack BoltGCP Workflowsを組み合わせてフローを改善しました。

  • Slack Bolt
    Slack Botやアプリを効率的に開発できる公式フレームワーク。Slack上のアクション(ボタン押下、モーダル送信など)をトリガーに特定のコードを実行する仕組みを簡単に構築できる。
  • GCP Workflows
    複数のクラウドサービスやAPIを連携・順次実行するGCPのオーケストレーションサービス。少ないコードで業務フローを自動化できる。

Slackをインターフェースとして採用した理由は、すでに全社的に導入されており、ユーザーにとって最も馴染み深く、使い慣れた環境であるためです。
新たなツールを導入する必要がなく、学習コストを抑えながら、通知や操作の導線を自然な形で組み込むことが可能です。

また、GCP Workflowsを選定した理由は、柔軟なフロー制御が可能だからです。
条件分岐やループ処理を含められるため、複数の Cloud Run Job を効率的かつ明示的に管理できます。
さらに、GCP内の他リソースとの統合も容易で、各ステップから外部への HTTP リクエストの送信も可能です。そのため、Slack 通知を含む一連の処理を、単一の GCP リソース内で完結させられます。

Slack Bolt

Slack Boltの構成には複数の選択肢があります。
当初はスラッシュコマンドでモーダルを開き、その送信をトリガーにWorkflowsを実行する構成を検討していました。
しかし、UXや将来の拡張性を考慮し、Slack Appを利用する構成に変更しました。

スラッシュコマンドはCLI的な操作感で、細かなコマンドを覚える必要があり、特に非エンジニアのメンバーには馴染みにくいという課題があります。
一方、Slack AppはGUIベースで直感的に操作できます。また、将来的にデータ修正以外の運用もSlackをインターフェースとして行う場合、機能を一画面に集約できる利点もあります。

これらを踏まえ、今回の改善ではSlack Appホームからのモーダル送信をSlack Boltで検知し、それをトリガーにGCP Workflowsを起動する構成としました。

最初に、Slack Appのホーム画面にデータ修正フローを開始するボタンを表示する処理を実装しました。これはapp_home_openedイベントを使って実現しています。

app.event('app_home_opened', async ({ event, client }) => {
  try {
    await client.views.publish({
      user_id: event.user,
      view: {
        type: 'home',
        callback_id: 'home_view',
        blocks: [
          // 省略
          {
            // 省略
            accessory: {
              type: 'button',
              text: {
                type: 'plain_text',
                text: 'インポート処理を開始',
                emoji: true,
              },
              value: 'import_manually_corrected_business_location',
              action_id: 'import_manually_corrected_business_location_button',
            },
          },
        ],
      },
    });
  } catch (error) {
    // 省略
  }
});

次に、開始ボタンが押された際にモーダルを表示する処理を実装しました。モーダルでは、修正内容が記載されたスプレッドシートのIDを入力できるようにしています。

app.action(
  // 前述のbuttonのaction_idに対応
  'import_manually_corrected_business_location_button',
  async ({ ack, body, client }) => {
    // 3秒以内に応答する必要がある
    await ack();

    try {
      await client.views.open({
        trigger_id: (body as any).trigger_id,
        view: {
          type: 'modal',
          callback_id: 'import_manually_corrected_business_location_modal',
          // 省略
          blocks: [
            // 省略
            {
              type: 'input',
              block_id: 'sheet_id_input',
              element: {
                type: 'plain_text_input',
                action_id: 'sheet_id',
                placeholder: {
                  type: 'plain_text',
                  text: 'Sheet IDを入力してください',
                },
              },
              label: {
                type: 'plain_text',
                text: 'Sheet ID',
                emoji: true,
              },
            },
          ],
        },
      });
    } catch (error) {
      // 省略
    }
  },
);

最後に、モーダルの送信をトリガーとして、GCP Workflowsを起動する処理を実装しました。

app.view(
  // 前述のmodalのcallback_idに対応
  'import_manually_corrected_business_location_modal',
  async ({ ack, body, view, client }) => {
    // 3秒以内に応答する必要がある
    await ack();

    // 省略

    const sheetId = view.state.values.sheet_id_input.sheet_id.value?.trim();
    try {
      const execution = await workflowsClient.createExecution({
        parent: workflowsClient.workflowPath(
          GCP_PROJECT,
          REGION,
          // 後述のWorkflow名を代入した定数
          MANUALLY_CORRECTED_BUSINESS_LOCATION_IMPORT_WORKFLOW_NAME,
        ),
        execution: {
          argument: JSON.stringify({
            SHEET_ID: sheetId,
          }),
        },
      });
    } catch (error) {
      // 省略
    }
  },
);

補足として、Slack Events API には「リクエストへの応答を3秒以内に完了しなければならない」という制約があります。
この制約に対応するため、イベント受信時には処理の先頭で即時に応答を返すように実装しています。

GCP Workflows

GCP Workflowsでは、次の3つの処理を一連のフローとして自動実行するように設計しました。

  1. ステージング環境での事前検証
  2. 本番環境データの修正
  3. 修正完了のSlack通知
main:
  params: [args]
  steps:
    - init:
        assign:
          - importerJobIdStg: projects/${project_id_stg}/locations/${location}/jobs/${importer_job_name}
          - importerJobId: projects/${project_id}/locations/${location}/jobs/${importer_job_name}
          - sheetId: $${args.SHEET_ID}

    # 省略

    - runStgImportJob:
        try:
          call: googleapis.run.v2.projects.locations.jobs.run 
          args:
            name: $${importerJobIdStg}
            body:
              overrides:
                containerOverrides:
                  env:
                    - name: SHEET_ID
                      value: $${sheetId}
          result: stgImportJobResult

    # 省略

    - runImportJob:
        try:
          call: googleapis.run.v2.projects.locations.jobs.run
          args:
            name: $${importerJobId}
            body:
              overrides:
                containerOverrides:
                  env:
                    - name: SHEET_ID
                      value: $${sheetId}
          result: importJobResult

    # 省略

    - notifySuccess:
      # 省略
      # Slack APIのchat.postMessageを使って通知

googleapis.run.v2.projects.locations.jobs.run は、GCP Workflowsで利用可能なConnectorsの1つです。 Connectorsとは、Workflowsから他のGoogle Cloudサービスと連携するための組み込みインターフェースを指します。
このConnectorは、Cloud Run Jobsを起動するために使用されるもので、Job の実行完了(または失敗)までをポーリング処理によって待機してくれるという特徴があります。 そのため、別途ポーリング処理を自前で記述する必要がなく、シンプルかつ効率的にジョブの実行を制御できます。

Workflowsの実装で特に苦労したのは、エラー通知の部分です。
上記のコード例では便宜上省略していますが、実際にはステージングや本番環境でデータ修正アプリの実行が失敗した場合にも、Slackへ通知が送られるように実装しています。 通知にはアプリ実行時のエラーメッセージを含めることで、デバッグを容易にすることを意図していました。
しかし、GCP Workflowsの例外処理で取得できるエラーオブジェクトには、次のような限られたメッセージしか含まれません。

Task xxx failed with message: The container exited with an error.

当然といえば当然ですが、Cloud Run Job内の処理で標準出力に出力したログはWorkflowsのエラーオブジェクトには含まれないのです。
そのため、デバッグに必要な詳細なエラーメッセージを取得するには、Cloud Loggingでクエリするか、あるいはCloud Run Job側の実装に手を加える必要があります。

改善結果

この仕組みにより、作業フローは次のように改善しました。

  • 開発メンバー以外のメンバーも修正作業を実行可能に
  • データの検証・修正が自動化され、作業間の待機や確認が不要に

改善後のフロー

結果として、開発メンバーの負担を大幅に軽減し、全体の作業効率を向上させることができました。

KPI測定プロセスの運用工数削減

次に取り組んだのは、KPI測定プロセスにかかる運用工数の削減です。

KPI測定では、データをサンプリングし、それを基に精度や網羅率などの指標を算出しますが、その多くは手作業で行われており毎月一定の作業負担となっていました。

そこで、作業効率の向上と測定精度の安定化を目的に、KPI測定プロセスの自動化プロジェクトが始動しました。
自動化の構成は比較的シンプルです。 サンプルデータの抽出処理を Cloud Functions 上に実装し、その結果を Google Drive に出力。これら一連の処理を GCP Workflows で呼び出すことで自動化しています。

構成は単純である一方で、実装中にはいくつかの技術的な課題にも直面しました。
中でも特に検討を要したのは、次の2点です。

  • Google Apps Scriptの制約
  • Google Driveのストレージ容量(Storage Quota)問題

ここでは、直面したこれら課題について詳しくご紹介します。

Google Apps Scriptの制約

最初に直面したのは、Google Apps Script(以下、GAS)における機能制約の問題です。

GASは、Google が提供するクラウドベースのスクリプト言語です。Google Workspace(Gmail、Google Sheets、Drive、Calendar など)の操作を簡単に自動化・拡張できる点が特徴です。

当初の構想では、このGASを使うことで、従来はスクリプト実行が必要だった指標の算出処理をエンジニア以外のメンバーでも扱えるようにしようと考えていました。

しかし、GASにはNode.jsのコアモジュール(例:httpfsなど)をサポートしていないという制約があります。 代替手段として、たとえば http 相当の処理には UrlFetchApp というGAS専用のAPIが用意されていますが、これはあくまで簡易的な代替です。

本プロジェクトでは住所の正規化処理が必要でしたが、自前での実装は現実的でなく、ライブラリの利用が不可欠でした。 加えて、利用予定のライブラリは内部的にコアモジュールを使用しており、GASでは利用が困難でした。
一部の処理はポリフィルで代替可能でしたが、GASのサンドボックス環境では禁止されている動作も含まれており、最終的に完全な互換性の実現には至りませんでした。

そもそもGASは「軽量な自動化」向けに設計されており、Node.jsのような本格的な実行環境とは思想が異なります。
結果的に、今回の要件にはそぐわないと判断し、指標算出処理は別の手段で実装することになりました。

Google Driveのストレージ容量(Storage Quota)問題

次に直面したのは、Google Driveのストレージ容量制限に関する問題です。

KPI測定プロセスの自動化が一通り完成し、本番環境での稼働テストに入ったタイミングで、次のようなエラーが発生しました。

googleapiclient.errors.HttpError: <HttpError 403 when requesting https://www.googleapis.com/drive/v3/files?fields=id&alt=json returned "The user's Drive storage quota has been exceeded.". Details: "[{'message': "The user's Drive storage quota has been exceeded.", 'domain': 'usageLimits', 'reason': 'storageQuotaExceeded'}]">

開発・検証環境では問題なく動作していたため、当初は原因がつかめませんでした。
しかし、Drive API を用いて storageQuota を確認したところ、本番環境のアカウントのみストレージ上限が0に設定されていることを確認できました。

確認方法:

from googleapiclient.discovery import build

service = build("drive", "v3", credentials=CREDENTIALS)
storage_quota = service.about().get(fields="storageQuota").execute()
print(storage_quota)

本番環境:

'storageQuota': {
  'limit': '0',
  'usage': '1252665',
  'usageInDrive': '1252665',
  'usageInDriveTrash': '0'
}

開発環境:

'storageQuota': {
  'limit': '16106127360',
  'usage': '6249021',
  'usageInDrive': '6249021',
  'usageInDriveTrash': '0'
}

2025年8月7日時点では、GCPから公式なアナウンスは確認できていないものの、サービスアカウントに割り当てられるデフォルトのストレージ容量について、何らかの仕様変更が行われた可能性があります。

この問題への対応として、次の3つの代替案を検討しました。

  1. 共有ドライブを利用する
    サービスアカウントの書き込み先として、Google Driveの共有ドライブを指定する方法です。

  2. Domain-Wide Delegationを用いたユーザー権限の委任
    サービスアカウントに Google Workspace ドメイン内のユーザー権限を委任し、そのユーザーとしてファイルを操作する方法です。

  3. Google Driveの代替として GCS(Google Cloud Storage)を利用する
    Drive を使わず、GCS バケットをストレージとして利用する方法です。

今回は「2. Domain-Wide Delegationを用いたユーザー権限の委任」を採用することとしました。
なお、DWDはサービスアカウントに広範なアクセス権限を委任するため、セキュリティリスクへの十分な配慮が必要です。

効果とこれから

今回紹介した2つの施策により、削減できた工数は次の通りです。

  • データ修正作業の運用工数削減: 月3〜5人時(フィードバック件数によって上下)
  • KPI測定プロセスの運用工数削減: 月2人時
  • 合計: 月5〜7人時

今後も継続的に運用プロセスを改善し、最終的には月30時間以上の削減を目指します。

おわりに

運用改善は一見地味なようでいて、開発チーム全体の生産性を向上させる非常に重要な取り組みです。 これからも改善事例やナレッジをブログで発信していきたいと思います。
最後までお読みいただき、ありがとうございました!

Sansan技術本部ではカジュアル面談を実施しています

Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。

*1:ChatGPTが命名。

© Sansan, Inc.