【AWS CDK】「CDKを用いたKMSマルチリージョンキー実装」

## はじめに
AWS KMSには、複数のリージョンで同じキーを利用できるマルチリージョンキーという機能があります。
[KMSマルチリージョンキー](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/multi-region-keys-overview.html)

このマルチリージョンキーを利用する際は、ソースとなるマルチリージョンキーをDeployした後に、利用するリージョンにてレプリカキーをDeployする必要があります。
CDKを用いて実装する場合、Stackコードはリージョンに紐つくため、1Stackコードでは実装することは出来ず、ソースとなるマルチリージョンキーをDeployするStack、利用するリージョンにてレプリカキーをDeployするStackの2つに分割する必要があります。

この分割を1APP 2Stack構成で実施した際にちょっとした問題が発生したので、ご紹介させて頂きます。

問題の詳細は、本記事に記載しますが、結論としては、「リージョン毎にAPPを分割する」ことをお勧めします。
1APP複数Stackの注意点は以下記事にも紹介しております。
[1APP複数Stack実装における注意事項](https://qiita.com/akwayne/items/eb0c146fab5fb6ac8906)

今回構成したKMSマルチリージョンキー実装用の構成が上記別記事に記載した注意事項にマッチしているか確認している時に発覚した事象となり、上記記事でまとめた事象とは別の事象となります。

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。

## 1APP 2Stack構成でのマルチリージョンキー実装
本サンプルでは、マルチリージョンキーの実装に関して、以下シナリオで1APP複数Stack構成とります。

**シナリオ**
ソースとなるマルチリージョンキーを東京Regionに、レプリカキーを大阪RegionにDeployするシナリオとなります。

このサンプルは以下5つのセクションに分割されます。
- APPコード
- マルチリージョンキー用Stackコード
- マルチリージョンキー用Constructsコード
- レプリカキー用Stackコード
- レプリカキー用Constructsコード

マルチリージョンキーを実装する際のポイントはAPPコードになります。

それぞれのセクションに分けてご紹介させて頂きます。

### APPコード
sourceKmsがソースとなるマルチリージョンキーを東京RegionにDeployします。
multiKmsは、sourceKmsでDeployされるマルチリージョンキーのレプリカキーを大阪RegionにDeployします。

~~~typescript:sample-multi-kms.ts
// マルチリージョンキー用Stack
const sourceKms = new SourceKmsStack(app, 'SourceKms', {
    env: {
    region: "ap-northeast-1",
    account: "xxxxx"
  },
  crossRegionReferences: true,
});

// レプリカキー用Stack
const multiKms =  new MultiKmsStack(app, 'MultiKms', {
    env: {
    region: "ap-northeast-3",
    account: "xxxx"
  },
  crossRegionReferences: true,
  sourceKms: sourceKms.myKmsSource,
});
~~~
APPコードを構成するポイントはStack Constructsに引き渡すprops(引数のセット)となり、3つあります。

**env**
env propsにてDeployする先のRegionを指定することが出来ます。
そのため、sourceKmsではap-northeast-1(東京)をmultiKmsでは、ap-northeast-3(大阪)を指定しています。

**crossRegionReferences**
※本記事投稿日現在GAとは言えない状態のpropsになります。
このpropsをtrueに指定することにより、リージョン間スタック参照が実現されます。
実体としては、CloudFormationのカスタムリソースにて、参照先のRegionにSSM Prameter Storeが構成され、参照値が埋められます。
今回のシナリオでは、大阪RegionにSSM Parameter Storeが構成され、参照値が埋められます。
これが、リージョン間でオブジェクトを引き渡す際のポイントになります。

**sourceKms: sourceKms.myKmsSource**
MultiKmsStackにsourceKmsとしてSourceKmsStackクラスオブジェクトのmyKmsSourceを渡しています。
これが、Stack間でのオブジェクト引き渡しのポイントになります。

### マルチリージョンキー用Stackコード
ソースとなるマルチリージョンキーを構成するStackコードです。
~~~typescript:sample-sourcekms-stack.ts
export class SourceKmsStack extends cdk.Stack {
  public myKmsSource: kms.CfnKey
  
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    const kms= new SourceKms(this, "mySourceKms")
    this.myKmsSource = kms.mySourceKms
~~~
ポイントはAPPコード経由でオブジェクトを渡す部分に関連するものとなり、2つあります。

**public myKmsSource: kms.CfnKey**
myKmsSourceという名前でkms.CfnKey型のパブリックなプロパティを定義しています。
これによりSourceKmsStackクラスのオブジェクトにmyKmsSourceとしてアクセスすることができます。

**this.myKmsSource = kms.mySourceKms**
myKmsSourceプロパティに、kmsオブジェクトのmySourceKmsプロパティを代入しています。

### マルチリージョンキー用Constructsコード
ソースとなるマルチリージョンキーを構成するConstructsコードです。
~~~typescript:sample-sourcekms-constructs.ts
export class SourceKms extends Construct {
  public mySourceKms: kms.CfnKey
  constructor(scope: Construct, id: string) {
    super(scope, id);
    
  this.mySourceKms = new kms.CfnKey(this, 'mySourceKms', {
      description: 'sourcekey',
      enabled: true,
      keyUsage: 'ENCRYPT_DECRYPT',
      multiRegion: true,
      pendingWindowInDays: 7,
});
~~~
ポイントはAPPコード経由でオブジェクトを渡す部分に関連する箇所と選定するConstructsの2つあります。

**public mySourceKms: kms.CfnKey**
mySourceKmsという名前でkms.CfnKey型のパブリックなプロパティを定義しています。
これによりSourceKmsクラスのオブジェクトにmyKmsSourceとしてアクセスすることができます。

**this.mySourceKms = new kms.CfnKey(this, 'mySourceKms', {**
L1 CfnKey Constructsを使用してCMKを作成し、mySourceKmsプロパティに代入しています。
※マルチリージョンキーはL2 Key Constructでは、現状構成できません。そのため、L1 CfnKey Constructsを使用する必要があります。
[CDK Constructs](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/constructs.html)

### レプリカキー用Stackコード
sourceKmsでDeployされるマルチリージョンキーのレプリカキーを構成するStackコードです。
~~~typescript:sample-multikms-stack.ts
export interface MultiKmsStackProps extends StackProps {
  sourceKms: kms.CfnKey,
}
export class MultiKmsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: MultiKmsStackProps) {
    super(scope, id, props);
    new MultiKms(this, "mySourceKms", 
    {
    sourceKms: props.sourceKms
    }
    )
  }
}
~~~
ポイントはAPPコード経由でオブジェクトを受け取る部分に関連するものとなり、2つあります。

**sourceKms: kms.CfnKey**
kms.CfnKey型のsourceKmsをMultiKmsStackPropsインターフェースとして定義しています。MultiKmsStackクラスのpropsからの入力値として受け取る必要があります。

**sourceKms: props.sourceKms**
MultiKmsクラスにsourceKmsとしてAPPコード経由で受け取ったsourceKmsを渡します。

### レプリカキー用Constructsコード
sourceKmsでDeployされるマルチリージョンキーのレプリカキーを構成するConstructsコードです。
~~~typescript:sample-multikms-constructs.ts
export interface MultiKmsProps {
  sourceKms: kms.CfnKey
}
export class MultiKms extends Construct {
  constructor(scope: Construct, id: string, props: MultiKmsProps) {
    super(scope, id); 
    new kms.CfnReplicaKey(this, "multiKey", {
    keyPolicy: {
      "Id": "key-policy",
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "Enable IAM User Permissions",
          "Effect": "Allow",
          "Principal": {
            "AWS": "arn:aws:iam::xxxx:root"
          },
          "Action": "kms:*",
          "Resource": "*"
        }
        ]
    },
    primaryKeyArn: props.sourceKms.attrArn,
    description: "sample multi kms",
    pendingWindowInDays: 7
    });
~~~
ポイントはAPPコード経由でオブジェクトを受け取る部分に関連する箇所と選定するConstructsの3つあります。

**sourceKms: kms.CfnKey**
kms.CfnKey型のsourceKmsをMultiKmsStackPropsインターフェースとして定義しています。MultiKmsクラスのpropsからの入力値として受け取る必要があります。

**new kms.CfnReplicaKey(this, "multiKey", {*
L1 CfnReplicaKey Constructsを使用してレプリカキーを作成します。
※レプリカキーはL2 Key Constructでは、現状構成できません。そのため、L1 CfnReplicaKey Constructsを使用する必要があります。

**primaryKeyArn: props.sourceKms.attrArn**
CfnReplicaKeyクラスにprimaryKeyArnとしてAPPコード経由で受け取ったsourceKmsのattrArnを渡します。
※attrArnから渡されたマルチリージョンキーのARNにアクセスできます。

## 1APP 2Stack構成でのマルチリージョンキーのDeploy
上記コードでDeployします。
マルチリージョンキー用Stack
~~~console
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxx:stack/SourceKms/xxxxxx

~~~~

レプリカキー用Stack
~~~console
Stack ARN:
arn:aws:cloudformation:ap-northeast-3:xxxx:stack/MultiKms/xxxxxx
~~~~

整理すると以下の構成でDeployされています。
| Region | KMS種別 | 
|---|---|
| ap-northeast-1(東京)| マルチリージョンキー|
| ap-northeast-3(大阪) | 上記マルチリージョンキーのレプリカキー|


## 1APP 2Stack構成でのマルチリージョンキーで問題ないかの確認
上記1APP 2Stack構成で1APP複数Stackの懸念に該当しないか確認のために、レプリカキーのみ削除した際にDeployエラーが発生しないか確認します。
**APPコード**
~~~typescript:sample-multi-kms.ts
// マルチリージョンキー用Stack
const sourceKms = new SourceKmsStack(app, 'SourceKms', {
    env: {
    region: "ap-northeast-1",
    account: "xxxxx"
  },
//   crossRegionReferences: true,
});

// レプリカキー用Stack
const multiKms =  new MultiKmsStack(app, 'MultiKms', {
    env: {
    region: "ap-northeast-3",
    account: "xxxx"
  },
//  crossRegionReferences: true,
//  sourceKms: sourceKms.myKmsSource,
});
~~~
crossRegionReferences propsとマルチリージョンキーStackからのオブジェクト受け取りをコメントアウトしています。

**レプリカキー用Stackコード**
~~~typescript:sample-sourcekms-stack.ts
export class SourceKmsStack extends cdk.Stack {
  //public myKmsSource: kms.CfnKey
  
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    const kms= new SourceKms(this, "mySourceKms")
    //this.myKmsSource = kms.mySourceKms
~~~
オブジェクトの引き渡しに関連する部分をコメントアウトしています。

**マルチリージョンキー用Constructsコード**
~~~typescript:sample-sourcekms-constructs.ts
export class SourceKms extends Construct {
  //public mySourceKms: kms.CfnKey
  constructor(scope: Construct, id: string) {
    super(scope, id);
    
  //this.mySourceKms = new kms.CfnKey(this, 'mySourceKms', {
  const mySourceKms = new kms.CfnKey(this, 'mySourceKms', {
      description: 'sourcekey',
      enabled: true,
      keyUsage: 'ENCRYPT_DECRYPT',
      multiRegion: true,
      pendingWindowInDays: 7,
});
~~~
オブジェクトの引き渡しに関連する部分をコメントアウトし、そのコメントアウトに伴う変更をしています。

**レプリカキー用Stackコード**
~~~typescript:sample-multikms-stack.ts
//export interface MultiKmsStackProps extends StackProps {
//  sourceKms: kms.CfnKey,
//}
export class MultiKmsStack extends cdk.Stack {
  //constructor(scope: Construct, id: string, props: MultiKmsStackProps) {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    //super(scope, id, props);
    super(scope, id);
    new MultiKms(this, "mySourceKms", 
    //{
    //sourceKms: props.sourceKms
    //}
    )
  }
}
~~~
オブジェクトの受け取りに関連する部分をコメントアウトし、そのコメントアウトに伴う変更をしています。

 **レプリカキー用Constructsコード**
~~~typescript:sample-multikms-constructs.ts
// export interface MultiKmsProps {
//   sourceKms: kms.CfnKey
// }


export class MultiKms extends Construct {
  // constructor(scope: Construct, id: string, props: MultiKmsProps) {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    
    // new kms.CfnReplicaKey(this, "multiKey", {
    // keyPolicy: {
    //   "Id": "key-policy",
    //   "Version": "2012-10-17",
    //   "Statement": [
    //     {
    //       "Sid": "Enable IAM User Permissions",
    //       "Effect": "Allow",
    //       "Principal": {
    //         "AWS": "arn:aws:iam::656100244460:root"
    //       },
    //       "Action": "kms:*",
    //       "Resource": "*"
    //     }
    //     ]
    // },
    // primaryKeyArn: props.sourceKms.attrArn,
    // description: "sample multi kms",
    // pendingWindowInDays: 7
    // });
~~~
オブジェクトの受け取りに関連する部分、レプリカキーを生成する部分をコメントアウトし、そのコメントアウトに伴う変更をしています。

これらのコードで再Deployした結果は以下になります。
**レプリカキー用Stack**
~~~console
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxx:stack/MultiKms/xxxx
~~~~

Deployエラーは発生しませんでしたが、これではダメなんです。
この時点で違和感にお気づきでしょうか。。。

### 発生した事象
本来であれば、以下「①想定構成」になっていてほしいかったのですが、「②Deploy出来た構成」は以下のようになってしまいました。

**①想定構成**
| Region | Deployされるリソース | 
|---|---|
| ap-northeast-1(東京)| マルチリージョンキー|
| ap-northeast-3(大阪) | レプリカキー用Stackに構成されるその他リソース|

**②Deploy出来た構成**
| Region | Deployされるリソース | 
|---|---|
| ap-northeast-1(東京)| マルチリージョンキー、レプリカキー用Stackに構成されるその他リソース|
| ap-northeast-3(大阪) | 上記マルチリージョンキーのレプリカキー、レプリカキー用Stackに構成されるその他リソース|

変更セットを実行した際の出力をご確認ください。
そうなんです。ap-northeast-3をStackコードで指定しているにも関わらす、ap-northeast-1に変更セットがあたってしまっています。

原因は確認中となり、進展があれば更新させて頂きますが、現状では、「crossRegionReferences」もGAしていないので、「リージョン毎にAPPを分割する」方が安全だと思われます。

## まとめ
KMSマルチリージョンキーを構成する際は、1APP 2Stack構成を組むことは出来ますが、レプリカキーがいらなくなった等の変更を加えると、レプリカキーをDeployするRegionへの変更が無視されます。
そのため、KMSマルチリージョンキーを構成する場合は、「リージョン毎にAPPを分割する」方が現状では安全と思われます。
KMSマルチリージョンキーの構成をご検討される際は、参考にして頂けると幸いです。

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。