ElastiCache on Outposts を試す

こんにちは、イノベーションセンターの福田優真です。

NTT Comでは AWS Outposts を日本で初めて導入し、様々な検証を進めています。

今回は Amazon ElastiCache を Infrastructure as Code (IaC) によって Outposts 上へとプロビジョニングする際に得られたノウハウを共有したいと思います。

AWS Outposts

AWS Outposts は AWS が提供する、ハイブリッドクラウド1を実現するための製品です。

AWS が用意するラックやサーバーをオンプレ内に設置することで Amazon EC2Amazon S3 といったサービスをオンプレで利用できるようにしてくれます。 これによって AWS のサービスの柔軟性をオンプレ上に展開しつつ、データはオンプレ内のみで処理し、必要なところは AWS の各種サービスとシームレスに連携するといったことが可能になります。 また、オンプレ側に AWS サービスを展開できるため、 AWS のサービスを非常に近いロケーションで利用可能になるため、低レイテンシーが求められる関係で AWS の導入が難しかった場面でも AWS のサービスが利用できるようになります。

このようにオンプレ内へ AWS サービスの柔軟性を持ち込みつつ、オンプレとの連携をシームレスにやってくれるソリューションが Outposts です。 より詳しく Outposts について知りたい方は先述した本エンジニアブログの記事を更に参考にしてみてください。

現在、 Outposts は 42U ラックタイプと 1U/2U のサーバータイプの2種類が販売されていますが、今回利用する ElastiCache on Outposts は 42U ラックタイプでのみ提供されています2

Amazon ElastiCache

Amazon ElastiCache とは AWS が提供するフルマネージドで Memcached/Redis 互換なインメモリキャッシングサービスです。 パッチ適用やモニタリング、設定作業等は AWS 側にまかせつつ高速なキャッシュを利用できるため、本サービスを利用すれば自前で Memcached や Redis を運用するよりも楽にプロビジョニング・運用を行えます。 また、スケーリング等もノードの追加や削除のみを行えば自動で行ってくれるため、スケーラブルなキャッシュシステムを簡単に構築・運用できます。

AWS Cloud Development Kit

AWS Cloud Development Kit (CDK) は、 AWS 上のリソースを TypeScript や Go といったプログラミング言語を通して管理できるようにしてくれるフレームワークです。

CDK はリソースの状態を記述したテンプレートを元に各種リソースのプロビジョニングを行う AWS CloudFormation というサービスをバックエンドで用いています。 CloudFormation 自体は YAML や JSON でテンプレートを記述します。 そのため、全ての設定値を宣言的に書かなければならず、同じような設定値を何度も記述したり、一部プロパティが同じ設定値となる似たようなリソースをそれぞれ別個に記述したりしなければなりません。 CloudFormation は指定された配列の内容を Join する関数や、その配列の中から指定された位置の要素を取り出したりといった最低限の機能は提供されていますが、インフラの状態が大きくなればなるほど記述するテンプレートの行数は膨大になっていきます。

CDK はこれを TypeScript や Go を利用することで解決します。 具体的には TypeScript 等で記載されたコードからテンプレートを生成してくれます。 CDK を使うことで、ユーザーはプログラミング言語の関数化やクラス化といった強力な抽象力を用いてインフラのあるべき状態を記述できます。 プログラミング言語の抽象化力によって、ユーザーは YAML テンプレートと比較してより少ない記述で AWS リソースの状態を記述できます。

今回はこの CDK を通して ElastiCache on Outposts を利用する方法について解説します。 なお、ここでは CDK v2 のものを利用するので、 CDK v1 のものを利用したい方は別途読み替えてください。

ElastiCache on Outposts のプロビジョニング

VPC を準備する(必須)

ElastiCache は VPC 上にあるサブネットの上で動作します。 さらに、 ElastiCache を Outposts で利用するにはそのサブネットを Outpost 上へデプロイしておかなければなりません。 イメージとしては次のような図の通りの構成が要求されます。

CDK による Outposts 上への VPC とサブネットのプロビジョニングは、以前私が書いた記事で紹介しているので、そちらを参考にしてください。

この Outposts 上へプロビジョニングしたサブネットはそのままだと ElastiCache のサブネットとしては使えません。 ElastiCache にプロビジョニングしたサブネットを利用させるにはそのサブネットを含むサブネットグループというものを作る必要があります。 サブネットグループとは複数個のサブネットを1つの固まりとしたものです。

// TypeScript による例

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const subnetGroup = new cdk.aws_elasticache.CfnSubnetGroup(this, "SubnetGroup", {
      subnetIds: ["<Outposts 上にデプロイしたサブネットの ID>"],
      description: "<デプロイするサブネットグループを設置する説明等>",
      cacheSubnetGroupName: "<サブネットグループの名前>"
    });
  }
}

subnetIds にはサブネットグループへと登録したい Outposts 上にデプロイしたサブネットが持つ ID (subnet-xxx という形式をしています)を指定します。 また、 description にはデプロイするサブネットグループがどのような目的で設置されているのかといった説明を自由に記述できます。 ここに指定した文字列はデプロイしたサブネットグループの サブネットグループの詳細 画面の 説明 という欄に表示されます。 この description にはよりわかりやすくそのサブネットグループがデプロイされた経緯等を記載しておくとあとでデプロイされた目的等を把握しやすくなります。

cacheSubnetGroupName にはデプロイするサブネットグループに付ける名前を指定します。 ただし、この cacgeSubnetGroupName はデプロイ先アカウントで既にデプロイされている他のサブネットグループと被らないようにする必要があります。 名前が被った状態でデプロイしようとすると Cache subnet group <サブネットグループの名前> already exists. というエラーでデプロイできません。

ここまでで ElastiCache を利用するための準備は完了です。

クラスターモードが無効な ElastiCache をデプロイする

Outposts 上にプロビジョニングする方法はパブリッククラウド上にプロビジョニングする方法と変わりません。 Outposts へデプロイするにあたって本質的な部分となるのは Outposts 上へサブネットをプロビジョニングする部分であり、それさえ済んでしまえばあとはパブリッククラウドへプロビジョニングする通常の ElastiCache と変わりはありません。

// TypeScript による例

const replicationGroup = new cdk.aws_elasticache.CfnReplicationGroup(this, "ReplicationGroup", {
  cacheNodeType: "cache.m5.large",
  replicationGroupDescription: "<デプロイする ElastiCache クラスターの設置目的の説明等>",
  replicationGroupId: "<ElastiCache クラスターの ID>",
  cacheSubnetGroupName: subnetGroup.cacheSubnetGroupName,
  engine: "redis",
  engineVersion: "6.2",
  cacheParameterGroupName: "default.redis6.x",
  snapshotRetentionLimit: 1,
  numCacheClusters: 1,
  automaticFailoverEnabled: false,
  multiAzEnabled: false,
});

設定しているプロパティについて解説します。

cacheNodeType プロパティにはデプロイしたい ElastiCache のノードタイプを設定します。 ここに指定できるものは購入している Outposts 次第で変化します。 パブリック AWS 上の ElastiCache は様々なノードタイプを選択できます3が、 NTT Com 側で購入している Outposts は m5 のみ利用できるので、 cache.m5.large を指定しています。 おそらく、 r5 等を利用できる Outposts 上では cache.r5.large 等の指定が可能となるはずです。

replicationGroupDescription ですが、ここはサブネットグループの description プロパティと同じです。 このプロパティに設定した文字列が クラスターの詳細 画面の 説明 欄に表示されます。

replicationGroupId は ElastiCache クラスターに付ける名前を指定します。 ただし、名前は既にプロビジョニング済みである他の ElastiCache クラスターと被らないようにする必要があります。

cacheSubnetGroupName に先程 Outposts 上のサブネットを含めたサブネットグループの名前を指定します。 サブネットグループには cacheSubnetGroupName というプロパティがあるので、先に作成したサブネットグループの cacheSubhetGroupName を指定しています。

engine には使用する ElastiCache のエンジンを指定します。 今回は Redis を利用するので "redis" を指定します。

engineVersion には使用するエンジンのバージョンを指定します。 今回は "6.2" を指定して Redis v6.2 を利用するようにしています。

cacheParameterGroupName には ElastiCache のパラメータをまとめたパラメータグループというものの名前を指定します。 パラメータグループの画面には Memcached/Redis のバージョン毎のデフォルトパラメータを設定したパラメータグループがあり、今回は Redis の6系のパラメータグループを指定します。

numCacheClusters はクラスター数を指定するプロパティです。今回は検証用途で設定するため、 Outposts の容量を使いすぎないよう 1 に設定しています。

automaticFailoverEnabled は名前の通り、フェイルオーバー機能を有効にするかを設定します。 複数のクラスターをデプロイする(numCacheClusters1 よりも大きい値に設定する)場合 true を設定できます。 しかし、今回は numCacheClusters1 と設定しており、このとき automaticFailoverEnabledfalse に設定しなければなりません。 そのため、このプロパティには false を指定しています。

multiAzEnabled は複数の AZ で機能させるようにするかを指定するプロパティですが、今回は1つの Outposts でしか利用しないため、このプロパティには false を指定しています。

設定自体は以上で完了です。 しかし、実際にデプロイしようとするとサブネットグループと同時にデプロイしようとしている場合、サブネットグループの作成完了を待たずに並行して Redis クラスターのデプロイを試そうとします。 その時点ではサブネットグループの作成が完了していないため、デプロイ時に Cace subnet group "<プロビジョニングするサブネット名>" does not exist というエラーが出てデプロイに失敗します。

これを解決するには、作成したいサブネットグループと Redis クラスターの間にデプロイの順序関係を持たせます。

replicationGroup.addDependsOn(subnetGroup);

このコードによってサブネットグループがデプロイされたのちに Redis クラスターがデプロイされるという順序関係を宣言できます。 この関係性は削除のときにも発揮され、まずサブネットグループに依存している Redis クラスターが削除されてからサブネットグループが削除されるという順番になります。

以上の設定を施してデプロイすると ElastiCache クラスターが Available になるまで待ったのちにデプロイ完了となります。

Memcached / クラスターモード有効な Redis をデプロイする

Memcached やクラスターモードが有効な Redis もデプロイ可能なのですが、こちらはクラスターモードが無効な Redis クラスターと違い、いくつか注意点が存在します。 まず、 CloudFormation によるデプロイですが、元から CloudFormation で用意されている ElastiCache をデプロイするためのコンポーネントである AWS::ElastiCache::CacheCluster との相性が良くありません。 私たちもこのリソースに対応する CDK リソースでデプロイしようと試みてみましたが、 Can specify AZ arguments for outpost subnet group というエラーが出てデプロイできませんでした。

こういった CloudFormation が提供しているコンポーネントが利用できない場合、カスタムリソースというものを利用してデプロイを行うことになります。

カスタムリソースとは AWS API 等を実行する Lambda 関数を用意し、 CloudFormation によるプロビジョニング時にその Lambda 関数を実行して CloudFormation の枠組みでリソースのライフサイクルを管理する仕組みです。 これによって CloudFormation が提供しているコンポーネントでは扱えないリソースや新しい機能などを、 CloudFormation の枠組みの中で管理できるようになります。 今回のように Outposts 上へ CloudFormation からリソースをプロビジョニングする場合は、しばしば CloudFormation の提供するコンポーネントとの相性が悪いため、カスタムリソースを利用して管理することが多いです。

デプロイ方法ですが、次のようにします。

// TypeScript による例

const cacheCluster = new cdk.custom_resources.AwsCustomResource(this, "CacheCluster", {
  policy: cdk.custom_resources.AwsCustomResourcePolicy.fromSdkCalls({
    resources: cdk.custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE,
  }),
  onCreate: {
    service: "ElastiCache",
    action: "createCacheCluster",
    physicalResourceId: cdk
      .custom_resources
      .PhysicalResourceId.fromResponse("CacheCluster.CacheClusterId"),
    parameters: {
      CacheClusterId: "<ElastiCache クラスター名>",
      NumCacheNodes: 1,
      CacheNodeType: "cache.m5.large",
      // Redis を利用する場合は `Engine` に "redis" を指定します
      Engine: "memcached",
      // Engine が `redis` である場合は Redis のバージョン(たとえば `6.2`)を指定します
      EngineVersion: "1.6.12",
      // Engine が `redis` である場合はパラメータグループも Redis のもの
      // (たとえば6系の Redis を利用するのであれば `default.redis6.x`)を指定します
      CacheParameterGroupName: "default.memcached1.6",
      CacheSubnetGroupName: subnetGroup.cacheSubnetGroupName,
    },
  },
  // ここはちゃんと動いていませんが、参考として
  // onDelete: {
  //   service: "ElastiCache",
  //   action: "deleteCacheCluster",
  //   parameters: {
  //     CacheClusterId: "<ElastiCache クラスター名>"
  //   }
  // },
});

CloudFormation ではカスタムリソースのプロビジョニング時に呼ばれる Lambda 関数のコードを自前で書いたり、その Lambda 関数を CloudFormation が呼び出せるようにグルーコードを書いたりする必要があります。

一方、 CDK を利用している場合、 AWS API を呼び出すのみであればこれら Lambda 関数のコードや Lambda 関数を CloudFormation から駆動するためのグルーコードをラップしてくれている AwsCustomResource というコンポーネントを利用できます。 この AwsCustomResource は作成時や削除時に呼ばれる AWS API の名前や設定値を記載するだけで指定した AWS API を呼び出すカスタムリソースを用意できます。

今回は ElastiCache の CreateCacheCluster という API を使ってクラスターモードが有効な ElastiCache on Outposts クラスターを構築したいので、 serviceElastiCache を指定し、 actioncreateCacheCluster を指定しています。 AwsCustomResource 内で用意される Lambda 関数は JavaScript で記述されている4ようで、 AWS SDK for JavaScript の API リファレンス から serivce に利用したいサービスと対応するサービス名を、 action へ利用したい API と対応したメソッド名を指定します。 たとえば、 EC2 のインスタンスを AwsCustomResource で作成するのであれば、 serviceEC2 を、 actionrunInstances を指定します。

physicalResourceId には AWS API のレスポンスにある CacheCluster.CacheClusterId というプロパティの値を設定するようにしています。 基本的に physicalResourceId には AWS API のレスポンス内に含まれるリソースの ID を指定すれば問題ありません。 リソースの ID がどのプロパティに含まれているのかは AWS SDK for JavaScript の API リファレンスを参照してください。 AWS SDK for JavaScript のレスポンスも JavaScript のオブジェクトなので、プロパティにアクセスする際は JavaScript オブジェクトのプロパティアクセスと同じ表記でアクセスします(例示したコードであれば CacheCluster オブジェクト内に CacheClusterId というプロパティがあるので、 CacheCluster.CacheClusterId を指定しています)。

// TypeScript による例

// レスポンスから Physical Resource ID を取得してくれる便利メソッド
cdk.custom_resources.PhysicalResourceId.fromResponse(
  // レスポンス内の CacheCluster.CacheClusterId プロパティにデプロイした
  // ElastiCache クラスターの ID が入っているので、これを Physical Resource ID として利用する
  "CacheCluster.CacheClusterId"
)

parameters には AWS SDK for JavaScript の各種メソッドに指定するパラメータ値をそのまま記載します。 今回のコード例であれば createCacheCluster メソッドに指定する値として次のものを指定しています。

  • CacheClusterId
  • NumCacheNodes
  • Engine
  • EngineVersion
  • CacheParameterGroupName
  • CacheSubnetGroupName

ほとんどはクラスターモードが無効な ElastiCache クラスターと同じプロパティなので、差分である CacheClusterId について説明します。 こちらはクラスターモードが無効な ElastiCache クラスターの replicationGroupId に相当するもので、プロビジョニングする ElastiCache クラスターにつける名前を指定します。 ただし、指定する名前は既にデプロイされている他の ElastiCache クラスターと被らないものを設定する必要があります。

クラスターモードが有効な ElastiCache クラスターもクラスターモードが無効な Redis クラスターと同じで先にサブネットグループが作成されている必要があるため、次のようにしてデプロイ順序を設定しておく必要があります。

// TypeScript による例

cacheCluster.node.addDependency(subnetGroup);

今回は ElastiCache クラスターの削除部分をコメントアウトによって無効化していますが、削除する場合は onDelete へ使用する AWS API に対応した AWS SDK for JavaScript のサービス名とメソッド名を各 service, action プロパティに指定します。 ElastiCache であれば ElastiCache サービスに deleteCacheCluster というものがあるので、そちらを serivceaction に設定します。 deleteCacheCluster は削除するクラスター名だけ指定すればいいので、 parameters には削除するキャッシュクラスター ID を指定しています。

デプロイ方法を紹介するために簡便な AwsCustomResource を利用していますが、これをそのまま使うには注意が必要です。 AwsCustomResource は対象とするリソースの各ライフサイクル(削除/更新/作成)にあたって AWS API を1つしか指定できません。 今回であれば CreateCacheCluster API, DeleteCacheCluster API を削除/作成のライフサイクルに利用しているため、これらライフサイクルにて他の AWS API を利用できません。 なので、 ElastiCache のように AWS API からのレスポンスはすぐに返ってくるが、リソースの初期化処理や削除処理がその後に裏で動くような場合に問題が発生します。

CreateCacheCluster API はリソースの作成までは待機してくれるものの、リソースの初期化処理までは待機してくれません。 初期化が終了しなければデプロイした ElastiCache クラスターは削除できないため、 CDK や CloudFormation を通して初期化処理が終了するまでリソースの削除を行えません。 さらに、作成した ElastiCache クラスターの削除についても DeleteCacheCluster API を利用しますが、こちらも削除が完了するのを待たずに削除が開始した時点でレスポンスが返ってきます。 これによって、リソースの削除時に削除が完了していないがカスタムリソースとしてはレスポンスが返ってきた時点でリソースの削除は完了したとみなされます。 すると、まだ ElastiCache クラスターが削除されていないのに ElastiCache クラスターを削除したと見做されサブネットグループの削除が開始されます。 しかし、サブネットグループは ElastiCache クラスターが完全に削除されなければ削除できないため、サブネットグループの削除に失敗するという状況が発生します。

これは今回紹介する AwsCustomResource では回避できませんが、 AwsCustomResource の利用を止め、自前でカスタムリソースをハンドルする Lambda 関数を用意すれば回避できます。 具体的にはハンドラーである Lambda 関数へリソースの完全削除等を監視する仕組みを入れれば問題は回避できます5

まとめ

ElastiCache on Outposts を利用する手段を CDK を用いた場合で解説しました。 ElastiCache on Outposts 自体は AWS のマネージメントコンソールからオンプレミス上にデプロイするように指定すればデプロイできるのですが、 IaC をしたい場合はここに載せた方法を参考にしてもらえれば幸いです。

ただし、記事中でも触れていますがまだ上手くデプロイできなかったりデプロイは可能なものの CDK/CloudFormation での管理をしづらかったりと IaC をやるには難しい部分もあります。 サブネットグループとクラスターのみあれば最低限十分であるため、 ElastiCache だけ IaC の枠組みからはずして管理するということも選択肢には入ると思います。

一方、クラスターモードを無効にした Redis であれば CloudFormation の範囲でデプロイでき、きちんと Available になるまで待機してリソースの作成完了としてくれるため、 Outposts 上で動かすのも Public AWS と変わらないレベルで簡単にデプロイし管理できるのは驚きでした。 すなわち、簡単にオンプレミス上にフルマネージドな Redis クラスターを作成・運用できるため、運用の大部分を簡素化できますし、 IaC による管理も可能となりインフラの再現性も高めることができます。

最後になりますが、 Outposts 上の ElastiCache を利用する場合の参考にしていただければと思います。


  1. 今日、 ハイブリッドクラウド の語は、クラウドベンダーごとに異なる解釈が与えられています。この記事では AWS Outposts を紹介しますので、 AWS の提唱する クラウドからオンプレミス、そしてエッジまで、必要な場所で一貫した AWS エクスペリエンスを提供 することをハイブリッドクラウドであるとします(参考)。
  2. AWS Outposts の概要 にある 特徴の比較 に AWS Outposts ラックは ElastiCache の記載があり、一方 AWS Outposts サーバーにはないことが確認できます。
  3. https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html
  4. https://github.com/aws/aws-cdk/blob/8b20446aeccc787de1511f01dd27cc590d57ce89/packages/%40aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts#L397
  5. ただし、 Lambda 関数の最大実行時間は 15分 です。したがってデプロイ時間を超える場合は回避するのがより難しいものとなります。
© NTT Communications Corporation All Rights Reserved.