Amazon Web Services ブログ
Migration Evaluator エクスポートファイルの機密データを匿名化する
1. はじめに
AWS はワークロードを AWS へ移行する方針を決定するビジネスケースを、データに基づいて作成するのに役立つ無料の移行評価サービスとして Migration Evaluator を提供しています。これには、オンプレミスで実行されているサーバーワークロードと、その使用パターンを検出するデータ収集ツールが含まれています。
Migration Evaluator Collector で収集したデータを AWS Migration Evaluator チームが受け取ることで、ワークロードの適切なサイズの分析、Amazon EC2 へのマッピングを作成することができます。これには次の 2 つの方法があります:
- Migration Evaluator Collector は毎日 Migration Evaluator チームにデータを自動的に送信するよう設定することができます
- Migration Evaluator Collector はデータをファイルにエクスポートし、Migration Evaluator チームに向けて安全にアップロードすることができます
最初のオプションは、Migration Evaluator が評価の範囲を検証し、対象範囲のサーバーからの収集が成功していることを確認できるため、ほとんどのお客様に適しています。
Migration Evaluator が収集するインベントリデータには、サーバー名と IP アドレスが含まれます。お客様によっては、サーバー名と IP アドレスを社外に開示できないようにセキュリティ要件を設けている場合があります。これに対処するためには、データを AWS に送信する前に匿名化する必要があります。セキュリティ要件を満たすための最も適切なアプローチは、手動による匿名化ではなく、スクリプト化されたソリューションを使用することです。前者の場合、時間がかかりエラーも起こりやすいためです。
このブログでは、Migration Evaluator によって収集された機密データを匿名化するために、Python スクリプトを使用したソリューションを紹介します。
データが AWS で利用可能になったら、評価収集期間の終了時に分析され (こちらを参照) 、次の 2 つの成果物が提供されます:
- Quick Insights: 使用量パターンに基づき、AWS でリホストした場合の想定コスト削減額をインフラストラクチャとソフトウェアライセンスで分割した 1 ページの要約です。
 オンプレミスの検出データ (サーバーハードウェアのプロビジョニング、Microsoft SQL Serverの設定、およびリソースの使用状況) と、Amazon Elastic Compute Cloud (Amazon EC2) および Amazon Elastic Block Storage (Amazon EBS) へのリホストに関する推奨事項を組み合わせた、詳細な CSV エクスポートも可能です。
- Directional Business Case にはいくつかのセクションがあります: 
         - Microsoft Windows と Microsoft SQL Server のライセンス最適化分析による最適化とライセンス評価 (OLA) に加えて、ワークロードが適切なサイズにして Amazon EC2 に移行した場合のコストを示す、カスタマイズされた複数のコストモデルシナリオ
- 予想される CO2 削減量を示すサステナビリティ分析
- オプションで、検出の範囲に応じて、VMware Cloud on AWS、Amazon Relational Database Service
 (RDS)、Amazon WorkSpaces、AWS Elastic Disaster Recoveryなどの追加の AWS サービスの詳細も確認できます
 
2. ソリューション概要
サーバー名、ハイパーバイザーのホスト名、IP アドレスを匿名化するスクリプトについて説明します。このスクリプトには次の 2 つの機能があります:
- Migration Evaluator Collector のエクスポートファイルをインプットとして受け取り、匿名化されたバージョンをアウトプットとして生成します。出力ファイルには以下が含まれます: 
         - 匿名化されたサーバー名
- 匿名化されたハイパーバイザーホスト名
- IP アドレスなし
 
- Migration Evaluator Quick Insights の結果の ZIP ファイルをインプットとして受け取り、匿名化されていない結果をアウトプットとして生成します。
注: 匿名化されたデータと匿名化されていないデータのマッピングは、ランダムに生成された識別子を使用し、スクリプトを実行するシステムに保持されるため、AWS に送信されることはありません。
2.1 前提条件
- Python 3 (現行バージョン)
- openpyxl ライブラリがインストールされていること。openpyxl ライブラリをインストールするには、以下のコマンドを使用します:
 pip install openpyxl
- お好みのテキストエディタを使用して、”collector-anonymizer.py “というファイルを作成し、このブログの「3. ME-Collector のエクスポートファイルを匿名化する Python コード」をこのファイルにコピーします
2.2 Migration Evaluator Collector のエクスポートファイルのデータ匿名化
- エクスポートファイルをダウンロードし、 (必要な場合は) 注釈を付けます (詳細については、Installation Guide の Section 10 を参照してください) 。この例では、ファイル名「Inventory_And_Usage_Workbook-2023-03-24.xlsx」を使用します

サーバー名 (B列) とハイパーバイザー名 (H列) 示す仮想プロビジョニングシートの例
- 先ほど保存したpythonスクリプトを実行します
 python collector-anonymizer.py an Inventory_And_Usage_Workbook-2023-03-24.xlsx
出力されるファイル名は、Inventory_And_Usage_Workbook Anonymized.xlsx になります。ファイルを開き、匿名化の要件に従っていることを確認します。その後、Installation Guide の Section 10 で説明されているように、匿名化されたエクスポートを ME コンソールにアップロードすることができます。

匿名化されたサーバー名 (B列) とハイパーバイザー名 (H列) を示す仮想プロビジョニングシートの例
2.3 Migration Evaluator Quick Insights の非匿名化
Quick Insights の準備が整うと、メールで通知が届きます:
- ME コンソールに移動し、Quick Insights 標準フォーマットの zip ファイルをダウンロードして、元のエクスポートファイルと同じフォルダに配置します
- 以下のコマンドを実行して結果の匿名化を解除します。
 python collector-anonymizer.py de Inventory_And_Usage_Workbook-2023-03-24.xlsx standard-customernamemas-12345-mas-12345_2023-03-30-11-26-00.zip
この例では、元のエクスポートファイルの名前は Inventory_And_Usage_Workbook-2023-03-24.xlsx で、Quick Insights の zip ファイルの名前は standard-customernamemas-12345-mas-12345_2023-03-30-11-26-00.zip です。
 注: サンプルコマンドは、読みやすくするために 2 行に分割されています。コマンドは 1 行にする必要があります。
このスクリプトは、実際のサーバー名を含む Quick Insights の結果を含むファイルを出力し、収集した情報から Microsoft SQL Server が検出された場合は、Microsoft SQL Server 情報を含む 2 番目のファイルを出力します。
3. ME-Collector のエクスポートファイルを匿名化する Python コード
#Beginning of Script
#!/usr/bin/env python3
import csv
import zipfile
import argparse
import openpyxl
def get_column_indexes(sheets):
    """提供されているすべてのシートのシート名 -> 列名-> インデックス番号を含む dict を作成する"""
    headers = {}
    for sheet in sheets:
        headers[sheet.title] = {}
        for idx, column in enumerate(sheet.columns):
            headers[sheet.title][column[0].value] = idx + 1
    return headers
def anonymize(filename):
    """指定された Excel ファイルを匿名化し、アウトプットをを新しいファイルとして保存する"""
    wb = openpyxl.load_workbook(filename)
    # シート名の読み込み
    uti = wb["Utilization"]
    asset = wb["Asset Ownership"]
    virt = wb["Virtual Provisioning"]
    phys = wb["Physical Provisioning"]
    column_indexes = get_column_indexes([uti, asset, virt, phys])
    # 仮想プロビジョニングシートの Hypervisor Name をハイパーバイザーの固有識別子に置き換える
    for cell in list(virt.columns)[
        column_indexes["Virtual Provisioning"]["Hypervisor Name"] - 1
    ]:
        for b_cell in list(phys.columns)[
            column_indexes["Physical Provisioning"]["Human Name"] - 1
        ]:
            if b_cell.value == cell.value:
                virt.cell(
                    row=cell.row,
                    column=column_indexes["Virtual Provisioning"]["Hypervisor Name"],
                ).value = phys.cell(
                    row=b_cell.row,
                    column=column_indexes["Physical Provisioning"]["Unique Identifier"],
                ).value
    # すべてのシートで「人間の名前」を「一意の識別子」に置き換える
    for sheet in [uti, asset, virt, phys]:
        for row in range(2, sheet.max_row + 1):
            sheet.cell(
                row=row, column=column_indexes[sheet.title]["Human Name"]
            ).value = sheet.cell(
                row=row, column=column_indexes[sheet.title]["Unique Identifier"]
            ).value
    # 物理、仮想プロビジョニングシートの IP アドレスを削除
    for sheet in [phys, virt]:
        for cell in list(sheet.columns)[column_indexes[sheet.title]["Address"] - 1][1:]:
            cell.value = None
    wb.save("Inventory_And_Usage_Workbook Anonymized.xlsx")
    print(
        "Anonymization successful, Inventory_And_Usage_Workbook Anonymized has been created"
    )
def deanonymize(filename, qi):
    """元の Collector エクスポートファイルを使用して、指定された Quick Insights の zip ファイルの匿名化を解除する"""
    # Quick Insights の zip ファイルからファイル名を取得
    with zipfile.ZipFile(qi, "r") as z:
        zip_filenames = z.namelist()
        # 元の匿名化済みスクリプトの Asset Ownership と Physical Provisioning を取得
        wb = openpyxl.load_workbook(filename)
        asset = wb["Asset Ownership"]
        phys = wb["Physical Provisioning"]
        column_indexes = get_column_indexes([asset, phys])
        unique_id_to_hostname = {}
        for row in asset.rows:
            unique_id_to_hostname[
                row[
                    column_indexes["Asset Ownership"]["Unique Identifier"] - 1
                ].value.upper()
            ] = row[column_indexes["Asset Ownership"]["Human Name"] - 1].value
        for row in phys.rows:
            unique_id_to_hostname[
                row[
                    column_indexes["Physical Provisioning"]["Unique Identifier"] - 1
                ].value.upper()
            ] = row[column_indexes["Physical Provisioning"]["Human Name"] - 1].value
        # 匿名化されていないサーバーおよび SQL QI ファイルの作成
        for zip_file in zip_filenames:
            with z.open(zip_file) as f:
                file_string = f.read().decode("utf-8")
            reader = csv.reader(file_string.splitlines())
            headers = next(reader)
            csv_col_indexes = {}
            for idx, column in enumerate(headers):
                csv_col_indexes[column] = idx
            # 非匿名化ファイルの作成
            output_filename = "deanonymized_" + zip_file
            print(f"Creating output file: {output_filename}")
            with open(output_filename, "w", newline="") as g:
                writer = csv.writer(g)
                writer.writerow(headers)
                for row in reader:
                    # 「Server Name」列の値を元のホスト名に戻す
                    row[csv_col_indexes["Server Name"]] = unique_id_to_hostname[
                        row[csv_col_indexes["Server Id"]].upper()
                    ]
                    # 「Virtualization | Host Name」列の値を元のホスト名に戻す (列が存在し、値がある場合)
                    if (
                        "Virtualization | Host Name" in csv_col_indexes
                        and row[csv_col_indexes["Virtualization | Host Name"]]
                    ):
                        row[
                            csv_col_indexes["Virtualization | Host Name"]
                        ] = unique_id_to_hostname[
                            row[csv_col_indexes["Virtualization | Host Name"]].upper()
                        ]
                    # 変更された行を CSV に書き込む
                    writer.writerow(row)
if __name__ == "__main__":
    # 引数処理
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "method",
        help="The method to use, 'an' for anonymization or 'de' for de-anonymization",
    )
    parser.add_argument("filename", help="Inventory and Utilization Export file")
    parser.add_argument("QI", help="QI .zip file", nargs="?", default=None)
    args = parser.parse_args()
    if args.method == "an":
        anonymize(args.filename)
    elif args.method == "de":
        if args.QI is None:
            parser.error("QI is required for de-anonymization")
        deanonymize(args.filename, args.QI)
    else:
        parser.error("Invalid input. Please enter either 'an' or 'de'.")
#End of script4. おわりに
この投稿では、Migration Evaluator を使用するお客様が、サーバーのメタデータ (ホスト名、IPアドレス、サーバー名) を匿名化および非匿名化する簡単な方法を紹介しました。コードの最適化を手伝ってくれた Roger Trevor に感謝します。