🗂

さらば手作業!AdminaをAPIキャッシュ層として使い倒し、自律型資産管理システムを構築した話

に公開

🚀 はじめに

社内ITでSaaSの海を泳いでいる皆さん!こんにちは。
アカウントやデバイス管理をしていると「IntuneとJamf、情報が別々で管理めんど…」とか。「このPCのMACアドレス、誰かスプシで管理してない?」
こんな「情シスあるある」、一つは経験ありますよね?

私が所属しているチームはIT資産管理とSaaSアカウント管理のハブとして「Admina」を導入しています。
多くのSaaSと連携でき、GUIも綺麗で非常に強力なツールです。

…しかし、完璧ではありません。
例えば、Adminaが標準で取得してくれるデバイス情報には、MDM連携していたとしても、MACアドレスが含まれていなかったりします。(え、そこ取ってくれないの!?っていう)

そこでこの記事では、Adminaを 「各SaaS APIのキャッシュ層 兼 柔軟なデータストア」 と割り切り、その“ちょっと足りない部分”を Google Cloud (Cloud Functions) と PythonのAPI連携で補強・自動化することで、「我々にとって理想の資産管理データベース」を育て上げた話を紹介します。

面倒なことは全部クラウドに任せて、我々はもっとクリエイティブな仕事をしようぜ!という話です。


😫 Before / 😊 After:この仕組みで何が変わったか

理屈の前に、この仕組みで僕らの日常がどう変わったかを見てください。

Before: よくある"惜しい"資産管理

  • MACアドレスの管理が属人化
    • Adminaを見てもMACアドレスがわからず、結局IntuneやJamfの管理画面を見に行くか、担当者が管理するスプレッドシートを探す必要があった。ネットワーク認証のトラブルシュート時に時間がかかる…。
  • リース管理がスプレッドシート職人頼み
    • Adminaの情報を手でスプレッドシートに転記し、関数を駆使してリース終了日や支払額を管理。担当者が不在だと誰も更新できず、情報が古くなるリスクがありました。(いわゆる「神Excel」ならぬ「神スプシ」ですね)
  • 2FAの催促が気合と手作業
    • Google Workspaceで2FA設定を強制している場合、一定期間未登録ですとログインできなくなります。それを防ぐため、定期的に管理画面から2FA未設定者を手動でエクスポート。休職者や共有アカウントを除外し、Slackで一人ひとりメンションして催促…。週一度の苦行でした。

After: 自律的に育っていく資産管理DB

  • Adminaを見ればMACアドレスがわかる!
    • 毎朝、Google CloudがIntuneとJamfからMACアドレスを自動取得し、Adminaのカスタムフィールドに書き込んでくれる。Adminaが信頼できる唯一のデバイス情報源になりました。
  • リース台帳が"勝手に"最新化される
    • 毎晩、Google CloudがAdminaのリース情報を元に契約終了日や残債を計算し、Googleスプレッドシートを自動更新。もう誰も手作業で触る必要はありません。
  • 2FA未設定者への催促が完全自動化
    • 毎朝、Google CloudがAdmina経由で2FA未設定者リストを取得し、Slackで自動メンション。情シスは「今日もちゃんと動いてるな」と通知を眺めるだけ。(最高)

🗺️ 図解:情報の流れはどう変わったか

言葉だけでなく、情報の流れがどう変わったかを構成図で見比べてみましょう。

Before: 人力と気合でSaaSの情報を突き合わせる世界

以前は、各SaaSの管理画面を個別に開き、目視で情報を確認し、手作業で転記や通知を行っていました。情報があちこちに点在し、担当者のスキルや記憶に依存する、まさに「属人化」した世界でした。

After: Adminaをハブにした自律的な情報連携の世界

そして現在。Adminaを「情報ハブ」とし、Google Cloudのサーバレス機能を「接着剤」にすることで、情報の流れを完全に自動化しました。情シス担当者は、各SaaSの管理画面を直接触ることなく、集約・加工された最終的なアウトプットを確認するだけで済むようになりました。

このように、情シス担当者がSaaSと直接やり取りするのではなく、Google CloudとAdminaが代行してくれる構成に変わったことで、業務効率が劇的に向上し、ミスもなくなりました。

🤔 なぜAdminaなのか? メリットとデメリット

数あるツールの中で、なぜAdminaを「情報ハブ」として選んだのか。私が感じているメリット・デメリットは以下の通りです。

メリット

  • APIキャッシュとしての優秀さ
    • 最大のメリットはこれ。 Google Workspace, Slack, Intune, Jamf…など、様々なSaaSのAPI仕様(認証、ページネーション、レート制限等)をAdmina側がある程度吸収・抽象化してくれます。
    • これにより、我々はAdminaのAPI仕様だけを考えれば良くなり、自前で連携するSaaSのAPI仕様変更に振り回されるリスクが軽減されます。(例: 2FA監査ではGoogle Admin SDKを直接叩かず、Admina APIだけで済んでいる)
  • 柔軟なカスタムフィールド
    • 今回のMACアドレスのように、標準機能で足りない情報を「カスタムフィールド」として柔軟に追加できます。これにより、Adminaを自社の運用に合わせたデータストアとして拡張できるわけです。
  • GUIの見やすさとアカウント連携
    • デバイス情報と、それを利用しているSaaSアカウント情報が紐づいて表示されるため、「このPCは誰が使っているか」が一目瞭然。エンジニア以外にも情報共有しやすいです。

デメリット

  • 標準機能の限界
    • メリットの裏返しですが、標準で取得できる情報は完璧ではありません。今回のMACアドレスのように、「欲しい情報がない」ケースは往々にしてあります。APIによる機能拡張が前提と考えた方が良いでしょう。
  • Admina API自体の制約
    • AdminaのAPIにも当然レート制限があります。大量のデバイス情報を一度に更新する際は、time.sleep()のような待機処理を入れないとエラーになります。

コスト

  • 当然ながらライセンス費用がかかります。しかし、手作業による人件費や、自前で同等のシステムをフルスクラッチで構築・維持するコストを考えれば、我々にとっては十分な投資対効果がありました。
  • 結論として、Adminaを「完成された製品」としてではなく、 「拡張性の高いIT情報データベース基盤」 として捉えることで、その価値を最大化できると判断しました。

🏗️ システム全体像

今回構築したシステムの全体像はこんな感じです。複数のCloud Functionsが、Cloud Schedulerによる定期実行をトリガーに、それぞれの役割をこなします。

ポイントは、それぞれのFunctionが独立しつつも、GCSやHTTPリクエストを通じて連携している点です。これにより、責務が明確でメンテナンスしやすい構成になっています。

🛠️ UseCase 1: MDM連携でAdminaの“弱点”を補強する

課題:AdminaだけではMACアドレスがわからない!

前述の通り、Adminaはデバイス情報を集約してくれますが、ネットワーク認証に不可欠なMACアドレスは標準では取得してくれません。これでは資産台帳としては片手落ちです。

解決策:MDMから引っこ抜いて、Adminaのカスタムフィールドに注入!

この仕組みは2つのCloud Functionsで構成されます。

  • mdm-data-pretreatment (前処理担当)

    • IntuneとJamfから全デバイス情報をAPIで取得。
    • GCSに保存しておいた前回取得データと突き合わせ、差分(MACアドレスの変更や新規デバイス)だけを抽出。
    • 差分があった場合のみ、後続のadmina-mac-updateをHTTPで叩いて起動!
    • [おまけ] シリアル番号が重複している"幽霊デバイス"がいたら、Slackに警告してくれます。
  • admina-mac-update (更新担当)

    • 前段のFunctionからファイルパスを受け取り、GCSから差分ファイルを読み込み。
    • Admina API (PATCH /api/v1/.../devices/{id}) を叩き、あらかじめ用意しておいたMACアドレス用のカスタムフィールド (custom.txt_7, custom.txt_8) に値を書き込みます。
    • これにより、Adminaの標準機能の弱点をAPI連携で補強し、Adminaを信頼できる唯一のデバイス情報源(Single Source of Truth)へと"育てて"います。

⚙️ コード抜粋:Jamf/Intuneからのデータ取得

azure-identity や requests を使って、それぞれのAPIからデータを取得しています。差分取得ロジックがキモですが、長くなるのでここでは取得部分の雰囲気を。


# --- Intune データ取得の雰囲気 ---
from azure.identity import ClientSecretCredential
import requests

# ... (中略) ...
credential = ClientSecretCredential(tenant_id, client_id, client_secret)
access_token = credential.get_token('[https://graph.microsoft.com/.default').token](https://graph.microsoft.com/.default').token)

# 全デバイスIDを取得してから、前回との差分があるものだけ詳細を取得
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get("[https://graph.microsoft.com/v1.0/deviceManagement/managedDevices](https://graph.microsoft.com/v1.0/deviceManagement/managedDevices)", headers=headers)
# ... (中略) ...


# --- Jamf データ取得の雰囲気 ---
# ... (中略) ...
# まずトークンを取得
token_url = f"{jamf_url}/api/oauth/token"
token_data = {'client_id': client_id, 'grant_type': 'client_credentials', 'client_secret': client_secret}
access_token = requests.post(token_url, data=token_data).json()['access_token']

# インベントリ情報をページネーションしながら取得
inventory_url = f"{jamf_url}/api/v1/computers-inventory?section=GENERAL&section=HARDWARE"
response = requests.get(inventory_url, headers={'Authorization': f'Bearer {access_token}'})
# ... (中略) ...

⚙️ コード抜粋:AdminaのMACアドレスを更新

受け取った差分データを元に、Admina APIを叩いてカスタムフィールドを更新します。

# --- Admina更新処理の雰囲気 ---
import requests
import time
# import logging
# logger = logging.getLogger(__name__)

# ... (中略) ...
def _update_admina_device(device_id: str, wifi_mac: str, ether_mac: str, admina_api_token: str) -> bool:
    """Adminaのデバイス情報を更新する"""
    ORG_ID = "your_admina_org_id"
    url = f"[https://api.itmc.i.moneyforward.com/api/v1/organizations/](https://api.itmc.i.moneyforward.com/api/v1/organizations/){ORG_ID}/devices/{device_id}"

    fields_to_update = {
        "custom.txt_7": wifi_mac or "", # WiFi MAC Address
        "custom.txt_8": ether_mac or ""  # Ethernet MAC Address
    }
    payload = {"fields": fields_to_update}
    headers = {"authorization": f"Bearer {admina_api_token}", "Content-Type": "application/json"}

    try:
        response = requests.patch(url, json=payload, headers=headers)
        response.raise_for_status()
        # logger.info(f"Successfully updated Admina device: {device_id}")
        print(f"Successfully updated Admina device: {device_id}") # Cloud Functionsならprintでログ出ますね
        return True
    except requests.exceptions.RequestException as e:
        # logger.error(f"Failed to update Admina device {device_id}: {e}")
        print(f"Failed to update Admina device {device_id}: {e}")
        return False

# ... (中略) ...
# 
# # メインの処理(イメージ)
# df_mac_diff = ... # GCSから差分DataFrameを読み込む
# token = ... # Admina APIトークン
#
# # 差分DataFrameをループして更新
# for index, row in df_mac_diff.iterrows():
#     _update_admina_device(row['deviceId'], row['wifiMacAddress'], row['ethernetMacAddress'], token)
#     time.sleep(0.2) # APIレート制限のための小休止

🛠️ UseCase 2: リース資産管理の完全自動化

課題:リース台帳、神Excelと化す問題

リース資産は情シスの悩みのタネ。
契約開始日、契約期間、月額費用、契約終了日…これらの情報を正確に管理し、将来の予算策定に活かすのって、マジで至難の業ですよね。
多くの現場では、担当者が丹精込めて作ったスプレッドシートが「秘伝のタレ」のように受け継がれています。
細かく手入力しますと、契約終了時の日付の入れ間違いはあるあるだと思います。

解決策:Pandasでゴリっと計算してSheets APIで流し込む

  • こちらもCloud Function で解決しています。
    • Admina APIから全デバイス情報を取得。
    • procurement_method が Lease のものに絞り込み。
    • Pandasを使い、Adminaのカスタムフィールドに入力されたpurchase_date(リース開始日)とwarranty_period(リース期間・月)から、usage_end_date(リース終了日)を自動計算。
    • さらに、purchase_cost(総額)を期間で割り、cost_per_month(月額費用)も算出。
    • 整形したデータをGoogle Sheets APIでスプレッドシートに丸ごと書き込み。ヘッダーはそのままに、2行目以降を全クリアしてから書き込むことで、常に最新の状態を保ちます。
    • おまけに、契約タイプ(期間と月額の組み合わせ)ごとに月々の支払額と残債をまとめたサマリーシートまで自動生成!

⚙️ コード抜粋:Pandasによる日付計算

relativedelta を使ってリース終了日を計算している部分です。Pandasの強力さが光るポイントです。

import pandas as pd
from dateutil.relativedelta import relativedelta

# ... (中略) ...
# df_lease は Admina から取得したリースデバイスの DataFrame とする

# 更新対象のデバイスをフィルタリング (例: 終了日が未計算のもの)
# ここでは例として、Admina本体も更新する想定のコードにしていますが、
# スプレッドシートへ書き出すDataFrameを加工するだけでもOK
df_to_update = df_lease[df_lease['usage_end_date'].isna()]

for index, row in df_to_update.iterrows():
    device_id = row['deviceId']
    purchase_date = pd.to_datetime(row['purchase_date'])
    warranty_period = int(row['warranty_period'])

    # リース開始月の翌月1日を開始日とする(※ここは自社の契約ルールに合わせてください)
    usage_start_date = (purchase_date.replace(day=1) + relativedelta(months=1))
    # そこからリース期間分進めて、1日引くと終了日になる
    usage_end_date = (usage_start_date + relativedelta(months=warranty_period)) - pd.Timedelta(days=1)

    # この後、Admina APIで usage_start_date と usage_end_date を更新する処理が続く...
    # (もしくは、df_lease の該当行を更新して後でまとめてスプレッドシートに書き込む)
    df_lease.loc[index, 'usage_end_date'] = usage_end_date.strftime('%Y-%m-%d')
    df_lease.loc[index, 'usage_start_date'] = usage_start_date.strftime('%Y-%m-%d')

⚙️ コード抜粋:Googleスプレッドシートへの書き込み

google-api-python-client を使って、整形したDataFrameをスプレッドシートに書き込んでいます。

from googleapiclient.discovery import build
from google.oauth2 import service_account
# import logging
# logger = logging.getLogger(__name__)

# ... (中略) ...
def write_data_to_spreadsheet(df, spreadsheet_id, sheet_name):
    # ... (サービスアカウントでの認証処理) ...
    SCOPES = ['[https://www.googleapis.com/auth/spreadsheets](https://www.googleapis.com/auth/spreadsheets)']
    # credentials = service_account.Credentials.from_service_account_file(SA_KEY_PATH, scopes=SCOPES)
    credentials = ... # 実際には認証情報を取得する処理
    
    service = build('sheets', 'v4', credentials=credentials)
    sheet = service.spreadsheets()

    # DataFrameをリストのリストに変換 (ヘッダー除く)
    # ヘッダーはスプシ側に固定で書いておく想定
    values = df.fillna('').values.tolist()

    # まずA2以降の全データをクリア!
    sheet.values().clear(
        spreadsheetId=spreadsheet_id,
        range=f"'{sheet_name}'!A2:ZZ" # 範囲は余裕を持って指定
    ).execute()

    # A2から新しいデータを書き込む
    result = sheet.values().update(
        spreadsheetId=spreadsheet_id,
        range=f"'{sheet_name}'!A2",
        valueInputOption='USER_ENTERED', # USER_ENTEREDだと日付や数値もいい感じに解釈してくれる
        body={'values': values}
    ).execute()
    
    # logger.info(f'{result.get("updatedCells")} cells updated in {sheet_name}.')
    print(f'{result.get("updatedCells")} cells updated in {sheet_name}.')

🛠️ UseCase 3: 2FA監査を“APIキャッシュ”で楽々実装

課題:Google Admin SDK、直接叩くのちょっと面倒問題

2FA未設定者を監査するだけなら、Google Admin SDKを直接叩く方法もあります。 しかし、そのためにはサービスアカウントの権限設定(ドメイン全体の委任とか)、ライブラリの導入、Google独自のページネーション仕様の理解など、それなりに手間がかかりますよね…。

解決策:Adminaの"キャッシュ"を叩いて、実装をシンプルに!

  • Adminaが定期的にGoogle Workspaceから同期してくれているアカウント情報(=APIキャッシュ)を活用します。

    • Admina API (.../accounts?twoFa=false) を叩き、GWSアカウントで2FAが有効になっていないユーザーの一覧をAdminaから取得。
    • GCSに置いてある除外リスト(CSV形式の共有アカウント、休職者など)を読み込み、催促が不要なユーザーを除外。
    • statusがactiveなユーザーだけに絞り込み。
    • Admina APIでSlackのユーザー情報も取得し、GWSのメールアドレスと突き合わせてSlackのIDを特定。
    • 最後に、Slack Webhookを使い、対象者(のSlack ID)をメンションして、設定を促すメッセージを自動投稿!
    • 自前でGoogle APIを直接叩く代わりに、Admina APIを叩くだけで済むため、認証やデータ取得のロジックが非常にシンプルになります。これこそ、Adminaを「APIキャッシュ層」として利用する大きなメリットです。

⚙️ コード抜粋:Admina経由でのGWSユーザー取得

Admina APIのフィルター機能(twoFa=false)を使って、目的のユーザー群をピンポイントで取得します。

import requests
import pandas as pd

# ... (中略) ...
# AdminaのGWS連携サービスIDを指定し、 twoFa=false でフィルタリング
ADMINA_ORGANIZATION_ID = "your_admina_org_id"
ADMINA_GWS_SERVICE_ID = "your_gws_service_id" # Admina上のGWSのサービスID
admina_api_key = "your_admina_api_key"

gws_user_url_base = f"[https://api.itmc.i.moneyforward.com/api/v1/organizations/](https://api.itmc.i.moneyforward.com/api/v1/organizations/){ADMINA_ORGANIZATION_ID}/services/{ADMINA_GWS_SERVICE_ID}/accounts?limit=200&twoFa=false"

headers = {"authorization": f"Bearer {admina_api_key}", "Content-Type": "application/json"}

# get_all_admina_dataはページネーションをよしなに処理してくれるヘルパー関数 (別途定義)
# all_gws_data_raw: pd.DataFrame = get_all_admina_data(gws_user_url_base, headers)
all_gws_data_raw = ... # 実際には全ページ取得する処理

# この後、除外リストとのマージ処理が続く...

⚙️ コード抜粋:Slackへのメンション付き通知

最終的に絞り込んだユーザーリスト(df_table)を使って、Slackに通知を送信します。

import requests
import json
import pandas as pd

def slack_notify_2fa_false(df_table: pd.DataFrame, webhook_url: str) -> None:
    # ... (中略) ...

    # ここでは簡略化のためdisplayNameを使用。
    # 実際には、事前にAdminaのSlackユーザー情報と突き合わせて
    # <@Uxxxxxxxx> 形式のSlack IDリストを作成しておくのがベスト
    
    # user_list_text = "■対象者\n" + "\n".join([f"- {name}" for name in df_table['displayName']])
    
    # slackUserId カラムに <@Uxxxxxxx> が入っている想定
    user_mention_list = "\n".join([f"- {slack_id}" for slack_id in df_table['slackUserId']])


    message = (
        f"<!subteam^S09607NE6GY>\n"  # チームにメンション (必要に応じて)
        f"現在、Googleアカウントで二段階認証を設定していないユーザーは以下の通りです。\n"
        f"早急に設定してください!\n"
        f"■手順書:(ここに手順書のURL)\n\n"
        f"■対象者\n"
        f"{user_mention_list}"
    )

    payload = {"text": message}
    requests.post(webhook_url, data=json.dumps(payload))

# ... (中略) ...
#
# # メイン処理(イメージ)
# df_final_target = ... # 除外リストと突き合わせた最終対象者のDataFrame
# SLACK_WEBHOOK_URL = ... # 環境変数から取得
#
# if not df_final_target.empty:
#     slack_notify_2fa_false(df_final_target, SLACK_WEBHOOK_URL)

🔭 まとめと今後の展望

今回は、IT資産管理SaaS「Admina」を拡張性の高いデータベース基盤と捉え、その“弱点”や“便利なキャッシュ機能”をGoogle CloudとPythonで徹底的に活用する3つの事例を紹介しました。

  • 弱点を補強する: 標準で取れないMACアドレスをMDMから取得して注入。
  • データを活用する: Adminaの情報を元に、リース台帳を自動生成。
  • キャッシュを利用する: Adminaが同期したSaaSアカウント情報を利用し、監査・通知をシンプルに実装。

このアプローチのキモは、「SaaSをそのまま使うのではなく、APIを通じて自社の業務フローに最適化させていく」という考え方です。 面倒な手作業はクラウドとAPIに任せ、我々は「どうすればもっと楽できるか?」「どうすればもっと安全になるか?」といった、よりクリエイティブな仕事に集中していきたいですね!

この記事が、どこかの情シス・コーポレートエンジニアの助けになれば幸いです!

Discussion