Application Migration Service(MGN)の初回レプリケーション時間を記録する仕組み作ってみた

記事タイトルとURLをコピーする

CR課の前田です。今回はApplication Migration Service(以降MGNと表記)の話です。

前提

  • 本ブログではMGNとは何か?という話はいたしません。AWS Application Migration Service をグラレコで解説が非常に分かりやすいのでご覧ください。

  • 作成した仕組みはまだ実戦投入しておりません。思わぬ不具合がある可能性もあるのでご承知おきください。

  • 本ブログに記載する内容はエージェントベースにおけるレプリケーションを想定しています。エージェントレスベースでは検証しておりません。

  • 本文中では「初回レプリケーション」という言葉が出てきます。この言葉の定義は下記とします。

    • ソースサーバーからレプリケーションサーバーへデータレプリケーションが開始されてから、MGN上のステータスが「テストの準備完了」になるまでの時間

課題

MGNによる移行時、ソースサーバーからMGNへの初回レプリケーションにどのくらいの時間を要したか知りたいことがあります。

ソースサーバーの移行対象データ量によっては初回レプリケーションに時間を要するためボトルネックになりやすく、実績時間を記録しておくことで後続のソースサーバーの初回レプリケーション時間を予測し、移行スケジュールの管理に役立てたい…ということがあるためです。

しかし、コンソールのMGN管理画面やAWS CLIのMGN関連APIではMGNの初回レプリケーション時間は確認できません(※1)。惜しい項目として「レプリケーション経過時間」という項目はあるのですが、これは増分レプリケーションの時間もカウントされるため、カットオーバーが完了するまで続きます。

また、CloudTrailにも初回レプリケーション完了のイベントは記録されません(※1)。

※1…2025/2/10時点

解決策について

MGNの「テストの準備完了」ステータスをEventBridge ruleで検知して、Lambdaを起動します。Lambdaから初回レプリケーション時間やその関連情報をDynamoDBに記録する仕組みとなります。

EventBridge

MGNで管理しているソースサーバーのステータスが「テストの準備完了」となったこと(※1)をトリガーとしてLambdaを呼び出します。 イベントパターンには下記を登録しておきます。

{
  "detail-type": ["MGN Source Server Lifecycle State Change"],
  "source": ["aws.mgn"],
  "detail": {
    "state": ["READY_FOR_TEST"]
  }
}

※1…実際に検知されるイベントのサンプルはこちら。(Lambdaコード単体をテストしたい際もご利用できます。)

{
  "version": "0",
  "id": "fd9d9cd7-1d38-3059-dc84-1bd3f3e32d4",
  "detail-type": "MGN Source Server Lifecycle State Change",
  "source": "aws.mgn",
  "account": "123456789012",
  "time": "2025-02-10T04:08:22Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:mgn:us-east-1:123456789012:source-server/s-3af58bc13a359716b"
  ],
  "detail": {
    "state": "READY_FOR_TEST"
  }
}

Lambda

コードを表示

import json
import boto3
import traceback
from datetime import datetime, timedelta, timezone
from decimal import Decimal

# boto3クライアントの初期化など
mgn_client = boto3.client('mgn')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('mgn_initial_replication_info')
jst = timezone(timedelta(hours=9))

def lambda_handler(event, context):
    try:
        # イベントからソースサーバーIDとテスト準備完了になった時刻を抽出
        source_server_arn = event['resources'][0]
        source_server_id = source_server_arn.split('/')[-1]
        ready_for_test_time_utc = datetime.fromisoformat(event['time'].replace('Z', '+00:00'))

        # JSTに変換
        ready_for_test_time_jst = ready_for_test_time_utc.astimezone(jst)

        # ソースサーバー情報を取得
        source_server_info = mgn_client.describe_source_servers(
            filters={
                'sourceServerIDs': [
                    source_server_id
                ]
            }
        )

        # ソースサーバーに紐づくレプリケーション設定を取得
        replication_config = mgn_client.get_replication_configuration(
            sourceServerID=source_server_id
        )


        source_server_info_item = source_server_info['items'][0]

        # ホスト名を抽出
        hostname = source_server_info_item['sourceProperties']['identificationHints']['hostname']

        # レプリケーション開始時刻を抽出してJSTに変換
        replication_start_time_utc = datetime.fromisoformat(source_server_info_item['lifeCycle']['firstByteDateTime'])
        replication_start_time_jst = replication_start_time_utc.astimezone(jst)

        # レプリケートされたストレージ量を抽出・合計してGBに変換
        replicated_total_storage_bytes = sum(disk['replicatedStorageBytes'] for disk in source_server_info_item['dataReplicationInfo']['replicatedDisks'])
        replicated_total_storage_gb = Decimal(replicated_total_storage_bytes) / Decimal(1024 ** 3)

        # 初回レプリケーションに要した時間を分単位で算出
        initial_replication_duration_minutes = int((ready_for_test_time_jst - replication_start_time_jst).total_seconds() / 60)

        # レプリケーション時に設定されていた帯域(Mbps)を抽出
        bandwidth_throttling = replication_config['bandwidthThrottling']

        # レプリケーションサーバーのインスタンスタイプを抽出
        replication_server_instance_type = replication_config['replicationServerInstanceType']

        # DynamoDBにデータを保存。
        # 本Lambda起動トリガーとなるMGNの「テストの準備完了」ステータスはユーザ操作で再び発生する可能性あり、初回レプリケーション時間が上書きされると不都合なので上書きを禁止する。
        table.put_item(
            Item={
                'SourceServerID': source_server_id,
                'Hostname': hostname,
                'ReplicationStartTimeJST': replication_start_time_jst.isoformat(),
                'ReadyForTestTimeJST': ready_for_test_time_jst.isoformat(),
                'InitialReplicationDurationMinutes': initial_replication_duration_minutes,
                'ReplicatedTotalStorageGB': replicated_total_storage_gb,
                'BandwidthThrottlingMbps': bandwidth_throttling,
                'ReplicationServerInstanceType': replication_server_instance_type
            },    
            Expected={
                'SourceServerID': {
                    "Exists": False
                }
            }
        )

        return {
            'statusCode': 200,
            'body': json.dumps('Success')
        }

    except Exception as e:
        # エラー時にスタックトレースを出力
        traceback.print_exc()
        return {
            'statusCode': 500,
            'body': json.dumps('Error: ' + str(e))
        }

大まかに下記のような処理を実施しています。

  • 1.受け付けたイベントから下記を抽出。

    • ソースサーバーID

    • 「テストの準備完了」になった時刻

  • 2.ソースサーバーIDをキーとしてソースサーバー情報(※1)を取得し、下記を抽出。「初回レプリケーション時間」の算出なども実施する。

    • ホスト名

    • レプリケーション開始時刻

    • レプリケートされたストレージ量の合計

  • 3.ソースサーバーIDをキーとしてソースサーバー固有のレプリケーション設定(※2)を取得し、下記を抽出

    • レプリケーション時の帯域制御値(Mbps)

    • レプリケーションサーバーのインスタンスタイプ

  • 4.抽出した情報をDynamoDBに格納する(※3)

※1…取得できるソースサーバー情報のサンプルはこちら `

{
    "items": [
        {
            "arn": "arn:aws:mgn:us-east-1:123456789012:source-server/s-3af58bc13a359716b",
            "connectorAction": {},
            "dataReplicationInfo": {
                "dataReplicationInitiation": {
                    "nextAttemptDateTime": "2025-02-10T04:18:00+00:00",
                    "startDateTime": "2025-02-10T03:47:06.511270+00:00",
                    "steps": [
                        {
                            "name": "WAIT",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "CREATE_SECURITY_GROUP",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "LAUNCH_REPLICATION_SERVER",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "BOOT_REPLICATION_SERVER",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "AUTHENTICATE_WITH_SERVICE",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "DOWNLOAD_REPLICATION_SOFTWARE",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "CREATE_STAGING_DISKS",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "ATTACH_STAGING_DISKS",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "PAIR_REPLICATION_SERVER_WITH_AGENT",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "CONNECT_AGENT_TO_REPLICATION_SERVER",
                            "status": "SUCCEEDED"
                        },
                        {
                            "name": "START_DATA_TRANSFER",
                            "status": "SUCCEEDED"
                        }
                    ]
                },
                "dataReplicationState": "CONTINUOUS",
                "lagDuration": "P0D",
                "lastSnapshotDateTime": "2025-02-10T03:54:48.926888+00:00",
                "replicatedDisks": [
                    {
                        "backloggedStorageBytes": 0,
                        "deviceName": "/dev/xvdf",
                        "replicatedStorageBytes": 8589934592,
                        "rescannedStorageBytes": 8589934592,
                        "totalStorageBytes": 8589934592
                    },
                    {
                        "backloggedStorageBytes": 0,
                        "deviceName": "/dev/xvda",
                        "replicatedStorageBytes": 8589934592,
                        "rescannedStorageBytes": 8589934592,
                        "totalStorageBytes": 8589934592
                    }
                ]
            },
            "isArchived": false,
            "lifeCycle": {
                "addedToServiceDateTime": "2025-02-10T03:38:21.214723+00:00",
                "elapsedReplicationDuration": "PT23M56.654198S",
                "firstByteDateTime": "2025-02-10T03:49:46.788605+00:00",
                "lastCutover": {
                    "finalized": {},
                    "initiated": {},
                    "reverted": {}
                },
                "lastSeenByServiceDateTime": "2025-02-10T04:09:30.728968+00:00",
                "lastTest": {
                    "finalized": {},
                    "initiated": {},
                    "reverted": {}
                },
                "state": "READY_FOR_TEST"
            },
            "replicationType": "AGENT_BASED",
            "sourceProperties": {
                "cpus": [
                    {
                        "cores": 1,
                        "modelName": "Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz"
                    }
                ],
                "disks": [
                    {
                        "bytes": 8589934592,
                        "deviceName": "/dev/xvda"
                    },
                    {
                        "bytes": 8589934592,
                        "deviceName": "/dev/xvdf"
                    }
                ],
                "identificationHints": {
                    "awsInstanceID": "i-0f8b3103e1eea1c7f",
                    "fqdn": "ip-10-1-10-191.us-east-2.compute.internal",
                    "hostname": "ip-10-1-10-191.us-east-2.compute.internal"
                },
                "lastUpdatedDateTime": "2025-02-10T03:39:31.076897+00:00",
                "networkInterfaces": [
                    {
                        "ips": [
                            "10.1.10.191"
                        ],
                        "isPrimary": true,
                        "macAddress": "02-20-4D-40-E6-B1"
                    }
                ],
                "os": {
                    "fullString": "Linux version 4.14.355-275.582.amzn2.x86_64 (mockbuild@ip-10-0-48-186) (gcc version 7.3.1 20180712 (Red Hat 7.3.1-17) (GCC)) #1 SMP Sat Jan 25 09:56:35 UTC 2025"
                },
                "ramBytes": 1017335808,
                "recommendedInstanceType": "c5.large"
            },
            "sourceServerID": "s-3af58bc13a359716b",
            "tags": {
                "Name": "ip-10-1-10-191.us-east-2.compute.internal"
            }
        }
    ]
}

※2…取得できるレプリケーション設定のサンプルはこちら `

{
    "associateDefaultSecurityGroup": true,
    "bandwidthThrottling": 300,
    "createPublicIP": true,
    "dataPlaneRouting": "PUBLIC_IP",
    "createPublicIP": true,
    "dataPlaneRouting": "PUBLIC_IP",
    "defaultLargeStagingDiskType": "GP3",
    "ebsEncryption": "DEFAULT",
    "name": "Data Replication Configuration for Source Server s-3af58bc13a359716b",
    "replicatedDisks": [
        {
            "deviceName": "/dev/xvdf",
            "isBootDisk": false,
            "stagingDiskType": "AUTO"
        },
        {
            "deviceName": "/dev/xvda",
            "isBootDisk": true,
            "stagingDiskType": "AUTO"
        }
    ],
    "replicationServerInstanceType": "t3.small",
    "replicationServersSecurityGroupsIDs": [],
    "sourceServerID": "s-3af58bc13a359716b",
    "stagingAreaSubnetId": "subnet-09c0e0ff84bda10da",
    "stagingAreaTags": {},
    "useDedicatedReplicationServer": false,
    "useFipsEndpoint": false
}

※3…「テストの準備完了」イベントはユーザーの操作次第で再び発生する可能性があり、更新されてしまうと不正確な情報が記録されてしまうのでDynamoDBへの上書きは禁止しています。

DynamoDB

初回レプリケーション時間の実績を見返せるようにDynamoDBに記録する方式としました。項目と説明は下記の通りです。
注意点として、DynamoDBテーブルに登録される項目は検証でお見せするように順不同になるので、記録を確認するのはCSV形式に落としてデータ加工するなど別の手段を用いた方が良いです。

項目 備考
SourceServerID ソースサーバーID。DynamoDBテーブルのパーティションキー。
Hostname ソースサーバーのホスト名
ReplicationStartTimeJST レプリケーション開始時刻。
ReadyForTestTimeJST 「テストの準備完了」になった時刻。
InitialReplicationDurationMinutes 初回レプリケーション時間(分単位)。レプリケーション開始時刻と「テストの準備完了」になった時刻の差分。
ReplicatedTotalStorageGB レプリケートされたストレージ量の合計。
BandwidthThrottlingMbps MGNから指定出来る、レプリケーション時の帯域制御(Mbps単位)。0の場合は設定なし。
ReplicationServerInstanceType レプリケーションサーバーのインスタンスタイプ。レプリケーションに影響することがあるため収集。

解決策の仕組みを実装出来るCloudFormationテンプレート

テンプレート本文 `

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template to create EventBridge, Lambda, IAM Role for Lambda, and DynamoDB table for MGN.
Resources:
  MgnDynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: mgn_initial_replication_info
      AttributeDefinitions:
        - AttributeName: SourceServerID
          AttributeType: S
      KeySchema:
        - AttributeName: SourceServerID
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST
  MgnLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: role_for_function_get_info_mgn_initial_replication
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: LambdaPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - dynamodb:PutItem
                  - mgn:DescribeSourceServers
                  - mgn:GetReplicationConfiguration
                Resource: "*"
  MgnLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: function_get_info_mgn_initial_replication
      Handler: index.lambda_handler
      Role: !GetAtt MgnLambdaExecutionRole.Arn
      Runtime: python3.13
      Timeout: 60
      Code:
        ZipFile: |
          import json
          import boto3
          import traceback
          from datetime import datetime, timedelta, timezone
          from decimal import Decimal

          # boto3クライアントの初期化など
          mgn_client = boto3.client('mgn')
          dynamodb = boto3.resource('dynamodb')
          table = dynamodb.Table('mgn_initial_replication_info')
          jst = timezone(timedelta(hours=9))

          def lambda_handler(event, context):
              try:
                  # イベントからソースサーバーIDとテスト準備完了になった時刻を抽出
                  source_server_arn = event['resources'][0]
                  source_server_id = source_server_arn.split('/')[-1]
                  ready_for_test_time_utc = datetime.fromisoformat(event['time'].replace('Z', '+00:00'))

                  # JSTに変換
                  ready_for_test_time_jst = ready_for_test_time_utc.astimezone(jst)

                  # ソースサーバー情報を取得
                  source_server_info = mgn_client.describe_source_servers(
                      filters={
                          'sourceServerIDs': [
                              source_server_id
                          ]
                      }
                  )

                  # ソースサーバーに紐づくレプリケーション設定を取得
                  replication_config = mgn_client.get_replication_configuration(
                      sourceServerID=source_server_id
                  )


                  source_server_info_item = source_server_info['items'][0]

                  # ホスト名を抽出
                  hostname = source_server_info_item['sourceProperties']['identificationHints']['hostname']

                  # レプリケーション開始時刻を抽出してJSTに変換
                  replication_start_time_utc = datetime.fromisoformat(source_server_info_item['lifeCycle']['firstByteDateTime'])
                  replication_start_time_jst = replication_start_time_utc.astimezone(jst)

                  # レプリケートされたストレージ量を抽出・合計してGBに変換
                  replicated_total_storage_bytes = sum(disk['replicatedStorageBytes'] for disk in source_server_info_item['dataReplicationInfo']['replicatedDisks'])
                  replicated_total_storage_gb = Decimal(replicated_total_storage_bytes) / Decimal(1024 ** 3)

                  # 初回レプリケーションに要した時間を分単位で算出
                  initial_replication_duration_minutes = int((ready_for_test_time_jst - replication_start_time_jst).total_seconds() / 60)

                  # レプリケーション時に設定されていた帯域(Mbps)を抽出
                  bandwidth_throttling = replication_config['bandwidthThrottling']

                  # レプリケーションサーバーのインスタンスタイプを抽出
                  replication_server_instance_type = replication_config['replicationServerInstanceType']

                  # DynamoDBにデータを保存。
                  # 本Lambda起動トリガーとなるMGNの「テストの準備完了」ステータスはユーザ操作で再び発生する可能性あり、初回レプリケーション時間が上書きされると不都合なので上書きを禁止する。
                  table.put_item(
                      Item={
                          'SourceServerID': source_server_id,
                          'Hostname': hostname,
                          'ReplicationStartTimeJST': replication_start_time_jst.isoformat(),
                          'ReadyForTestTimeJST': ready_for_test_time_jst.isoformat(),
                          'InitialReplicationDurationMinutes': initial_replication_duration_minutes,
                          'ReplicatedTotalStorageGB': replicated_total_storage_gb,
                          'BandwidthThrottlingMbps': bandwidth_throttling,
                          'ReplicationServerInstanceType': replication_server_instance_type
                      },    
                      Expected={
                          'SourceServerID': {
                              "Exists": False
                          }
                      }
                  )

                  return {
                      'statusCode': 200,
                      'body': json.dumps('Success')
                  }

              except Exception as e:
                  # エラー時にスタックトレースを出力
                  traceback.print_exc()
                  return {
                      'statusCode': 500,
                      'body': json.dumps('Error: ' + str(e))
                  }
                  
  MgnEventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Name: rule_for_function_get_info_mgn_initial_replication
      EventPattern:
        detail-type:
          - "MGN Source Server Lifecycle State Change"
        source:
          - "aws.mgn"
        detail:
          state:
            - "READY_FOR_TEST"
      Targets:
        - Arn: !GetAtt MgnLambdaFunction.Arn
          Id: "MgnLambdaFunction"
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref MgnLambdaFunction
      Action: "lambda:InvokeFunction"
      Principal: "events.amazonaws.com"
      SourceArn: !GetAtt MgnEventBridgeRule.Arn
Outputs:
  DynamoDBTableName:
    Description: "Name of the DynamoDB Table"
    Value: !Ref MgnDynamoDBTable
  LambdaFunctionName:
    Description: "Name of the Lambda Function"
    Value: !Ref MgnLambdaFunction
  EventBridgeRuleName:
    Description: "Name of the EventBridge Rule"
    Value: !Ref MgnEventBridgeRule

Github Gistにも記載しております。

動作確認

今回はMGNハンズオンで用意出来るEC2をもとに検証を実施します。

初回レプリケーション時間記録用の仕組み実装

MGNをセットアップしているバージニア北部リージョンにて、前述のCloudFormationを実行します。 特に指定するパラメータはありません。また、リソースにリージョン依存するパラメータもないため、どのリージョンでも使用できるかと思います。 無事に作成に成功しました。

検証用EC2の準備

オハイオリージョンにおいてMGNハンズオン_CloudFormationでVPC,EC2を作成の手順を実行し、検証用EC2をセットアップします。尚、今回の検証にはOSがAmazon Linuxの方のEC2を使用します。


作成されたEC2には追加でEBSをアタッチしておき合計18GBのストレージ容量がある状態にしておきます。複数のストレージ容量が合算されてDynamoDBに記録出来るか確認するためです。

MGNハンズオン_移行元サーバーの構築(Linux)の手順を実行し、レプリケーションエージェントをインストールします。インストールと同時にレプリケーションが開始されます。

AWSコンソール側では画像のような状態になっています。

ソースサーバーのステータスが「テストの準備完了」になった後

初回レプリケーション完了後、ステータスが「テストの準備完了」となっており、コンソールでは下記画像のように表示されています。

DynamoDBを見ると記録されていることが確認出来ます。前述のように、DynamoDBテーブルの項目は順不同となりますので記録を確認する場合はExcelなどでご確認下さい。

DynamoDBテーブル上の記録を転載したものが下記になります。初回レプリケーション時間は14分かかったことが確認できました。 また、レプリケートされたストレージ量の合計も想定通り18GBになっています。

SourceServerID Hostname ReplicationStartTimeJST ReadyForTestTimeJST InitialReplicationDurationMinutes ReplicatedTotalStorageGB BandwidthThrottlingMbps ReplicationServerInstanceType
s-3a2a83fcdee292049 ip-10-1-10-150.us-east-2.compute.internal 2025-02-10T21:51:42.303447+09:00 2025-02-10T22:06:09+09:00 14 18 0 t3.small

感想

今回は結構役立つものが作れたのではないかなと思いますが、PythonコードやCloudFormationテンプレート作成にはChatGPTの力をめちゃくちゃ借りているので、そんなに大きい顔は出来ません…。 このブログが、MGNに手を焼いている方のお役に立てば幸いです。

前田 青秀(執筆記事の一覧)

2023年2月入社 技術4課改めCR課

AWS資格12冠

ジムに通い始めましたが、なるべく楽してマッチョになりたい…