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に手を焼いている方のお役に立てば幸いです。