Amazon Web Services ブログ

AWS Lambda、Amazon Route 53、および Amazon SNS を使用して、Redis クラスター用 Amazon ElastiCache (クラスターモードが無効) のリードレプリカエンドポイントを監視します



Redis 用 Amazon ElastiCache では、アプリケーションが指定されたエンドポイントを使用して ElastiCache ノードまたはクラスタに接続します。Redis 用 ElastiCache ユーザーガイドRedis 用 ElastiCache コンポーネントと機能によると、複数のノードを持つ Redis (クラスターモード無効) クラスターには次の 2 種類のエンドポイントがあります。

プライマリエンドポイントは、プライマリロール内の特定のノードが変更された場合でも、常にクラスタ内のプライマリノードに接続します。クラスターへのすべての書き込みにプライマリエンドポイントを使用します。Redis (クラスターモードが無効) クラスターの読み取りエンドポイントは、常に特定のノードを示します。リードレプリカを追加または削除するときは、アプリケーションで関連するノードエンドポイントを更新する必要があります。」

Redis 用 ElastiCache のプライマリエンドポイント機能は、プライマリノードエンドポイントを解決する際に常に一貫性を提供します。AWS の顧客はこの機能を高く評価しています。

「ElastiCache Redis の便利な機能は、クラスターの現在のプライマリノードを常に示す、プライマリエンドポイントを利用できる点です。このエンドポイントは、クラスターにフェイルオーバーが発生しても変更されません。したがって、プライマリノードが変更されてもアプリケーションを変更する必要はありません。この機能は、自動フェイルオーバーが発生した場合に特に役立ちます。」—AWS の顧客

ベストプラクティスとして、およびワークロードバランシングのため、リードレプリカに読み取りリクエストを送信する必要があります。ただし、フェイルオーバーが発生した場合は、以前に使用したリードレプリカをプライマリロールに昇格させることができます。読み取りリクエストを同じエンドポイントに送信し続けると、新しいプライマリ (古いリードレプリカ) の負荷が増える可能性があります。この場合、フェイルオーバー発生後であっても、常にレプリカを示すリードレプリカエンドポイントがあると便利です。

これを行うには、リードレプリカエンドポイントの監視および更新が可能な AWS Lambda 関数を設定します。この目的は、Amazon Route 53 のプライベートゾーンにある各リードレプリカに対してカスタムの CNAME を作成して使用し、これらの CNAME を Redis のクライアントで使用することです。

フェイルオーバーが発生すると、Amazon Simple Notification Service (Amazon SNS) トピックにプッシュ通知が配信されます。この SNS トピックでリッスンする Lambda 関数は、これに応じて CNAME を適切なリードレプリカのエンドポイントに更新します。その結果、Redis クライアントは、プライマリノードでの書き込み操作のための通常の ElastiCache プライマリエンドポイントに加えて、リードレプリカのエンドポイントを示す CNAME を常に持ちます。

この記事では、Amazon SNS をリッスンし、Redis クラスター用 Amazon ElastiCache (クラスターモードが無効) のリードレプリカに使用される CNAME を更新する AWS Lambda 関数の作成手順について説明します。

ソリューションの概要

このソリューションの構造は次のとおりです。

クライアントアプリケーション

この例では、書き込みに ElastiCache プライマリエンドポイントを使用します。読み取りの場合、カスタム CNAME で 5 つのリードレプリカを使用します。

  • readonly1.private.redisdub.pl.
  • readonly2.private.redisdub.pl.
  • readonly3.private.redisdub.pl.
  • readonly4.private.redisdub.pl.
  • readonly5.private.redisdub.pl.

Amazon ElastiCache

以下の例に示すように、1 つのプライマリノードと 5 つのレプリカを持つ ElastiCache クラスター専用の SNS トピック (クラスターモードが無効) を選択します。

Amazon Route 53

DNS プライベートゾーン (private.redisdub.pl ) を作成し、このゾーンで次の CNAME を使用します。

  • readonly1.private.redisdub.pl — testdns-002.6advcy.0001.euw1.cache.amazonaws.com
  • readonly2.private.redisdub.pl — testdns-003.6advcy.0001.euw1.cache.amazonaws.com
  • readonly3.private.redisdub.pl — testdns-004.6advcy.0001.euw1.cache.amazonaws.com
  • readonly4.private.redisdub.pl — testdns-005.6advcy.0001.euw1.cache.amazonaws.com
  • readonly5.private.redisdub.pl — testdns-006.6advcy.0001.euw1.cache.amazonaws.com

AWS Identity and Access Management (IAM)

Lambda 関数には、関数の実行に必要な権限を与えるための IAM ロールがあります。

  • AmazonElastiCacheReadOnlyAccess
  • AWSLambdaBasicExecutionRole
  • RedisReplica_Route53 (2 つの API コールのみ必要であるため、Route 53 のカスタムポリシー)

AWS Lambda

Lambda 関数は、クラスタの SNS トピックをリッスンしています。この SNS トピックにイベントが発生するたびに、それがフェイルオーバーなのか、あるいはリードレプリカの追加または削除なのかを検出します。この 3 つのイベントのいずれかが発生すると、Lambda 関数は API コールを実行して Redis クラスターの最新の構造を取得します (elasticache.describe_replication_groups)。

応答に基づき、この関数は別の API コールを実行して、Route 53 プライベートゾーンで CNAME を更新または作成します ( route53.change_resource_record_sets)。フェイルオーバーの場合は、既存の “読み取り” CNAME を更新します。ノードの作成または削除の場合は、それに応じて CNAME が追加または削除されます。

このシナリオでは、アプリケーションは、プライマリエンドポイント経由でプライマリノードで実行されている書き込みに加えて、リードレプリカに対する読み取り操作を常に開始します。

結果とベンチマーク

以下のテストでは、次の Redis ベンチマークコマンドを実行して、5 つのクライアントのインスタンスに対して cron ジョブを実行します。

redis-benchmark -n 10000 -k 0 -h readonly1.private.redisdub.pl -p 6379

-n 10000 は 10,000 件のリクエストを実行します。
-k 0 は、リクエストごとに再接続します。

-h readonly1.private.redisdub.pl は、レプリカ用に作成された 1 つの CNAME への接続を示します。

5 つのクライアントはそれぞれ、1 つの固有の CNAME をターゲットにしています。

  • readonly1.private.redisdub.pl
  • readonly2.private.redisdub.pl
  • readonly3.private.redisdub.pl
  • readonly4.private.redisdub.pl
  • readonly5.private.redisdub.pl

次のスクリーンショットは、Amazon CloudWatch メトリクスの NewConnections です。これは、ベンチマークによって生成されたリクエストがリードレプリカ全体に均等に分散されていることを示しています。

この CloudWatch メトリクスを詳しく見ると、16:00 にトリガーされたフェイルオーバーと、プライマリ testdns-001testdns-002 に対してフェイルオーバーしていることがわかります。

また、testdns-002 がベンチマークからリクエストを受け取っているのがわかります。フェイルオーバーがトリガーされた 16:00 には、CNAME レコードが更新されたため、リクエストの数が減少しています。その後、testdns-002 が新しいプライマリになり、読み取り操作 readonly1.private.redisdub.pl の CNAME を介したリクエストの受信を停止します。

16:00 のフェイルオーバー前:

プライマリエンドポイント –> プライマリ testdns-001

readonly1.private.redisdub.pl –> レプリカ testdns-002

16:00 のフェイルオーバー後:

プライマリエンドポイント –> 新しいプライマリ testdns-002

readonly1.private.redisdub.pl –> レプリカ testdns-001

通常のフェイルオーバーのシナリオと同様に、以前のプライマリノード testdns-001 が置き換わります。プライマリノードが出現して動作すると、ベンチマークからリクエストを受信し始めるのがわかります。これは、readonly1.private.redisdub.pl が testdns-001 を示しているからです。

testdns-001 は、16:06 以降リクエストを受信できる可能性がありましたが、次のベンチマークの実施は 16:10 でした。そのため、16:06 から 16:10 にかけて、フラットな茶色の線が表示されています。

このツールを実装する手順の詳細

ステップ 1: Redis クラスタとクライアントが配置されている Virtual Private Cloud (VPC) の Route 53 にプライベートゾーンを作成します。

詳細については、Amazon Route 53 Developer Guide 開発者ガイドプライベートホストゾーンの作成を参照してください。

また、以下のようなリードレプリカの CNAME を作成する必要があります。

  • read1.myredis.com — 現行の Redis レプリカ Node1 のエンドポイント
  • read2.myredis.com — 現行の Redis レプリカ Node2 のエンドポイント

既存のリードレプリカと同じ数だけ CNAME を作成できます。

CNAME を作成すると、クライアントアプリケーションで使用して検証することができます。

ステップ 2: クラスタに SNS トピックを追加します。

詳細については、Redis 用 ElastiCache ユーザーガイドElastiCache Amazon SNS通知の管理を参照してください。

ステップ 3: 次のポリシーに従って、Lambda 関数の IAM ロールを作成します。

  • AmazonElastiCacheReadOnlyAccess
  • AWSLambdaBasicExecutionRole

次のコンテンツを使用して、新しいポリシー RedisReplica_Route53 を作成します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1511707556511",
      "Action": [
        "route53:GetHostedZone",
        "route53:ChangeResourceRecordSets"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:route53:::hostedzone/Z32WVXIKNNRFKK"
    }
  ]
}

IAM ポリシーとロールの詳細については、以下のトピックを参照してください。

ステップ 4: 次のコードを使用して Lambda 関数を作成します。

AWS Lambda コンソールで、[関数の作成] を選択し、[一から作成] を選択します。関数に RedisReplica-autocname という名前を付けます。

ロールリストで、[既存のロールを選択] オプションを選択します。次に、ステップ 3 で作成したロールを選択します。

[関数の作成] を選択します。[設定]、[トリガー]、[モニタリング] の 3 つのタブがあるページが表示されます。[設定] タブで、[ランタイム] として [Python 2.7] を選択します。次に、以下のコードをコピーしてエディタに貼り付けます。

from  __future__ import print_function

import boto3
import re
import json
import os

AWS_REGION = os.environ['aws_region']
CNAME = os.environ['cname']
ZONE_ID = os.environ['zone_id']
CLUSTER = os.environ['cluster']


def aws_session(role_arn=None, session_name='my_session'):
    """
    If role_arn is given assumes a role and returns boto3 session
    otherwise return a regular session with the current IAM userFailoverComplete/role
    """

    if role_arn:
        client = boto3.client('sts')
        response = client.assume_role(
            RoleArn=role_arn, RoleSessionName=session_name)
        session = boto3.Session(
            aws_access_key_id=response['Credentials']['AccessKeyId'],
            aws_secret_access_key=response['Credentials']['SecretAccessKey'],
            aws_session_token=response['Credentials']['SessionToken'])
        return session
    else:
        return boto3.Session()


def get_nodes(cluster, session):
    """
    return list of nodes that breaks down a cluster
    """

    elasticache = session.client('elasticache', region_name=AWS_REGION)
    repgroups = elasticache.describe_replication_groups()['ReplicationGroups']
    nodes = {}
    for repgroup in repgroups:
        if repgroup['ReplicationGroupId'] == cluster:
            for nodegrp in repgroup['NodeGroups']:
                for cachecluster in nodegrp['NodeGroupMembers']:
                    nodes[cachecluster['CacheClusterId']] = {}
                    nodes[cachecluster['CacheClusterId']
                          ]['role'] = cachecluster['CurrentRole']
                    nodes[cachecluster['CacheClusterId']
                          ]['addr'] = cachecluster['ReadEndpoint']['Address']
    return(nodes)


def update_cname(nodes, cname, zone, session):
    """
    update CNAME entries from a dictionary of nodes.
    """

    route53 = session.client('route53')
    dzone = route53.get_hosted_zone(Id=zone)
    dzonedomain = dzone["HostedZone"]["Name"]

    """ CNAME should be a valid zone's sud-domain """
    if not re.match('[a-zA-Z\d-]{,63}(\.[a-zA-Z\d-]{1,63})*\.' + dzonedomain, cname):
        return('Error, cname {} doesnt match domain {}'.format(cname, dzonedomain))

    response = {}
    num = 1
    for node_name in nodes.keys():
        node = nodes[node_name]
        if node['role'] == 'replica':
            realcname = '.'.join(
                [i + str(num) if enum == 0 else i for enum, i in enumerate(cname.split('.'))])
            dns_changes = {
                'Changes': [
                    {
                        'Action': 'UPSERT',
                        'ResourceRecordSet': {
                            'Name': realcname,
                            'Type': 'CNAME',
                            'TTL': 10,
                            'ResourceRecords': [
                                {
                                  'Value': node['addr'],
                                }
                            ],
                        }
                    }
                ]
            }
            print(
                "DEBUG - Updating Route53 to create CNAME {} for {}".format(realcname, node['addr']))
            response[node_name] = route53.change_resource_record_sets(
                HostedZoneId=zone,
                ChangeBatch=dns_changes
            )
            num += 1
    return(response)


def lambda_handler(event, context):
    """
    Main lambda function
    Parse and check the event validity
    """

    msg = json.loads(event['Records'][0]['Sns']['Message'])
    msg_type = msg.keys()[0]
    msg_event = msg_type.split(':')[1]
    msg_node = msg[msg_type]

    events = ['CacheNodeReplaceComplete', 'TestFailoverApiCalled',
              'FailoverComplete', 'CacheClusterProvisioningComplete']

    if msg_event not in events:
        print('Event {} is not valid for RedisReplica-autocname function'.format(msg_type))
        return
    else:
        print(
            'Event {} is valid, processing with RedisReplica-autocname...'.format(msg_type))

    session = aws_session()
    nodes = get_nodes(CLUSTER, session)

    if msg_node not in [node for node in nodes.keys()]:
        print('{} not a node of cluster {}'.format(msg_node, CLUSTER))
        return

    dnsupdate = update_cname(nodes, CNAME, ZONE_ID, session)

    """ dnsupdate return list when OK and string on error """
    if isinstance(dnsupdate, str):
        print(dnsupdate)
        return

    for response in dnsupdate.iteritems():
        print("DNS record {} R53 status is {}".format(
            response[0], response[1]['ChangeInfo']['Status']))
    return

関数の作成の詳細については、AWS Lambda 開発者ガイドLambda 関数を作成するを参照してください。

次のステップでは、4 つの環境変数 (変数名/値) を設定します。これらはキーと値のペアであり、次のように設定する必要があります。

  • cluster: Redis クラスター名。
  • zone_id: プライベートゾーン (ID は右ペインに表示されています) を選択すると、Route 53 でこの情報を取得できます。
  • aws_region: Redis クラスタの AWS リージョン。
  • cname: リードレプリカに使用する CNAME 構造。たとえば、CNAME に read1.myredis.com、read2.myredis.com などを使用する場合は、「 read.myredis.com. 」と入力します (CNAME には最後にピリオド (.) を付けることに注意してください )。この CNAME は、新しいノードが作成され、レコードを関連付けると自動的に増えます。

コンソールの同じ [設定] タブで、コード実行パフォーマンスを制御するための関数のタイムアウトを設定します。このタイムアウトは 15 秒に設定することをお勧めします。(テストでは実行時間は約 3 秒でしたが、環境によって異なることがあります。)

最後に、[トリガー] タブで [SNS] を選択し、Redis クラスタに関連付けられているトピックを選択します。

ステップ 5: 環境をテストします。

最後のステップは、手動フェイルオーバーの実行による環境のテストです。このフェイルオーバーによって SNS トピックでイベントがトリガーされ、Lambda 関数がフェイルオーバーを検出します。その結果、新しいプライマリ/レプリカのマッピングが収集され、プライベートゾーンの CNAME が更新されます。

Time to Live (TTL) が期限切れになると (Amazon EC2 のプライベートホストゾーンでは 15 秒)、クライアントインスタンスは新しい DNS レコードを取得します。新しいリードレプリカ (古いプライマリ) に接続して、読み取り操作を実行します。アプリケーションのその他の変更は不要です。

もう一つのテストは、レプリケーショングループへの新しいノードの追加です。Lambda 関数は新しい CNAME を自動で作成します。既に read1.myredis.com と read2.myredis.com を使用している場合は、read3.myredis.com が作成され、この新しい CNAME をアプリケーションに追加できます。この関数は、リードレプリカと同じ数の CNAME を常に保持します。つまり、ノードを 1 つ削除すると、CNAME も 1 つ削除されます。

Lambda 関数について

Lambda 関数は、SNS トピックを直接リッスンしています。また、以下のものをフィルタリングするために各種検証を行います。

  • イベントタイプ
  • クラスター ID

SNS トピックの各メッセージは Lambda 関数をトリガーしますが、該当するメッセージだけがアクションをトリガーすることに注意してください。必要以上に実行して追加コストがかからないよう、Redis クラスター専用の SNS トピックを使用することをお勧めします。

まとめ

Redis 用 ElastiCache のプライマリエンドポイント機能は常に、プライマリロールで特定のノードが変わっても、クラスター内のプライマリノードに接続します。リードレプリカに読み取りリクエストを送信する際は、フェイルオーバー発生後であっても、常にレプリカを示すリードレプリカエンドポイントがあると便利です。この記事では、リードレプリカエンドポイントの監視および更新が可能な AWS Lambda 関数の作成方法について説明しました。このプロセスを実装すると、Redis クライアントは、プライマリノードでの書き込み操作のための通常の ElastiCache プライマリエンドポイントに加えて、リードレプリカのエンドポイントを示す CNAME を持つようになります。

この記事から得られた実践的な知識をもとに、このソリューションの構造を他のプロジェクトに再利用することができます。Lambda 関数をフィルタリングして実行するために使用するイベントタイプを変更し、ElastiCache イベントに応じて実行するコードを追加できます。

 


著者について

Yann Richard は AWS クラウドサポートエンジニア兼 ElastiCache サービス関連のエキスパートです。個人的に目標としていることは、4 時間以内にフルマラソンを完走し、ミリ秒以下でデータを移動できるようになることです。

 

 

 

Julien Prigent は、AWS の Linux クラウドサポートエンジニアです。 技術的な探求であれ、長距離のトレイルランであれ、体力の限界に挑戦することが好きです。