[Amazon FSx for NetApp ONTAP] SnapMirror relationshipのHealthをCloudWatchメトリクスにPUTしてみた

[Amazon FSx for NetApp ONTAP] SnapMirror relationshipのHealthをCloudWatchメトリクスにPUTしてみた

FSxNのサービスとして提供するCloudWatchメトリクスに追加されることを願う
Clock Icon2024.11.18

SnapMirrorの転送エラーを検知したい

以下記事でも触れているとおり、SnapMirror relationshipのHealthはCloudWatchメトリクスは存在しません。

https://dev.classmethod.jp/articles/amazon-fsx-for-netapp-ontap-smb-fileserver-operations/

そのため、以下のいずれかの対応が必要になります。

  • NetAppのData infrastructure InsightでSnapMirror relationshipのHealthを監視する
  • SnapMirror relationshipの状態をPutMetricDataするVPC Lambdaを定期実行し、そのカスタムメトリクスをベースにCloudWatchアラームを設定する

前者のData infrastructure Insight(以降DII)はNetAppが提供しているSaaSです。

FSxNのデータをDIIへ連携するにあたってAUという役割をするサーバーが必要になります。EC2インスタンスにインストールすることも可能です。

ただし、EC2インスタンスを用意するとなると、どうしても運用コストや金銭的コストが気になります。

そのため、SnapMirrorの転送エラーを検知したいがためだけにDIIを導入するのは少しコスト的に気になるのではないでしょうか。

DIIをその他の用途を利用する想定がない場合は、後者の方法を採用したいところです。

ということで実際に後者の対応をしてみました。

使用するコードの構成

Lambda関数で実行する処理

Lambda関数で実行する処理は以下のとおりです。

  • CloudWatchクライアントの初期化
  • AWS Parameter and Secrets Lambda extension で SSM Parameter Storeに保存している認証情報を取得
  • FSxNファイルシステムに接続し、SnapMirror relationshipの情報を取得
  • SnapMirror relationship個別のHealthをカスタムメトリクスとしてPUT
    • HealthがTrueの場合は1
    • HealthがFalseの場合は0
  • DestinationのSVM単位でSnapMirror relationshipのHealthをカスタムメトリクスとしてPUT
    • Destination SVM 単位で、全てのSnapMirror relationshipのHealthがTrueの場合は1
    • Destination SVM 単位で、いずれかのSnapMirror relationshipのHealthがFalseの場合は0

AWS Parameter and Secrets Lambda extensionでSSM Parameter Storeにアクセスする際に、not ready to serve traffic, please waitと400エラーが返ってくる時があります。緩和策としてExponential Backoffで複数回リトライするような処理を組み込んでいます。

実際のログは以下のとおりです。

{
    "time": "2024-11-17T01:52:37.563Z",
    "type": "platform.initStart",
    "record": {
        "initializationType": "on-demand",
        "phase": "init",
        "runtimeVersion": "python:3.12.v38",
        "runtimeVersionArn": "arn:aws:lambda:us-east-1::runtime:6ea5f72a8a27124ba0bc2bb6d2d4094f8560fac75ead0a3d8434a209061a5566",
        "functionName": "MonitoringSnapmirrorRelati-LambdaConstruct4CD6E168-VaruG0t6vFp6",
        "functionVersion": "$LATEST"
    }
}
[AWS Parameters and Secrets Lambda Extension] 2024/11/17 01:52:37 PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL is info. Log level set to info.
[AWS Parameters and Secrets Lambda Extension] 2024/11/17 01:52:37 INFO Systems Manager Parameter Store and Secrets Manager Lambda Extension 1.0.103
[AWS Parameters and Secrets Lambda Extension] 2024/11/17 01:52:37 INFO Serving on port 2773
{
    "timestamp": "2024-11-17T01:52:42Z",
    "level": "INFO",
    "message": "successfully patched module httplib",
    "logger": "aws_xray_sdk.core.patcher",
    "requestId": ""
}

{
    "timestamp": "2024-11-17T01:52:42Z",
    "level": "INFO",
    "message": "successfully patched module sqlite3",
    "logger": "aws_xray_sdk.core.patcher",
    "requestId": ""
}

{
    "timestamp": "2024-11-17T01:52:42Z",
    "level": "INFO",
    "message": "successfully patched module requests",
    "logger": "aws_xray_sdk.core.patcher",
    "requestId": ""
}

{
    "timestamp": "2024-11-17T01:52:42Z",
    "level": "INFO",
    "message": "successfully patched module botocore",
    "logger": "aws_xray_sdk.core.patcher",
    "requestId": ""
}

{
    "timestamp": "2024-11-17T01:52:42Z",
    "level": "INFO",
    "message": "Found credentials in environment variables.",
    "logger": "botocore.credentials",
    "requestId": ""
}

{
    "time": "2024-11-17T01:52:43.044Z",
    "type": "platform.extension",
    "record": {
        "name": "AWSParametersAndSecretsLambdaExtension",
        "state": "Ready",
        "events": [
            "INVOKE",
            "SHUTDOWN"
        ]
    }
}
{
    "time": "2024-11-17T01:52:43.046Z",
    "type": "platform.start",
    "record": {
        "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694",
        "version": "$LATEST",
        "tracing": {
            "spanId": "52abbf594f8c3bb0",
            "type": "X-Amzn-Trace-Id",
            "value": "Root=1-67394c65-6fef1dac569797352a348fc4;Parent=73517fbe3d0fdcff;Sampled=1"
        }
    }
}
{
    "timestamp": "2024-11-17T01:52:43Z",
    "level": "INFO",
    "message": "Requesting SSM parameter from: http://localhost:2773/systemsmanager/parameters/get/?name=/fsxn/non-97-fsxn/fsxadmin-readonly/password&withDecryption=true",
    "logger": "index",
    "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694"
}

[AWS Parameters and Secrets Lambda Extension] 2024/11/17 01:52:44 INFO ready to serve traffic
{
    "timestamp": "2024-11-17T01:52:44Z",
    "level": "WARNING",
    "message": "Extension not ready. Retrying in 1 seconds. Attempt 1/4",
    "logger": "index",
    "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694"
}

{
    "timestamp": "2024-11-17T01:52:49Z",
    "level": "INFO",
    "message": "Successfully put metric data: SnapMirrorRelationshipHealth",
    "logger": "index",
    "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694"
}

{
    "timestamp": "2024-11-17T01:52:49Z",
    "level": "INFO",
    "message": "Successfully put metric data: SnapMirrorRelationshipHealth",
    "logger": "index",
    "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694"
}

{
    "timestamp": "2024-11-17T01:52:49Z",
    "level": "INFO",
    "message": "Unhealthy SnapMirror Relationship detected Unhealthy Reason: [{'code': '13303943', 'message': 'SnapMirror relationship is unhealthy. Reason: Scheduled update failed to start. (Destination svm:vol_ntfs_dst must be a data-protection volume.).'}], Relationship UUID: 8a6d0455-9b21-11ef-accd-b31c82a68aa5, Source Path: svm:vol_ntfs, Destination Path: svm:vol_ntfs_dst",
    "logger": "index",
    "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694"
}

{
    "timestamp": "2024-11-17T01:52:49Z",
    "level": "INFO",
    "message": "Successfully put metric data: SnapMirrorRelationshipHealth",
    "logger": "index",
    "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694"
}

{
    "time": "2024-11-17T01:52:49.531Z",
    "type": "platform.report",
    "record": {
        "requestId": "88eeef8e-4c34-4175-bcb0-f3392150e694",
        "metrics": {
            "durationMs": 6483.897,
            "billedDurationMs": 6484,
            "memorySizeMB": 128,
            "maxMemoryUsedMB": 122,
            "initDurationMs": 5481.946
        },
        "tracing": {
            "spanId": "52abbf594f8c3bb0",
            "type": "X-Amzn-Trace-Id",
            "value": "Root=1-67394c65-6fef1dac569797352a348fc4;Parent=73517fbe3d0fdcff;Sampled=1"
        },
        "status": "success"
    }
}

この時のX-Rayのトレース結果は以下のとおりです。

3.初回実行時のX-Rayのトレース結果.png

http://localhost:2773/systemsmanager/parameters/getが400エラーになっていることがわかります。

キャッシュが効いている場合は以下のようなログが出力されます。

{
    "time": "2024-11-17T01:52:58.916Z",
    "type": "platform.start",
    "record": {
        "requestId": "c28c37b6-2510-4faa-b7a6-88c69d2faf0e",
        "version": "$LATEST",
        "tracing": {
            "spanId": "40be7a26287f3d90",
            "type": "X-Amzn-Trace-Id",
            "value": "Root=1-67394c7a-6ba543397d04c1e75aa2612e;Parent=3f9f22cae872f208;Sampled=1"
        }
    }
}
[AWS Parameters and Secrets Lambda Extension] 2024/11/17 01:52:58 INFO ready to serve traffic
{
    "timestamp": "2024-11-17T01:52:58Z",
    "level": "INFO",
    "message": "Requesting SSM parameter from: http://localhost:2773/systemsmanager/parameters/get/?name=/fsxn/non-97-fsxn/fsxadmin-readonly/password&withDecryption=true",
    "logger": "index",
    "requestId": "c28c37b6-2510-4faa-b7a6-88c69d2faf0e"
}

{
    "timestamp": "2024-11-17T01:53:00Z",
    "level": "INFO",
    "message": "Successfully put metric data: SnapMirrorRelationshipHealth",
    "logger": "index",
    "requestId": "c28c37b6-2510-4faa-b7a6-88c69d2faf0e"
}

{
    "timestamp": "2024-11-17T01:53:00Z",
    "level": "INFO",
    "message": "Successfully put metric data: SnapMirrorRelationshipHealth",
    "logger": "index",
    "requestId": "c28c37b6-2510-4faa-b7a6-88c69d2faf0e"
}

{
    "timestamp": "2024-11-17T01:53:00Z",
    "level": "INFO",
    "message": "Unhealthy SnapMirror Relationship detected Unhealthy Reason: [{'code': '13303943', 'message': 'SnapMirror relationship is unhealthy. Reason: Scheduled update failed to start. (Destination svm:vol_ntfs_dst must be a data-protection volume.).'}], Relationship UUID: 8a6d0455-9b21-11ef-accd-b31c82a68aa5, Source Path: svm:vol_ntfs, Destination Path: svm:vol_ntfs_dst",
    "logger": "index",
    "requestId": "c28c37b6-2510-4faa-b7a6-88c69d2faf0e"
}

{
    "timestamp": "2024-11-17T01:53:00Z",
    "level": "INFO",
    "message": "Successfully put metric data: SnapMirrorRelationshipHealth",
    "logger": "index",
    "requestId": "c28c37b6-2510-4faa-b7a6-88c69d2faf0e"
}

{
    "time": "2024-11-17T01:53:00.771Z",
    "type": "platform.report",
    "record": {
        "requestId": "c28c37b6-2510-4faa-b7a6-88c69d2faf0e",
        "metrics": {
            "durationMs": 1854.397,
            "billedDurationMs": 1855,
            "memorySizeMB": 128,
            "maxMemoryUsedMB": 122
        },
        "tracing": {
            "spanId": "40be7a26287f3d90",
            "type": "X-Amzn-Trace-Id",
            "value": "Root=1-67394c7a-6ba543397d04c1e75aa2612e;Parent=3f9f22cae872f208;Sampled=1"
        },
        "status": "success"
    }
}

この時のX-Rayのトレース結果は以下のとおりです。

4.2回目実行時のX-Rayトレース結果.png

実行時間はおおよそ2秒ほどです。コールドスタートの場合は14秒ほどかかることもあるのでかなり速くなりました。

とはいえ、SnapMirror relationshipの情報を取得する処理で1秒弱、カスタムメトリクスをPUTする処理でも1秒弱時間がかかっていそうですね。SnapMirror relationship数が多い場合はタイムアウト値に注意が必要そうです。

また、2分間隔でしばらく実行しましたが、実行時間はおおよそ1~2秒ほどでした。

6.通常時の実行時間は2秒ほど.png

コールドスタートとなる明確な条件は明らかになっていませんが、体感的には5分以上間隔が開くとコールドスタートになりそうです。

5.5分ほど間隔が開くとコールドスタートになる.png

SnapMirror relationshipの情報の取得はONTAP REST APIのPythonクライアントライブラリを使用しました。

https://pypi.org/project/netapp-ontap/

Pythonクライアントライブラリのリファレンスは以下にまとまっています。

https://docs.netapp.com/ja-jp/ontap-automation/python/packages.html

SnapMirror relationship関連の操作は以下ドキュメントです。

https://library.netapp.com/ecmdocs/ECMLP3319064/html/resources/snapmirror_relationship.html

細かいパラメーターを知りたい場合はONTAP REST APIのドキュメントも確認しておくと良いでしょう。

https://docs.netapp.com/us-en/ontap-restapi-9141/ontap/get-snapmirror-relationships.html#examples

Pythonクライアントライブラリのファーストステップは以下サイトが参考になります。

https://netapp.io/2020/06/09/ontap-rest-api-python-client-library-pt1/

https://www.netapp.com/blog/simplify-ontap-rest-api-consumption/

実際のLambda関数のコードは以下のとおりです。

/lib/src/lambda/index.py
import os
import logging
import json
import urllib.parse
import urllib.request
import urllib.error
import boto3
import sys
import time
from typing import Dict, List, Any
from collections import defaultdict
from botocore.exceptions import ClientError, BotoCoreError
from netapp_ontap import config, HostConnection
from netapp_ontap.resources import SnapmirrorRelationship
from netapp_ontap.error import NetAppRestError
from aws_xray_sdk.core import patch_all

# 各種定義
NAMESPACE = os.environ.get('NAMESPACE', 'ONTAP/SnapMirror')
PARAMETERS_SECRETS_EXTENSION_HTTP_PORT = os.environ.get('PARAMETERS_SECRETS_EXTENSION_HTTP_PORT', '2773').upper()
SSM_ENDPOINT = 'http://localhost:' + PARAMETERS_SECRETS_EXTENSION_HTTP_PORT
SSM_PATH = '/systemsmanager/parameters/get/'
MAX_RETRIES = 4
INITIAL_DELAY = 1
MAX_DELAY = 4

# Loggerの設定
def setup_logger() -> logging.Logger:
    log_level = os.environ.get('LOG_LEVEL', 'INFO').upper()
    logging.basicConfig(
        level=getattr(logging, log_level),
        format='%(asctime)s [%(levelname)s] %(name)s %(message)s'
    )
    return logging.getLogger(__name__)

logger = setup_logger()

# urllib にパッチ適用するには 二重パッチ適用が必要
patch_all(double_patch=True)

# CloudWatch クライアントの初期化
try:
    cloudwatch = boto3.client('cloudwatch')
except (ClientError, BotoCoreError) as e:
    logger.error(f"Failed to initialize CloudWatch client: {e}")
    sys.exit(1)

# AWS Parameter and Secrets Lambda extension で SSM Parameter StoreのSecure Stringを取得
def get_ssm_parameter(parameter_name: str) -> str:
    encoded_name = urllib.parse.quote(parameter_name)
    url = f"{SSM_ENDPOINT}{SSM_PATH}?name={encoded_name}&withDecryption=true"
    headers = {'X-Aws-Parameters-Secrets-Token': os.environ['AWS_SESSION_TOKEN']}

    logger.info(f"Requesting SSM parameter from: {url}")

    # "not ready to serve traffic, please wait" とエラーになることがあるため、その場合はExponential Backoffしながらリトライ
    for attempt in range(MAX_RETRIES):
        try:
            req = urllib.request.Request(url, headers=headers)
            with urllib.request.urlopen(req) as response:
                response_data = response.read().decode('utf-8')
            parameter = json.loads(response_data)
            return parameter['Parameter']['Value']
        except urllib.error.HTTPError as e:
            if e.code == 400 and "not ready to serve traffic, please wait" in e.read().decode('utf-8'):
                delay = min(INITIAL_DELAY * (2 ** attempt), MAX_DELAY)
                logger.warning(f"Extension not ready. Retrying in {delay} seconds. Attempt {attempt + 1}/{MAX_RETRIES}")
                time.sleep(delay)
            else:
                logger.error(f"HTTP Error {e.code}: {e.reason}")
                logger.error(f"Error response body: {e.read().decode('utf-8')}")
                raise
        except (urllib.error.URLError, TimeoutError) as e:
            logger.error(f"Error fetching SSM parameter: {e}")
            raise
        except (json.JSONDecodeError, KeyError) as e:
            logger.error(f"Error parsing SSM parameter response: {e}")
            raise

    logger.error("Failed to retrieve SSM parameter after all retries.")
    raise Exception("Max retries reached for SSM parameter retrieval")

# FSxNへの接続
def get_ontap_connection() -> HostConnection:
    try:
        password = get_ssm_parameter(os.environ['FSXN_USER_CREDENTIAL_SSM_PARAMETER_STORE_NAME'])
        return HostConnection(
            os.environ['FSXN_DNS_NAME'],
            username=os.environ['FSXN_USER_NAME'],
            password=password,
            verify=False
        )
    except KeyError as e:
        logger.error(f"Missing environment variable: {e}")
        raise
    except Exception as e:
        logger.error(f"Error setting up ONTAP connection: {e}")
        raise

# SnapMirror relationshipの取得
def get_snapmirror_relationships() -> List[SnapmirrorRelationship]:
    # SnapMirror relationshipの取得
    try:
        return SnapmirrorRelationship.get_collection(fields='*')
    except NetAppRestError as error:
        logger.error(f"Error fetching SnapMirror relationships: {error}")
        raise

# CloudWatchのメトリクスデータのPUT
def put_metric_data(metric_name: str, value: float, dimensions: List[Dict[str, str]]) -> None:
    try:
        cloudwatch.put_metric_data(
            Namespace=NAMESPACE,
            MetricData=[
                {
                    'MetricName': metric_name,
                    'Value': value,
                    'Dimensions': dimensions
                }
            ]
        )
        logger.info(f"Successfully put metric data: {metric_name}")
    except ClientError as e:
        error_code = e.response['Error']['Code']
        error_message = e.response['Error']['Message']
        logger.error(f"ClientError putting metric data: {error_code} - {error_message}")
    except BotoCoreError as e:
        logger.error(f"BotoCoreError putting metric data: {e}")
    except Exception as e:
        logger.error(f"Unexpected error putting metric data: {e}")

# 取得したSnapMirror relationshipの評価とレポーティング
def evaluate_and_report_snapmirror_health(relationships: List[SnapmirrorRelationship]) -> None:
    if not relationships:
        logger.info("No SnapMirror relationships found.")
        return

    # Destination の SVM 単位の SnapMirror relationship の Health を確認するための変数を宣言
    svm_health = defaultdict(lambda: {'healthy': True, 'name': '', 'uuid': ''})

    for relationship in relationships:
        rel_dict = relationship.to_dict()
        logger.debug(f"SnapMirror Relationship: {rel_dict}")

        # SnapMirror relationship個別の状態のレポート
        report_individual_relationship_health(rel_dict)

        # Destination SVM単位SnapMirror relationshipの状態の評価
        update_svm_health(rel_dict, svm_health)

    # Destination SVM単位SnapMirror relationshipの状態のレポート
    report_svm_level_health(svm_health)

# SnapMirror relationshipの個別のHealthをメトリクスとしてPUT
def report_individual_relationship_health(rel_dict: Dict[str, Any]) -> None:
    # HealthがTrueの場合は1
    # HealthがFalseの場合は0
    health_value = 1 if rel_dict.get('healthy', False) else 0
    dimensions = [
        {'Name': 'SourcePath', 'Value': rel_dict.get('source', {}).get('path', 'Unknown')},
        {'Name': 'DestinationPath', 'Value': rel_dict.get('destination', {}).get('path', 'Unknown')},
        {'Name': 'RelationshipUUID', 'Value': rel_dict.get('uuid', 'Unknown')}
    ]
    put_metric_data('SnapMirrorRelationshipHealth', health_value, dimensions)

# Destination SVM単位SnapMirror relationshipの状態の評価
def update_svm_health(rel_dict: Dict[str, Any], svm_health: Dict[str, Dict[str, Any]]) -> None:
    destination_svm = rel_dict.get('destination', {}).get('svm', {})
    destination_svm_uuid = destination_svm.get('uuid', 'Unknown')

    # SnapMirror relationshipがHealthではない場合、該当SnapMirror relationshipの詳細をログに記録
    if not rel_dict.get('healthy', False):
        svm_health[destination_svm_uuid]['healthy'] = False
        log_unhealthy_relationship(rel_dict)

    # UnhealthyなSnapMirror relationshipが存在するとして整理
    svm_health[destination_svm_uuid]['name'] = destination_svm.get('name', 'Unknown')
    svm_health[destination_svm_uuid]['uuid'] = destination_svm_uuid

# Destination SVM 単位でメトリクスをPUT
def report_svm_level_health(svm_health: Dict[str, Dict[str, Any]]) -> None:
    # Destination SVM 単位で、全てのSnapMirror relationshipのHealthがTrueの場合は1
    # Destination SVM 単位で、いずれかのSnapMirror relationshipのHealthがFalseの場合は0
    for svm_uuid, svm_info in svm_health.items():
        health_value = 1 if svm_info['healthy'] else 0
        dimensions = [
            {'Name': 'DestinationStorageVirtualMachineName', 'Value': svm_info['name']},
            {'Name': 'DestinationStorageVirtualMachineUUID', 'Value': svm_uuid}
        ]
        put_metric_data('SnapMirrorRelationshipHealth', health_value, dimensions)

# Unhealthy な SnapMirror relationshipの情報をロギング
def log_unhealthy_relationship(rel_dict: Dict[str, Any]) -> None:
    unhealthy_reason = rel_dict.get('unhealthy_reason', 'Unknown reason')
    relationship_uuid = rel_dict.get('uuid', 'Unknown')
    source_path = rel_dict.get('source', {}).get('path', 'Unknown')
    destination_path = rel_dict.get('destination', {}).get('path', 'Unknown')

    logger.info(
        f"Unhealthy SnapMirror Relationship detected "
        f"Unhealthy Reason: {unhealthy_reason}, "
        f"Relationship UUID: {relationship_uuid}, "
        f"Source Path: {source_path}, "
        f"Destination Path: {destination_path}"
    )

def main():
    try:
        config.CONNECTION = get_ontap_connection()
        relationships = get_snapmirror_relationships()
        evaluate_and_report_snapmirror_health(relationships)
    except Exception as e:
        logger.exception(f"An unexpected error occurred: {e}")
        sys.exit(1)

def lambda_handler(event, context):
    main()

AWS CDKの構成

リソースはAWS CDKでデプロイします。

使用したコードは以下リポジトリに保存しています。

https://github.com/non-97/monitoring-snapmirror-relationship-health

デプロイするリソースは以下のとおりです。

  • Lambda周り
    • Lambda関数
    • Lambda関数にアタッチするIAMロール
    • Lambda関数にアタッチするSecurity Group (指定したSecurity Groupを使用することも可)
  • EventBridge Scheduler周り (オプション)
    • EventBridge Scheduler
    • EventBridge Scheduler Group
    • EventBridge SchedulerにアタッチするIAMロール
  • VPCエンドポイント (オプション)
    • CloudWatch
    • SSM

ディレクトリツリーは以下のとおりです。

> tree
.
├── .gitignore
├── .npmignore
├── .python-version
├── README.md
├── bin
│   └── monitoring-snapmirror-relationship-health.ts
├── cdk.context.json
├── cdk.json
├── jest.config.js
├── lib
│   ├── construct
│   │   ├── lambda-construct.ts
│   │   ├── scheduler-construct.ts
│   │   └── vpc-endpoint-construct.ts
│   ├── monitoring-snapmirror-relationship-health-stack.ts
│   └── src
│       └── lambda
│           ├── index.py
│           └── requirements.txt
├── package-lock.json
├── package.json
├── parameter
│   ├── config
│   │   ├── index.ts
│   │   ├── lambda-config.ts
│   │   ├── scheduler-config.ts
│   │   └── vpc-endpoint-config.ts
│   ├── index.ts
│   └── types
│       └── index.ts
├── pyproject.toml
├── test
│   └── monitoring-snapmirror-relationship-health.test.ts
├── tsconfig.json
└── uv.lock

10 directories, 26 files

Stackに渡すパラメーターの型定義は以下のとおりです。

/parameter/types/index.ts
import * as cdk from "aws-cdk-lib";

export interface VpcEndpointProperty {
  vpcId: string;                                                      // VPCエンドポイントを作成するVPCのID
  vpcEndpointSubnetSelection: cdk.aws_ec2.SubnetSelection;            // VPCエンドポイントを作成するサブネットの検索条件
  shouldCreateSsmVpcEndpoint?: boolean;                               // SSMのVPCエンドポイントを作成するかどうか
  shouldCreateCloudWatchVpcEndpoint?: boolean;                        // CloudWatchのVPCエンドポイントを作成するかどうか
}

export interface LambdaProperty {
  vpcId: string;                                                      // Lambda関数が接続するVPCのID
  functionSubnetSelection: cdk.aws_ec2.SubnetSelection;               // Lambda関数が接続するサブネットの検索条件
  functionSecurityGroupId?: string;                                   // Lambda関数のENIにアタッチするSecurity Group
  functionApplicationLogLevel?: cdk.aws_lambda.ApplicationLogLevel;   // Lambda関数のアプリケーションログレベル
  paramsAndSecretsLogLevel?: cdk.aws_lambda.ParamsAndSecretsLogLevel; // AWS Parameter and Secrets Lambda extensionのログレベル
  fsxnDnsName: string;                                                // FSxNファイルシステム または SVMのDNS名
  fsxnUserName: string;                                               // FSxNファイルシステム または SVMのユーザー名
  fsxnUserCredentialSsmParameterStoreName: string;                    // FSxNファイルシステム または SVMのユーザーの認証情報を保存しているSSM Parameter Store
  fsxnUserCredentialSsmParameterStoreKmsKeyId?: string;               // FSxNファイルシステム または SVMのユーザーの認証情報を保存しているSSM Parameter StoreのKMSキーID (デフォルトキーの場合は指定不要)
}

export interface SchedulerProperty {
  scheduleExpression: string;                                         // Lambda関数の実行間隔
}

export interface MonitoringSnapMirrorRelationshipHealthProperty {
  vpcEndpointProperty?: VpcEndpointProperty;
  lambdaProperty: LambdaProperty;
  schedulerProperty?: SchedulerProperty;
}

export interface MonitoringSnapMirrorRelationshipHealthStackProperty {
  env?: cdk.Environment;
  props: MonitoringSnapMirrorRelationshipHealthProperty;
}

Lambda関数で使用するライブラリはrequirements.txtで定義して、バンドルします。

/lib/src/lambda/requirements.txt
netapp-ontap==9.16.1.0
boto3==1.34.63
aws-xray-sdk==2.14.0

ライブラリの依存関係は以下のとおりです。

> uv tree
Resolved 18 packages in 1ms
monitoring-snapmirror-relationship-health v0.1.0
├── aws-xray-sdk v2.14.0
│   ├── botocore v1.35.63
│   │   ├── jmespath v1.0.1
│   │   ├── python-dateutil v2.9.0.post0
│   │   │   └── six v1.16.0
│   │   └── urllib3 v2.2.3
│   └── wrapt v1.16.0
├── boto3 v1.35.63
│   ├── botocore v1.35.63 (*)
│   ├── jmespath v1.0.1
│   └── s3transfer v0.10.3
│       └── botocore v1.35.63 (*)
└── netapp-ontap v9.16.1.0
    ├── certifi v2024.8.30
    ├── marshmallow v3.23.1
    │   └── packaging v24.2
    ├── requests v2.32.3
    │   ├── certifi v2024.8.30
    │   ├── charset-normalizer v3.4.0
    │   ├── idna v3.10
    │   └── urllib3 v2.2.3
    ├── requests-toolbelt v1.0.0
    │   └── requests v2.32.3 (*)
    └── urllib3 v2.2.3
(*) Package tree already displayed

なお、デプロイされたLambda関数のパッケージサイズは47.2 MBです。少しでもパッケージサイズを小さくして、コールドスタートの時間を短くしたい場合はバンドルするライブラリを削りましょう。

以下ライブラリを削ると33.2MBほどになります。

  • aws-xray-sdk
    • X-Rayでトレースしない場合は不要
  • boto3
    • Lambdaのランタイムに含まれるため、バージョン指定をする要件がない場合はバンドル不要

Lambda関数デプロイ周りのコードは以下のとおりです。

/lib/construct/lambda-construct.ts
    // Lambda Function
    const lambdaFunction = new cdk.aws_lambda.Function(this, "Default", {
      runtime: cdk.aws_lambda.Runtime.PYTHON_3_13,
      handler: "index.lambda_handler",
      code: cdk.aws_lambda.Code.fromAsset(
        path.join(__dirname, "../src/lambda/"),
        {
          bundling: {
            image: cdk.aws_lambda.Runtime.PYTHON_3_13.bundlingImage,
            command: [
              "bash",
              "-c",
              "pip install -r requirements.txt -t /asset-output && cp -au . /asset-output",
            ],
          },
        }
      ),
      role,
      vpc,
      vpcSubnets: vpc.selectSubnets(props.functionSubnetSelection),
      securityGroups: [securityGroup],
      paramsAndSecrets: cdk.aws_lambda.ParamsAndSecretsLayerVersion.fromVersion(
        cdk.aws_lambda.ParamsAndSecretsVersions.V1_0_103,
        {
          cacheEnabled: true,
          cacheSize: 10,
          logLevel: props.paramsAndSecretsLogLevel,
          parameterStoreTimeout: cdk.Duration.seconds(10),
          parameterStoreTtl: cdk.Duration.minutes(5),
        }
      ),
      architecture: cdk.aws_lambda.Architecture.ARM_64,
      timeout: cdk.Duration.seconds(30),
      tracing: cdk.aws_lambda.Tracing.ACTIVE,
      logRetention: cdk.aws_logs.RetentionDays.ONE_MONTH,
      loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
      applicationLogLevelV2: props.functionApplicationLogLevel,
      environment: {
        LOG_LEVEL: props.paramsAndSecretsLogLevel || "INFO",
        FSXN_DNS_NAME: props.fsxnDnsName,
        FSXN_USER_NAME: props.fsxnUserName,
        FSXN_USER_CREDENTIAL_SSM_PARAMETER_STORE_NAME:
          props.fsxnUserCredentialSsmParameterStoreName,
      },
    });

やってみた

検証環境

実際にやってみましょう。

検証環境は以下のとおりです。

[Amazon FSx for NetApp ONTAP] Tamperproof SnapshotとSnapMirrorの逆再同期の相性を確認してみた検証環境構成図.png

以下記事の検証環境の続きから行います。

https://dev.classmethod.jp/articles/amazon-fsx-for-netapp-ontap-tamperproof-snapshot-snapmirror-resync/

後ほどSVMを追加して、複数SVMのSnapMirrorの状態を確認できるかも試します。

FSxNファイルシステムにアクセスする際に使用するユーザーの作成

各種リソースをデプロイする前にFSxNファイルシステムにアクセスする際に使用するユーザーを作成します。

SVMユーザーを作成しても良いですが、その場合、FSxNファイルシステム内のSVMごとにユーザーを作成することになります。

ユーザー管理が手間なので、ファイルシステムユーザーでアクセスしたいところです。

ファイルシステムのロールはfsxadminfsxadmin-readonlyの2つがあります。

FSxNの制約事項として、追加でファイルシステムロールを作成することはできません。

追加のファイルシステムユーザーを作成し、 fsxadminまたは fsxadmin-readonlyロールを割り当てることができます。新しいロールを作成したり、既存のロールを変更したりすることはできません。詳細については、「ファイルシステムとSVM管理用の新しいONTAPユーザーの作成」を参照してください。

ONTAP ロールとユーザー - ONTAP に関する FSx

SnapMirror relationshipの情報のみ取得できれば良いため、fsxadmin-readonlyでも権限的に過剰ではありますが、fsxadmin-readonlyロールを割り当てたユーザーfsxadmin-readonlyを用意します。

::> security login show

Vserver: FsxId0e64a4f5386f74c87
                                                                 Second
User/Group                 Authentication                 Acct   Authentication
Name           Application Method        Role Name        Locked Method
-------------- ----------- ------------- ---------------- ------ --------------
autosupport    console     password      autosupport      no     none
fsxadmin       http        password      fsxadmin         no     none
fsxadmin       ontapi      password      fsxadmin         no     none
fsxadmin       ssh         password      fsxadmin         no     none
fsxadmin       ssh         publickey     fsxadmin         -      none

Vserver: svm
                                                                 Second
User/Group                 Authentication                 Acct   Authentication
Name           Application Method        Role Name        Locked Method
-------------- ----------- ------------- ---------------- ------ --------------
vsadmin        http        password      vsadmin          no     none
vsadmin        ontapi      password      vsadmin          no     none
vsadmin        ssh         password      vsadmin          no     none
8 entries were displayed.

::> security login create -user-or-group-name fsxadmin-readonly -application http -authentication-method password -role fsxadmin-readonly

Please enter a password for user 'fsxadmin-readonly':
Please enter it again:

::> security login show

Vserver: FsxId0e64a4f5386f74c87
                                                                 Second
User/Group                 Authentication                 Acct   Authentication
Name           Application Method        Role Name        Locked Method
-------------- ----------- ------------- ---------------- ------ --------------
autosupport    console     password      autosupport      no     none
fsxadmin       http        password      fsxadmin         no     none
fsxadmin       ontapi      password      fsxadmin         no     none
fsxadmin       ssh         password      fsxadmin         no     none
fsxadmin       ssh         publickey     fsxadmin         -      none
fsxadmin-readonly
               http        password      fsxadmin-readonly
                                                          no     none

Vserver: svm
                                                                 Second
User/Group                 Authentication                 Acct   Authentication
Name           Application Method        Role Name        Locked Method
-------------- ----------- ------------- ---------------- ------ --------------
vsadmin        http        password      vsadmin          no     none
vsadmin        ontapi      password      vsadmin          no     none
vsadmin        ssh         password      vsadmin          no     none
9 entries were displayed.

デプロイ

リソースをデプロイします。

デプロイ時のパラメーターは以下のとおりです。

/parameter/config/vpc-endpoint-config.ts
import * as cdk from "aws-cdk-lib";
import { VpcEndpointProperty } from "../types";

export const vpcEndpointConfig: VpcEndpointProperty = {
  vpcId: "vpc-043c0858ea33e8ec2",
  vpcEndpointSubnetSelection: {
    subnetType: cdk.aws_ec2.SubnetType.PRIVATE_ISOLATED,
    availabilityZones: ["us-east-1a"],
    subnetFilters: [
      cdk.aws_ec2.SubnetFilter.byIds(["subnet-0ddc1cafa116ba0dd"]),
    ],
  },
  shouldCreateSsmVpcEndpoint: true,         // SSMのVPCエンドポイントを作成
  shouldCreateCloudWatchVpcEndpoint: true,  // CloudWatchのVPCエンドポイントを作成
};

/parameter/config/lambda-config.ts
import * as cdk from "aws-cdk-lib";
import { LambdaProperty } from "../types";

export const lambdaConfig: LambdaProperty = {
  vpcId: "vpc-043c0858ea33e8ec2",
  functionSubnetSelection: {
    subnetType: cdk.aws_ec2.SubnetType.PRIVATE_ISOLATED,
    availabilityZones: ["us-east-1a"],
    subnetFilters: [
      cdk.aws_ec2.SubnetFilter.byIds(["subnet-0ddc1cafa116ba0dd"]),
    ],
  },
  functionSecurityGroupId: "sg-03730d9e2b49e7cbc",                            // FSxNファイルシステムにTCP/443で接続を許可しているSecurity Groupを指定
  functionApplicationLogLevel: cdk.aws_lambda.ApplicationLogLevel.INFO,
  paramsAndSecretsLogLevel: cdk.aws_lambda.ParamsAndSecretsLogLevel.INFO,
  fsxnDnsName: "management.fs-0e64a4f5386f74c87.fsx.us-east-1.amazonaws.com", // FSxNファイルシステム内のSVM全てのSnapMirrorの情報を取得したいため、FSxNファイルシステムのDNS名を指定
  fsxnUserName: "fsxadmin-readonly",                                          // FSxNファイルシステム全体のRead Only権限を持ったユーザーwー指定
  fsxnUserCredentialSsmParameterStoreName:
    "/fsxn/non-97-fsxn/fsxadmin-readonly/password2",                          // fsxadmin-readonly の認証情報を保存したSSM Parameter Storeを指定
  fsxnUserCredentialSsmParameterStoreKmsKeyId:
    "6233b0b8-a26b-4f0d-8589-ac35b7152932",                                   // SSM Parameter StoreのKMSキーのIDを指定
};
/parameter/config/scheduler-config.ts
import { SchedulerProperty } from "../types";

export const schedulerConfig: SchedulerProperty = {
  scheduleExpression: "cron(0/2 * * * ? *)",  // 2分間隔でLambda関数を実行するように指定
};

デプロイ後のCloudWatchメトリクスの確認

現在のSnapMirror relationshipの状態は以下のとおりです。

::> snapmirror show
                                                                       Progress
Source            Destination Mirror  Relationship   Total             Last
Path        Type  Path        State   Status         Progress  Healthy Updated
----------- ---- ------------ ------- -------------- --------- ------- --------
svm:vol_ntfs
            XDP  svm:vol_ntfs_dst
                              Broken-off
                                      Idle           -         false   -
svm:vol_ntfs_dst
            XDP  svm:vol_ntfs Snapmirrored
                                      Idle           -         true    -
2 entries were displayed.

この状態をCloudWatchメトリクスとして反映できているのか確認します。

まず、SnapMirror relationship単位のHealthを確認します。

8.SnapMirror relationship単位.png

ONTAP CLIで確認したとおり、転送元がsvm:vol_ntfsで転送先がsvm:vol_ntfs_dst0、転送元がsvm:vol_ntfs_dstで転送先がsvm:vol_ntfs1になっていることが分かりますね。

2分間隔でカスタムメトリクスがPUTされていることも分かります。

Destination SVM単位のHealthも確認します。

7.Destination SVM単位.png

0となっていますね。これは転送元がsvm:vol_ntfsで転送先がsvm:vol_ntfs_dstのHealthがfalseであるため、意図したとおりです。

ちなみに、原因を調査するためにFSxNファイルシステムにSSHで接続するのも面倒そうだったので、Unhealthyとなっている原因はLambda関数のログに出力させています。

{
    "timestamp": "2024-11-18T02:12:35Z",
    "level": "INFO",
    "message": "Unhealthy SnapMirror Relationship detected Unhealthy Reason: [{'code': '13303943', 'message': 'SnapMirror relationship is unhealthy. Reason: Scheduled update failed to start. (Destination svm:vol_ntfs_dst must be a data-protection volume.).'}], Relationship UUID: 8a6d0455-9b21-11ef-accd-b31c82a68aa5, Source Path: svm:vol_ntfs, Destination Path: svm:vol_ntfs_dst",
    "logger": "index",
    "requestId": "53673aa2-7019-4a68-b2e2-97835223899e"
}

SVM間のSnapMirror relationshipの場合

SVMピアリング

SVM間のSnapMirror relationshipの場合も試したいところです。

svm2というSVMを追加して、svmとSnapMirror relationshipを組ませます。

SVM一覧は以下のとおりです。

::> vserver show
                               Admin      Operational Root
Vserver     Type    Subtype    State      State       Volume     Aggregate
----------- ------- ---------- ---------- ----------- ---------- ----------
svm         data    default    running    running     svm_root   aggr1
svm2        data    default    running    running     svm2_root  aggr1
2 entries were displayed.

::> cifs show
            Server          Status    Domain/Workgroup Authentication
Vserver     Name            Admin     Name             Style
----------- --------------- --------- ---------------- --------------
svm         SMB-SERVER      up        CORP             domain
svm2        SVM2            up        CORP             domain
2 entries were displayed.

SVMピアリングを作成します。

::> vserver peer create -vserver svm -peer-vserver svm2 -applications snapmirror

Info: 'vserver peer create' command is successful.

::> vserver peer show
            Peer        Peer                           Peering        Remote
Vserver     Vserver     State        Peer Cluster      Applications   Vserver
----------- ----------- ------------ ----------------- -------------- ---------
svm         svm2        peered       FsxId0e64a4f5386f74c87
                                                       snapmirror     svm2
svm2        svm         peered       FsxId0e64a4f5386f74c87
                                                       snapmirror     svm
2 entries were displayed.

SnapMirror relationshipの作成

snapMirror protectでSnapMirror relationshipを作成します。

::> snapmirror protect -destination-vserver svm2 -path-list svm:vol_ntfs_dst -auto-initialize false -policy MirrorAllSnapshots -schedule 5min -support-tiering true
[Job 1169] Job is queued: snapmirror protect for list of source endpoints beginning with "svm:vol_ntfs_dst".

::> snapmirror show
                                                                       Progress
Source            Destination Mirror  Relationship   Total             Last
Path        Type  Path        State   Status         Progress  Healthy Updated
----------- ---- ------------ ------- -------------- --------- ------- --------
svm:vol_ntfs
            XDP  svm:vol_ntfs_dst
                              Broken-off
                                      Idle           -         false   -
svm:vol_ntfs_dst
            XDP  svm2:vol_ntfs_dst_dst
                              -       -              -         -       -
                 svm:vol_ntfs Snapmirrored
                                      Idle           -         true    -
3 entries were displayed.

作成後にLambda関数を手動で実行します。

実行後のCloudWatchメトリクスは以下のとおりです。

9.作りたての状態.png

新しくPUTされたメトリクスは1のようですね。

このまま放置すると、メトリクスが0になりました。

10.初期化前にスケジュールが実行された場合.png

これは作成したSnapMirror relationshipが初期化されていない状態で、設定したスケジュール実行を行おうとしたためです。

::> snapmirror show
                                                                       Progress
Source            Destination Mirror  Relationship   Total             Last
Path        Type  Path        State   Status         Progress  Healthy Updated
----------- ---- ------------ ------- -------------- --------- ------- --------
svm:vol_ntfs
            XDP  svm:vol_ntfs_dst
                              Broken-off
                                      Idle           -         false   -
svm:vol_ntfs_dst
            XDP  svm2:vol_ntfs_dst_dst
                              Uninitialized
                                      Idle           -         false   -
                 svm:vol_ntfs Snapmirrored
                                      Idle           -         true    -
3 entries were displayed.

::> snapmirror show -fields unhealthy-reason
source-path  destination-path unhealthy-reason
------------ ---------------- --------------------------------------------------------------------------------------------------
svm:vol_ntfs svm:vol_ntfs_dst Scheduled update failed to start. (Destination svm:vol_ntfs_dst must be a data-protection volume.)
svm:vol_ntfs_dst
             svm2:vol_ntfs_dst_dst
                              Scheduled update failed to start. (Volume svm2:vol_ntfs_dst_dst is not initialized.)
svm:vol_ntfs_dst
             svm:vol_ntfs     -
3 entries were displayed.

SnapMirror relationshipの初期化

SnapMirror relationshipの初期化を行います。

::> snapmirror initialize -destination-path svm2:vol_ntfs_dst_dst -source-path svm:vol_ntfs_dst
Operation is queued: snapmirror initialize of destination "svm2:vol_ntfs_dst_dst".

::> snapmirror show
                                                                       Progress
Source            Destination Mirror  Relationship   Total             Last
Path        Type  Path        State   Status         Progress  Healthy Updated
----------- ---- ------------ ------- -------------- --------- ------- --------
svm:vol_ntfs
            XDP  svm:vol_ntfs_dst
                              Broken-off
                                      Idle           -         false   -
svm:vol_ntfs_dst
            XDP  svm2:vol_ntfs_dst_dst
                              Snapmirrored
                                      Transferring   0B        true    11/18 02:49:58
                 svm:vol_ntfs Snapmirrored
                                      Idle           -         true    -
3 entries were displayed.

::> snapmirror show
                                                                       Progress
Source            Destination Mirror  Relationship   Total             Last
Path        Type  Path        State   Status         Progress  Healthy Updated
----------- ---- ------------ ------- -------------- --------- ------- --------
svm:vol_ntfs
            XDP  svm:vol_ntfs_dst
                              Broken-off
                                      Idle           -         false   -
svm:vol_ntfs_dst
            XDP  svm2:vol_ntfs_dst_dst
                              Snapmirrored
                                      Transferring   1.59GB    false   11/18 02:50:14
                 svm:vol_ntfs Snapmirrored
                                      Idle           -         true    -
3 entries were displayed.

::> snapmirror show
                                                                       Progress
Source            Destination Mirror  Relationship   Total             Last
Path        Type  Path        State   Status         Progress  Healthy Updated
----------- ---- ------------ ------- -------------- --------- ------- --------
svm:vol_ntfs
            XDP  svm:vol_ntfs_dst
                              Broken-off
                                      Idle           -         false   -
svm:vol_ntfs_dst
            XDP  svm2:vol_ntfs_dst_dst
                              Snapmirrored
                                      Finalizing     41.38GB   false   11/18 02:58:00
                 svm:vol_ntfs Snapmirrored
                                      Idle           -         true    -
3 entries were displayed.

::> snapmirror show
                                                                       Progress
Source            Destination Mirror  Relationship   Total             Last
Path        Type  Path        State   Status         Progress  Healthy Updated
----------- ---- ------------ ------- -------------- --------- ------- --------
svm:vol_ntfs
            XDP  svm:vol_ntfs_dst
                              Broken-off
                                      Idle           -         false   -
svm:vol_ntfs_dst
            XDP  svm2:vol_ntfs_dst_dst
                              Snapmirrored
                                      Idle           -         true    -
                 svm:vol_ntfs Snapmirrored
                                      Idle           -         true    -
3 entries were displayed.

完了しました。

完了後、CloudWatchメトリクスを確認します。

11.初期化後.png

すると、0になっていたメトリクスが1に変わりました。意図したとおり動作していますね。

SnapMirror relationshipのHealthがFSxNのサービスとして提供するCloudWatchメトリクスに追加されることを願う

SnapMirror relationshipのHealthをCloudWatchメトリクスにPUTしてみました。

今回は行いませんでしたが、あとはPUTされたメトリクスに対してCloudWatchアラームを設定するだけです。

SnapMirror relationship個別に設定しても良いですし、Destination SVM単位で設定して、通知設定数を減らすのもアリです。

なお、今回の構成の主な課金要素は以下のとおりです。

  • Lambda関数の実行時間
  • カスタムメトリクス数
  • VPCエンドポイント数
  • KMSキー
  • CloudWatch Logs出力ログ量

VPCエンドポイントを作成しない場合、SnapMirror relationshipの数が10個ほどであれば、5 USDほどで実装可能です。

VPCエンドポイントを作成する場合でも1AZのみであれば、25 USDほどです。

DIIの場合は、AUのEC2インスタンスを以下が要求されます。

  • vCPU : 2個
  • RAM : 8 GiB
  • ディスク : 50 GB

抜粋 : Acquisition Unit の要件

要件に当てはまるように、RHELのインスタンスをm6i.largeで動作させる場合、オンデマンドで116.34 USD、3年EC2 Instance Savings Plans前払いなしで66.82 USDかかります。

加えて、AUからDIIのサービスエンドポイントまでのアウトバウンド通信の経路としてNAT Gatewayも必要になります。NAT Gatewayの料金として毎月45 USDほどかかります。

DIIが運用要件的にオーバースペックなのであれば、今回紹介した方法で十分な場合もあるでしょう。

最後に、SnapMirror relationshipのHealthがFSxNのサービスとして提供するCloudWatchメトリクスに追加されることを願って締めます。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.