🔐

AWS CDK で Grants クラスが導入されました(v2.227.0)

に公開

AWS CDK の L2 Construct を利用するメリットの 1 つとしてgrantsメソッドによる権限付与の抽象化があります。例えば以下は L2 Construct で SNS トピックを生成し、IAM ロールに対して Publish の権限を付与します。

declare const role: iam.IRole;
const topic = new sns.Topic(this, "MyTopic");

// L2コンストラクトのメソッドを直接呼び出し
topic.grantPublish(role);

この grants メソッドですが PR #35782 でリファクタが行われ、v2.227.0 から Grants クラスが導入されました。

これにより grants の利用方法が変更となっています。従来の方法も現状は使えますが、今後は deprecated になっていく方向性であり、新方式での実装が推奨されます。

この記事では利用方法や、AWS CDK 自体の実装がどのように変わったかを解説します。

Grants クラスの利用方法

以下のように grants オブジェクトを経由して権限付与する形になります。

declare const role: iam.IRole;
const topic = new sns.Topic(this, "MyTopic");

// grants オブジェクトを経由して権限付与
topic.grants.publish(role);

もしくは Grants クラスからインスタンスを生成し、それを元に権限付与を行います。L1 Construct でも利用可能です。

declare const role: iam.IRole;

// L2 Construct で Grants クラスからインスタンスを生成
const topic = new sns.Topic(this, "MyTopic");
const topicGrants = TopicGrants.fromTopic(cfnTopic);
topicGrants.publish(role);

// L1 Construct も同様の使い方が可能
const cfnTopic = new sns.CfnTopic(this, "CfnTopic");
const topicGrants = TopicGrants.fromTopic(cfnTopic);
topicGrants.publish(role);

なお aws-rds や alpha モジュールなど、執筆時点では新しい grants が実装されていないものもあるのでご注意ください。

従来の grants はどうなるのか?

PR #35782 で以下のように記載されています。

"The grantPublish etc methods on the L2 are now no longer recommended (though they will not be deprecated immediately to not disrupt existing code too much)."

上記のように従来の grant メソッドは 「今後は推奨されない」 形になります(ただしすぐには deprecated にしない)

そのため今後新規に実装する際は Grants クラスを使いつつ、従来のものはどこかのタイミングで移行を考えると良いでしょう。

移行方法は?

以下のように grants メソッドを使っていたところを、grants オブジェクト経由に書き換えるだけです。

declare const role: iam.IRole;
const topic = new sns.Topic(this, "MyTopic");

- topic.grantPublish(role);
+ topic.grants.publish(role);

以下のように Grants クラスからインスタンスを生成する形でも問題ありません。

declare const role: iam.IRole;
const topic = new sns.Topic(this, "MyTopic");

+ // これでも OK
+ const topicGrants = TopicGrants.fromTopic(topic);
+ topicGrants.publish(role);

なおこの書き換えを行っても diff は発生しません。

$ npx cdk diff

start: Building CdkTestStack Template
success: Built CdkTestStack Template
start: Publishing CdkTestStack Template (current_account-current_region-570525fb)
success: Published CdkTestStack Template (current_account-current_region-570525fb)
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)

Stack CdkTestStack
There were no differences

aws-cdk の実装内容はどう変わったのか

ここからは CDK 自体の実装がどう変わったのかを見ていきます(そのため CDK を利用するだけであれば特に意識しなくて良い範囲です)

今回の変更内容として PR #35782 では以下のように言及されています。

"Instead of grants being attached to the L2 class, they are now available as separate classes."

aws-sns でどのように実装が変わったかを見てみます。

v2.227.0 より前の実装

以下のように L2 の Class において grants のロジックがそのまま実装されています。

/**
 * Either a new or imported Topic
 */
export abstract class TopicBase extends Resource implements ITopic {
// omit

  /**
   * Grant topic publishing permissions to the given identity
   */
  public grantPublish(grantee: iam.IGrantable) {
    const ret = iam.Grant.addToPrincipalOrResource({
      grantee,
      actions: ['sns:Publish'],
      resourceArns: [this.topicArn],
      resource: this,
    });
    if (this.masterKey) {
      this.masterKey.grant(grantee, 'kms:Decrypt', 'kms:GenerateDataKey*');
    }
    return ret;
  }

// omit

v2.227.0 以降の実装

以下のように実装が変わりました。

grants オブジェクトをプロパティとして保持しており、これにより topic.grants.publish(...) のように権限付与が行えるようになります。

また従来の grants メソッドも実装は維持されていますが、実装内容は新方式のものに書き換えられています。

+ import { TopicGrants } from './sns-grants.generated';

// omit

/**
 * Either a new or imported Topic
 */
- export abstract class TopicBase extends Resource implements ITopic {
+ export abstract class TopicBase extends Resource implements ITopic, IEncryptedResource {

  // omit

  // grants をオブジェクトとして保持するよう修正
+  /**
+   * Collection of grant methods for a Topic
+   */
+  public readonly grants: TopicGrants = TopicGrants.fromTopic(this);

  // omit

  // 従来の grants メソッドも後方互換性のため維持。ただし `grants` オブジェクトを使用するように修正
  /**
   * Grant topic publishing permissions to the given identity
   */
-  public grantPublish(grantee: iam.IGrantable) {
-    const ret = iam.Grant.addToPrincipalOrResource({
-      grantee,
-      actions: ['sns:Publish'],
-      resourceArns: [this.topicArn],
-      resource: this,
-    });
-    if (this.masterKey) {
-      this.masterKey.grant(grantee, 'kms:Decrypt', 'kms:GenerateDataKey*');
-    }
-    return ret;
+    return this.grants.publish(grantee);
  }

// omit

TopicGrants はどこからきているのか?という話になりますが、この sns-grants.generated.ts 自体はリポジトリに存在しません。

import 元のファイル名の ...generated からも推測できますがこれは自動生成されます。ポリシーなどが定義されたJSON からビルド時に自動生成されています。

JSON では以下のようにアクションや対応するポリシーなどが定義されています。

{
  "resources": {
    "Topic": {
      "hasResourcePolicy": true,
      "grants": {
        "publish": {
          "actions": ["sns:Publish"],
          "keyActions": ["kms:Decrypt", "kms:GenerateDataKey*"],
          "docSummary": "Grant topic publishing permissions to the given identity"
        },
        "subscribe": {
          "actions": ["sns:Subscribe"],
          "docSummary": "Grant topic subscribing permissions to the given identity"
        }
      }
    }
  }
}

ビルドするとこの JSON を元にした自動生成ファイルが生成されます。これ自体は spec2cdk で行われています(今回は詳細は割愛します。機会があれば別途記事にします)

生成されたファイルを見ると、以下のように従来の grants メソッドに近い内容のものが生成されていることがわかります。

sns-grants.generated.ts
/* eslint-disable @stylistic/max-len, eol-last */
import * as sns from "./sns.generated";
import * as iam from "aws-cdk-lib/aws-iam";

/**
 * Properties for TopicGrants
 */
interface TopicGrantsProps {
  /**
   * The resource on which actions will be allowed
   */
  readonly resource: sns.ITopicRef;

  /**
   * The resource with policy on which actions will be allowed
   *
   * @default - No resource policy is created
   */
  readonly policyResource?: iam.IResourceWithPolicyV2;

  /**
   * The encrypted resource on which actions will be allowed
   *
   * @default - No permission is added to the KMS key, even if it exists
   */
  readonly encryptedResource?: iam.IEncryptedResource;
}

/**
 * Collection of grant methods for a ITopicRef
 */
export class TopicGrants {
  /**
   * Creates grants for TopicGrants
   */
  public static fromTopic(resource: sns.ITopicRef): TopicGrants {
    return new TopicGrants({
      resource: resource,
      encryptedResource: (iam.GrantableResources.isEncryptedResource(resource) ? resource : undefined),
      policyResource: (iam.GrantableResources.isResourceWithPolicy(resource) ? resource : undefined)
    });
  }

  protected readonly resource: sns.ITopicRef;

  protected readonly encryptedResource?: iam.IEncryptedResource;

  protected readonly policyResource?: iam.IResourceWithPolicyV2;

  private constructor(props: TopicGrantsProps) {
    this.resource = props.resource;
    this.encryptedResource = props.encryptedResource;
    this.policyResource = props.policyResource;
  }

  /**
   * Grant topic publishing permissions to the given identity
   */
  public publish(grantee: iam.IGrantable): iam.Grant {
    const actions = ["sns:Publish"];
    const result = (this.policyResource ? iam.Grant.addToPrincipalOrResource({
      actions: actions,
      grantee: grantee,
      resourceArns: [sns.CfnTopic.arnForTopic(this.resource)],
      resource: this.policyResource
    }) : iam.Grant.addToPrincipal({
      actions: actions,
      grantee: grantee,
      resourceArns: [sns.CfnTopic.arnForTopic(this.resource)]
    }));
    this.encryptedResource?.grantOnKey(grantee, "kms:Decrypt", "kms:GenerateDataKey*");
    return result;
  }

  /**
   * Grant topic subscribing permissions to the given identity
   */
  public subscribe(grantee: iam.IGrantable): iam.Grant {
    const actions = ["sns:Subscribe"];
    const result = (this.policyResource ? iam.Grant.addToPrincipalOrResource({
      actions: actions,
      grantee: grantee,
      resourceArns: [sns.CfnTopic.arnForTopic(this.resource)],
      resource: this.policyResource
    }) : iam.Grant.addToPrincipal({
      actions: actions,
      grantee: grantee,
      resourceArns: [sns.CfnTopic.arnForTopic(this.resource)]
    }));
    return result;
  }
}

なお全てのリソースで上記の Grants クラス が自動生成されているわけではありません。執筆時点は以下のリソースにおいて個別に Grants クラスが実装されています。

なぜ Grants クラスを導入したのか(私見)

PR #35782 にはリファクタ理由までは明記されていないため、私見になります。

まず AWS CDK 自体の保守性向上が考えられます。従来は L2 Construct の class に直接 grant メソッドが実装されていましたが、新規の方式では Grants クラス(自動生成含む)に分割されます。これによりどのモジュールであっても Grants クラスに実装が集約されるため、改修時にも修正箇所の把握が容易になります。

また、この仕組みにより L1 Construct でも grants が利用可能になりました。将来的には L2 Construct が存在しないリソースであっても、自動生成された Grants クラスを元に grants が利用できるようになることが期待されます。RFC 655: Enhanced L1s のように L1 Construct を強化する方向性があるため、その流れに沿った変更であることが推測されます。

終わりに

今後 L2 Construct で権限付与を行う際は Grants クラスの使用を意識していただければと思います。

Reference

https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md#grants
https://github.com/aws/aws-cdk/pull/35782

アマゾン ウェブ サービス ジャパン (有志)

Discussion