AWS CDKでリソース作成 第6回: ElastiCache編

こんにちは。 エキサイト株式会社で内定者アルバイトをしています。

今回も、実際のリソースの作成を行いたいと思います。第6回は、ElastiCacheです。

前の記事 tech.excite.co.jp

ElastiCacheとは?

名前にもある通り、インメモリキャッシングのためのリソースです。

アプリケーションとデータベースパフォーマンスを高速化するキャッシングに使ったり、セッションストア、ゲーミングリーダーボード、ストリーミング、および分析などの耐久性を必要としないユースケースのデータストアとして使用できます。

RedisとMemcachedと互換性があります。

AWSにおけるRedis

今回はRedisのキャッシュを作成するので、AWSにおけるRedisについてもう少し詳しく説明します。

ノード(Node)

ElastiCacheにおけるRedisの最小単位で、EC2インスタンスを使用して動作します。
ノードは後ほど紹介するシャードに属しています。

各ノードはクラスタを作成した時に設定したバージョンで動作しており、クラスタ内のノードは全て同じノードタイプを持ちます。

シャード(Shard)

シャードは1~6個のノードをまとめたもので、そのうち1個が書き込みノードであるprimaryで、残りの最大5個のノードは読み込み専用のreplicaとなります。

シャードはクラスタに属しており、一つのクラスタは最大500個のシャードを持つことができます。(どれだけの数のシャードを作成し、そのシャードにどれだけのノードを割り当てるかは任意に決定できます。)

クラスタ

クラスタはシャードをまとめており、複数のシャードを作成することでシャーディング(データをシャード間で分割する。読み書きの負荷分散やコストパフォーマンス向上の効果があるよう。)が可能です。(ただし、クラスタモードというものを有効にする必要があります。クラスタモードが無効の場合はシャードは1つです。)

クラスタモードについては以下のページ内の図が参考になるかと思います。

レプリケーション: Redis (クラスターモードが無効) 対 Redis (クラスターモードが有効) - Amazon ElastiCache for Redis

実際に作成してみる

では、実際にElastiCacheのリソースを作成します。

今回は、前回までで作成しているVPC上にリソースを作成します。
作成するリソースの設定は以下の通りです。

項目 設定
モード Redis
エンジンバージョン 5.0.6
キャッシュノードタイプ cache.t2.micro
メンテナンスウィンドウ 土曜日 16:30 - 土曜日 17:30 UTC

また、ノードグループ(シャードのこと)に対する設定は以下の通りです。

項目 設定
primaryのアベイラビリティゾーン ap-northeast-1a
replicaの数 2
replica1のアベイラビリティゾーン ap-northeast-1c
replica2のアベイラビリティゾーン ap-northeast-1c

前回までのコード

前回はRoute 53を作成しましたが、今回は必要ないためVPCとサブネットを作成する部分のみ再掲します。

package com.myorg;

import software.amazon.awscdk.CfnTag;
import software.amazon.awscdk.services.ec2.CfnInternetGateway;
import software.amazon.awscdk.services.ec2.CfnVPCGatewayAttachment;
import software.amazon.awscdk.services.ec2.PrivateSubnet;
import software.amazon.awscdk.services.ec2.PublicSubnet;
import software.amazon.awscdk.services.ec2.Vpc;
import software.constructs.Construct;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;

import java.util.List;

public class MyProjectStack extends Stack {
    public MyProjectStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public MyProjectStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);

        Vpc vpc = Vpc.Builder.create(this, "SampleVPC")
                .vpcName("sample-vpc")
                .cidr("cidr")
                .subnetConfiguration(List.of())
                .build();

        // Availability zone: ap-northeast-1a
        PrivateSubnet privateSubnetA = PrivateSubnet.Builder.create(this, "PrivateSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetA = PublicSubnet.Builder.create(this, "PublicSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // Availability zone: ap-northeast-1c
        PrivateSubnet privateSubnetB = PrivateSubnet.Builder.create(this, "PrivateSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetB = PublicSubnet.Builder.create(this, "PublicSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // NAT gatewayの追加
        publicSubnetA.addNatGateway();
        publicSubnetB.addNatGateway();

        // Internet Gateway
        CfnInternetGateway internetGateway = CfnInternetGateway.Builder.create(this, "InternetGateway")
                .tags(List.of(CfnTag.builder()
                        .key("Name")
                        .value("sample-internet-gateway")
                        .build()))
                .build();

        // Internet gatewayをVPCにattachする
        CfnVPCGatewayAttachment.Builder.create(this, "VpcGateAwayAttachment")
                .vpcId(vpc.getVpcId())
                .internetGatewayId(internetGateway.getAttrInternetGatewayId())
                .build();
    }
}

ElastiCacheを作成するコード

        // ElastiCache
        // ElastiCache用のセキュリティグループ
        SecurityGroup sampleCacheRedisSecurityGroup = SecurityGroup.Builder.create(this, "SampleCacheRedisSecurityGroup")
                .securityGroupName("sample-cache-redis-security-group")
                .vpc(vpc)
                .allowAllOutbound(true)
                .description("security-group-for-sample-cache-redis")
                .build();

        // ElasticCache用のサブネットグループ
        CfnSubnetGroup cacheSubnetGroup = CfnSubnetGroup.Builder.create(this, "CacheSubnetGroup")
                .cacheSubnetGroupName("sample-cache-subnet-group")
                .description("subnet-group-for-cache")
                .subnetIds(List.of(privateSubnetA.getSubnetId(), privateSubnetB.getSubnetId()))
                .build();

        sampleCacheRedisSecurityGroup.addIngressRule(Peer.ipv4("0.0.0.0/0"), Port.tcp(6379));

        CfnParameterGroup parameterGroup = CfnParameterGroup.Builder.create(this, "RedisCacheParameterGroup")
                .cacheParameterGroupFamily("redis5.0")
                .description("parameter-group-for-redis-cache")
                .build();

        CfnReplicationGroup redisReplicationGroup = CfnReplicationGroup.Builder.create(this, "RedisReplicationGroup")
                .replicationGroupDescription("")
                .engine("redis")
                .engineVersion("5.0.6")
                .autoMinorVersionUpgrade(true)
                .cacheNodeType("cache.t2.micro")
                .cacheParameterGroupName(parameterGroup.getRef())
                .replicationGroupId("sample-redis-cache")
                .preferredMaintenanceWindow("sat:16:30-sat:17:30")
                .securityGroupIds(List.of(sampleCacheRedisSecurityGroup.getSecurityGroupId()))
                .cacheSubnetGroupName(cacheSubnetGroup.getCacheSubnetGroupName())
                .nodeGroupConfiguration(List.of(CfnReplicationGroup.NodeGroupConfigurationProperty.builder()
                        // 4文字より多いとダメらしい
                        .nodeGroupId("1")
                        .primaryAvailabilityZone("ap-northeast-1a")
                        .replicaAvailabilityZones(List.of("ap-northeast-1c", "ap-northeast-1c"))
                        .replicaCount(2)
                        .build()))
                .atRestEncryptionEnabled(false)
                .build();

        redisReplicationGroup.addDependsOn(cacheSubnetGroup);
        redisReplicationGroup.addDependsOn(parameterGroup);

長くなりましたので、以下でこのコードについて少し説明します。

前半部分はElastiCache用のセキュリティグループとサブネットグループを作成しています。

後半は、ElastiCacheに直接関係する部分になります。CfnParameterGroupはElastiCacheのためのパラメータグループです。今回はRedis5.0系のクラスタを作成しますのでcacheParameterGroupFamilyredis5.0を指定しています。

ちなみにElastiCacheは現段階ではL2コンストラクトに対応していないリソースの一つで、CDKで作成するためにはCloudFormationから直接生成されたより低レベルなコンストラクトであるL1コンストラクトを使用する必要があります。

最後に、Redisクラスタを作成しています。
この部分は非常にややこしいです。
実は、CfnCacheClusterというコンストラクトがあります。
名前の感じではこれでもRedisクラスタを作成できそうですが、実はできません。

正確には、できなくはないが制約が大きすぎるためお薦めしません。その制約はengineredisの場合はノードの数numNodeCacheを1にしなければならない。というものです。
実際、numNodeCacheを1以外にして無理矢理Redisのクラスタを作成しようとすると、

NumCacheNodes should be 1 if engine is redis(エンジンがRedisなんだったらNumCacheNodesは1にしろよ)と怒られます。

これでは、あまりにも使いにくいですね....

そこで、代わりに使用するのは上のコードの通りCfnReplicationGroupです。

これを使えば、Redisでも自由にノード数を設定することができます。

ただし、このコンストラクトにも落とし穴があります。

それは以下の5つのプロパティの相互関係です。

automaticFailoverEnabled
numNodeGroups
numCacheClusters
replicasPerNodeGroup
nodeGroupConfiguration: replicaCount

それぞれについて、説明します。

  • automaticFailoverEnabled(boolean)
    • primaryが機能しなくなった時にreplicaのうちの一つが自動的にprimaryとなること(フェイルオーバー)を許可するかどうか
  • numNodeGroups(Number)
    • ノードグループ(すなわちシャード)の数
    • クラスタモード無効のRedisでは設定しないか、1にする
  • numCacheClusters(Number)
    • このReplication Groupが最初に持つクラスタの数
    • automaticFailoverEnabledをtrueにすると2以上6以下の値でなければならない。
    • automaticFailoverEnabledをfalseにするとデフォルトが1で2以上6以下の値も設定できる
    • クラスタモードが無効の場合に指定する。
    • このプロパティとnumNodeGroupsreplicasPerNodeGroupnodeGroupConfiguration: ReplicaCountは同時に指定できない
    • クラスタの数とありますが、クラスタが増えるわけではなく、ノードの数が増える模様
  • replicasPerNodeGroup(Number)
    • 各ノードグループ(シャード)内のreplicaの数
    • nodeGroupConfiguration: replicaCountと同時に指定することができない
  • nodeGroupConfiguration: replicaCount
    • このノードグループ内のreplicaの数(クラスタモードが有効の場合、nodeGroupConfigurationはノードグループ数だけ指定する)

まとめると、

  • クラスタモード無効かつフェイルオーバー無効numCacheClustersで1〜6の値を設定することでノードの数を指定するか(デフォルト1で省略可)、replicaPerNodeGroupまたはnodeGroupConfiguration: replicaCountでシャード内のノードの数(クラスタ内のノードの数と一致)を指定する。

  • クラスタモード無効かつフェイルオーバー有効numCacheClustersで2〜6の値を設定することでクラスタの数を指定し(省略不可)、replicaPerNodeGroup()またはnodeGroupConfiguration: replicaCountでシャード内のノードの数(クラスタ内のノードの数と一致)を指定する。

  • クラスタモード有効numNodeGroupsでシャードの数を指定し、replicasPerNodeGroupで全部のシャードについてまとめてノード数を指定するか、nodeGroupConfiguration: replicaCountで各シャードのノードの数を指定する。

よくわからなければ、nodeGroupConfiguration: replicaCountを使用すればnodeGroupConfigurationの他のプロパティで詳細な設定もできますし、クラスタモードの有効無効に関わらず使えるので、大抵の場合このプロパティで統一してしまっても問題ないと思います。

このブログのコードでも、nodeGroupConfiguration: replicaCountを使用しています。

最後に、CfnSubnetGroupCfnParameterGroupCfnReplicationGroupはL1コンストラクトでリソース作成時の順番がコードに書いた順と必ずしも一致しませんので、最後の2行でリソース作成の際の依存関係を指定しています。 詳細は、以下の記事で解説していますので、興味がある方はご覧ください。

tech.excite.co.jp

終わりに

今回は、AWS CDKを用いてRedisキャッシュを作成しました。

ElastiCache自体がまだL2コンストラクトに対応しておらず、ノード数の設定回りが非常にややこしくなっており、かなりわかりづらいです。
今後、この辺りが改善されるといいですね...。

では、また次回。

次の記事

tech.excite.co.jp

参考文献