mazyu36の日記

某SIer所属のクラウドエンジニアのブログ

CDK L1 / CloudFormationでElastiCacheのUserを作成する際のハマりどころをまとめる

2024/10/8に ElastiCache for Valkeyが利用可能になりました。

aws.amazon.com

特にElastiCache Severless for Valkeyだと月額約6ドル程度から使用でき、なかなか魅力的です。

ServerlessだとRBACによる権限制御が必要であり、Userを作成してキャッシュに紐づける必要があります。

RBAC is also the only way to control access to serverless caches.

docs.aws.amazon.com

このUserの仕様がなかなかにクセがあるため、ハマったところをまとめておきます。

なおCDK L1 Constructをベースに記載しますが、CloudFormationでも同様です。

class CfnUser (construct) · AWS CDK

目次

Userの作成

RBACのためにキャッシュを使用するユーザーを作成する必要があります。

例えばパスワード認証を行うユーザーの実装例としては以下になります。

const passwordUser1 = new CfnUser(this, 'PasswordUser1', {
  engine: 'redis',  // エンジン(redis固定)
  userId: 'password-user-id-1', 
  userName: 'password-user-1',
  accessString: 'on ~app:*',  // 権限を指定
  passwords: [cdk.SecretValue.unsafePlainText('UserPassword1234').unsafeUnwrap()], // パスワードを指定
})

以降ハマった点を記載していきます。

engine は redis 固定

まず前提としてRBACはValkeyとRedis OSSでのみ使えます。

で、Valkeyを使うときはenginevalkeyを設定したくなりますが、ユーザーとユーザーグループはValkeyを使う場合でもredisの設定が必要です(つまり実質redisの固定値)

When using RBAC with Valkey clusters, you will still need to assign users and user groups the engine “redis”.

docs.aws.amazon.com

accessString はoptionalだが設定しないとdeployできない

accessStringに該当ユーザーの権限を指定します。

これはSyntaxが定められているのでドキュメントを参照してください。on ~* +@allだと該当ユーザーをアクティブにし、すべてのキーに対してすべてのコマンドが使用できる、というような意味です。

docs.aws.amazon.com

このaccessStringですが、ドキュメント上では Optional なものの、未設定だとdeploy時にInternal Errorになります。

上記のため実質必須項目です。

なおマネジメントコンソールでaccessStringに該当する項目を未設定のまま作成すると、off -@all が設定されます(該当ユーザーはインアクティブ、かつすべての操作ができない)。

認証方式が複数、かつ設定方法も複数ある

ユーザーの認証方式としては以下の3つがあります。

  1. パスワードなし
  2. パスワード認証
  3. IAM

このうち1と2は設定方法が2通りあります。

「1. パスワードなし」のユーザー

以下のように noPasswordRequired による設定と authenticationModeによる設定の2通りがあります。

なぜ2通りあるかは推測ですが、最初は前者だけだったものの、「3. IAM」の認証方式が後から追加になり後者が増えたのかと思います。

authenticationModeany なので以下のパターン2のように設定してやる必要があります。若干ややこしいですね。

基本的にはパターン1で設定するのが良いのではないかと思います。

// ------- パスワード不要のユーザー -------
const noPasswordUser1 = new CfnUser(this, 'NoPasswordUser1', {
  engine: 'redis',
  userId: 'no-password-user-id-1',
  userName: 'no-password-user-1',
  accessString: 'on ~* +@all',
  // パスワード不要を設定(パターン1)
  noPasswordRequired: true,
})

const noPasswordUser2 = new CfnUser(this, 'NoPasswordUser2', {
  engine: 'redis',
  userId: 'no-password-user-id-2',
  userName: 'no-password-user-2',
  accessString: 'on ~* +@all',
  // パスワード不要を設定(パターン2)
  authenticationMode: {
    Type: 'no-password-required'
  },
})

「2. パスワード認証」のユーザー

続いてパスワード認証ありのユーザーです。パスワードは最大2コマで設定できます。

これも2通りあり、 passwords による設定と authenticationModeによる設定の2通りがあります。

1と同様に前者のほうが扱いやすいと思います。

// ------- パスワード認証のユーザー -------
const passwordUser1 = new CfnUser(this, 'PasswordUser1', {
  engine: 'redis',
  userId: 'password-user-id-1',
  userName: 'password-user-1',
  accessString: 'on ~* +@all',
  // パスワード認証を設定(パターン1)
  passwords: [cdk.SecretValue.unsafePlainText('UserPassword1234').unsafeUnwrap()],
})

const passwordUser2 = new CfnUser(this, 'PasswordUser2', {
  engine: 'redis',
  userId: 'password-user-id-2',
  userName: 'password-user-2',
  accessString: 'on ~* +@all',
  // パスワード認証を設定(パターン2)
  authenticationMode: {
    Type: 'password',
    Passwords: [cdk.SecretValue.unsafePlainText('UserPassword1234').unsafeUnwrap()],
  },
})

「3. IAM」のユーザー

最後にIAM認証のパターンです。これはauthenticationModeによる設定のみです。

// ------- IAM認証のユーザー -------
const iamUser = new CfnUser(this, 'IamUser', {
  engine: 'redis',
  // 認証方式がIAMの時はuserIdとuserNameが一致する必要あり
  userId: 'iam-user',
  userName: 'iam-user',
  accessString: 'on ~* +@all',
  // IAM認証を設定
  authenticationMode: {
    Type: 'iam',
  },
})

なおIAMの時はuserIduserNameは同じ値の必要があります。こちら含め制約は以下のドキュメントにまとまっています。

docs.aws.amazon.com

デフォルトユーザー作成とUserGroup作成

作成したUserUserGroupに追加して、UserGroupをキャッシュに関連づけることで、ユーザーとキャッシュの紐付けが可能です。

このUserGroupですが一点注意が必要で、userNamedefaultのユーザー(デフォルトユーザー)を含める必要があります(userIdではないので注意)

以下のドキュメントに詳細の記載があります。

docs.aws.amazon.com

UserGroupではデフォルトのユーザー(userNamedefault)が必須になります。 そのため以下のどちらかのユーザーを必ずUserGroupに含める必要があります。

  1. ElastiCacheが自動作成しているデフォルトユーザー(userId: default, userName: default) ※編集不可
  2. 新たに作成したデフォルトユーザー(userId: default以外, userName: default)

1は自動作成されていますが、編集が不可です。またaccessStringon ~* +@allと強い権限になっています。そのためデフォルトの権限を弱くしたい場合は、別のuserIdでデフォルトユーザーを作成する形になります。

以下はデフォルトのユーザーとUserGroupの実装例です。

// ------- デフォルトユーザー -------
const newDefaultUser = new CfnUser(this, 'NewDefaultUser', {
  engine: 'redis',
  // IDはdefault"以外”にする
  userId: 'new-default-user',
  // Nameは default にする
  userName: 'default',
  // デフォルトユーザーの権限や認証方式を設定
  accessString: 'off -@all',
  authenticationMode: {
    Type: 'no-password-required',
  },
})

// ------- ユーザー作成後 -------Ï
// ユーザーグループを作成
const userGroup = new CfnUserGroup(this, "RedisRbacUserGroup", {
  userGroupId: "user-group-id",
  engine: "redis",
  userIds: [
    noPasswordUser1.ref,
    noPasswordUser2.ref,
    passwordUser1.ref,
    passwordUser2.ref,
    iamUser.ref,
    newDefaultUser.ref, // userName が default のユーザーを含める必要あり
  ],
});

UserGroupをキャッシュに関連付け

最後にUserGroupをキャッシュに関連付けて終わりです。以下はServerlessの例です

const cluster = new CfnServerlessCache(this, 'ServerlessCache', {
  engine: 'valkey', // valkeyを指定
  serverlessCacheName: 'my-serverless-cache',
  majorEngineVersion: '7',
  userGroupId: userGroup.ref, // UserGroupを関連付け
});

終わりに

そんなややこしい UserUserGroup ですが、Serverless Cacheと合わせて、open-constructsでL2 Constructを作成しています(執筆時点では未レビューのためマージ前)

マージされたらまた改めて記事書きます。

github.com