2
1

【AWS CDK】「CDKを用いたKMSマルチリージョンキー実装 ~APP分割での半自動化編~」

Last updated at Posted at 2024-03-27

はじめに

AWS KMSには、複数のリージョンで同じキーを利用できるマルチリージョンキーという機能があります。
KMSマルチリージョンキー

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

この分割を1APP 2Stack構成では現状実現できません。
なぜ実現できないのかは、以下のリンクの記事にて紹介させて頂いておりますので、合わせてご確認ください。
CDKを用いたKMSマルチリージョンキー実装

この問題を回避する場合、APPを分割する必要があります。
その際、ソースとなるマルチリージョンキーのARNをレプリカキーをDeployするAPPに渡す部分がちょっとした課題です。
ソースとなるマルチリージョンキーDeploy後にARNを取得して埋め込んであげてもいいんですが、少しでも楽にするための半自動化構成を組みましたので、ご紹介させて頂きます。
この半自動化構成はStack props(引数のセット)であるcrossRegionReferencesを利用した際の挙動から着想を得ています。

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

APP構成の構造概要

今回の構成では以下2つのAPPに分けて実装しており、Deploy順序に依存関係があります。

Deploy順序 APP名 APP種別 構成要素
1 sample-source-kms ソースとなるリージョンにマルチリージョンキーをDeployする マルチリージョンキー, SSM Parameter Store
2 sample-replica-kms レプリカキーをDeployする source-kmsでDeployされるマルチリージョンキーに紐つくレプリカキー, SDKを用いてSSM Parameter Storeからvalueを取得

それぞれのAPPに分けてご紹介させて頂きます。

sample-source-kms

コードとしては普通です。
強いてこのAPPのポイントを上げるなら、SSM Parameter Storeに登録する箇所のみです。
APPコード

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

Stackコード

sample-source-kms-stack.ts
export class SourceKmsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);
    const kms= new SourceKms(this, "mySourceKms")
  }

Constructsコード

sample-source-kms-constructs.ts
export class SourceKms extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    // マルチリージョンキー
    const mySourceKms = new kms.CfnKey(this, 'mySourceKms', {
      description: 'sourcekey',
      enabled: true,
      keyUsage: 'ENCRYPT_DECRYPT',
      multiRegion: true,
      pendingWindowInDays: 7,
      });
    // SSM Parameter Store
    new ssm.StringParameter(this, "KMSARN",{
      parameterName: "multi-kms-ARN",
      stringValue: mySourceKms.attrArn
    })
  }
}

new ssm.StringParameter(this, "KMSARN",{
このsample-source-kms APPのポイントになります。
上段で構成されたマルチリージョンキーのARNをDeploy先のSSM Parameter Storeに「multi-kms-ARN」という名前で格納しています。
※CDKの場合、Deploy先RegionはStackコードでしているので、マルチリージョンキーのDeploy先と同じRegionにSSM Parameter StoreもDeployします。
この部分が、crossRegionReferencesを用いた場合の挙動とは違います。
crossRegionReferencesを用いた場合、参照先のRegionにDeployされます。

このSSM Parameter Storeの値を後段のsample-replica-kmsで取得し、自動で埋め込みます。
つまり、他APPに値を連携するためにSSM Parameter Storeを経由する構造が、今回の構成のポイントになります。

sample-replica-kms

このAPPでは、パラメータの外だしを活用しています。
※利用しているパラメータの外だし手法に関しては、以下のリンクの記事をご参照ください。
【AWS CDK】「CDKにおけるパラメータの外だし方法 ~自前TypeScript実装編~」
パラメータを外に出すことにより、簡単にパラメータ置換を出来るようにしています。

このパラメータファイルに対して、APPコードでAWS SDK for JavaScriptを利用して、SSM Parameter Storeから「multi-kms-ARN」のvalueを取得し、更新しています。
そのうえで、Constructsコード内で必要なソースとなるマルチリージョンキーのARNをパラメータファイルから呼び込んでいます。

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

APPコード

今回の構成では、このsample-replica-kms APPコードが大きなポイントとなり、以下3つのセクションで構成されます。

  • パラメータファイルを置換する処理
  • パラメータファイル外出しのための処理
  • レプリカ―を生成するStackをDeployするための処理

「パラメータファイルを置換する処理」に関して、要素を分けて紹介させて頂きます。

※「パラメータファイル外出しのための処理」以下のリンクの記事をご参照ください。
【AWS CDK】「CDKにおけるパラメータの外だし方法 ~自前TypeScript実装編~」

sample-replica-kms.ts
// パラメータファイルを置換する処理

//SDKを使うため等の準備
import * as AWS from 'aws-sdk';
import * as fs from 'fs';
AWS.config.update({ region: 'ap-northeast-1' });
const ssm = new AWS.SSM();

// SSM Parameter Storeから値を取得する関数
async function getParameterValue(): Promise<string> {
  const params = {
    Name: 'multi-kms-ARN'
  };
  try {
    const response = await ssm.getParameter(params).promise();
    const parameterValue = response.Parameter?.Value;
    if (parameterValue === undefined) {
      throw new Error('Parameter value is undefined');
    }
    return parameterValue;
  } catch (error) {
    console.error('Error retrieving parameter value:', error);
    throw error;
  }
}

// ファイルの読み書きを行う関数
function updateFileWithValue(value: string) {
  try {
    const filePath = './config/Dev.ts';
    let fileContent = fs.readFileSync(filePath, 'utf8');
    const source = "multi-kms-ARN"
    fileContent = fileContent.replace(source, value);
    fs.writeFileSync(filePath, fileContent);
    console.log('File content updated successfully.');
  } catch (error) {
    console.error('Error updating file content:', error);
    throw error;
  }
}

// メイン処理
(async function main() {
  try {
    const parameterValue = await getParameterValue();
    updateFileWithValue(parameterValue);
  } catch (error) {
    console.error('An error occurred:', error);
  }
})();

//パラメータファイル外だしのための処理
const envName = process.env.TS_ENV;
if (envName == null) {
  console.error(
    "Error: 環境変数'TS_ENV'にパラメータファイル名となる環境名を設定してください。\n \
     例: export TS_ENV=Dev"
  );
  process.exit(1);
}

//レプリカ―を生成するStackをDeployするための処理
const app = new cdk.App();
const replicaKms =  new ReplicaKmsStack(app, 'ReplicaKms', {
    env: {
    region: "ap-northeast-3",
    account: "xxxx"
  },
  tsEnv: envName,
});

SDKを使うための準備

AWS SDK for JavaScriptや、ode.jsのfsモジュールをインポートします。

AWS.config.update({ region: 'ap-northeast-1' });
SDKのグローバル設定をしています。
今回の目的はsample-source-kms APPで格納されたSSM Parameter Storeの値を取得することです。
sample-source-kmsはap-northeast-1に対して実行されているため、SDKのグローバル設定はap-northeast-1としています。

SSM Parameter Storeから値を取得する関数

SSM Parameter Storeから値を非同期に取得します。
async function getParameterValue(): Promise {
Promiseを返す非同期の処理を行い、resolveかrejectを返します。

const response = await ssm.getParameter(params).promise();
ssm.getParameter(params).promise() を非同期に実行し、SSMパラメータストアから値を取得します。getParameterメソッドの戻り値であるPromiseを返し、response変数に代入します。

const parameterValue = response.Parameter?.Value;
responseからValueを取得し、parametervalueに格納します。
response.Parameterがnullもしくはundefinedの場合、parameterValueはundefinedになります。

ファイルの読み書きを行う関数

ファイルの内容を読み込み、特定のテキストを指定した値で置換し、上書き保存します。

function updateFileWithValue(value: string) {
この関数はstring型の引数を受け取ります。

let fileContent = fs.readFileSync(filePath, 'utf8');
指定されたPathのファイルをutf8形式でテキストとして読み、fileContentに格納します。

fileContent = fileContent.replace(source, value);
replace() Methodを用いて、「multi-kms-ARN」を置換対象として、受け取った引数と置換処理をし、fileContentに格納します。

fs.writeFileSync(filePath, fileContent);
fs.writeFileSyncメソッドを使用し、fileContentを指定したファイルパスに同期的に書き込み、ファイルを上書き保存します。

メイン処理

メイン処理です。各関数を呼び出し、SSM Parameter Storeからvalueを取得し、パラメータファイルを置換します。

(async function main() {
非同期関数を宣言しています。IIFE(Immediately Invoked Function Expression)として即座に実行されます。

Stackコード

sample-replica-kms-stack.ts
export interface ReplicaKmsStackProps extends cdk.StackProps {
  tsEnv: string;
}
export class ReplicaKmsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: ReplicaKmsStackProps) {
    super(scope, id, props);
    const config = require("../config/" + props.tsEnv);
    new ReplicaKms(this, "myReplicaKms", {
      sourceArn: config.kms.sourceArn
    }
    )

Constructsコード

sample-replica-kms-constructs.ts
export interface ReplicaKmsProps {
  sourceArn: string
}
export class ReplicaKms extends Construct {
  constructor(scope: Construct, id: string, props: ReplicaKmsProps) {
    super(scope, id);
    new kms.CfnReplicaKey(this, "replicaKey", {
    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.sourceArn,
    description: "sample replica kms",
    pendingWindowInDays: 7
    });

パラメータコード

Dev.ts
export const kms = {
  sourceArn: "multi-kms-ARN",
};

sample-replica-kms実行結果

cdk synthesizeを実行するとパラメータファイルが以下のようになります。

Dev.ts
export const kms = {
  sourceArn: "arn:aws:kms:ap-northeast-1:xxxx:key/mrk-xxxx",
};

無事SSM Parameter Storeから取得したvalueで置換出来ていることが分かります。

Constructsコードはこのパラメータファイルからパラメータを取得しているの、sample-source-kmsのマルチリージョンキーのレプリカキーがDeployされます。

まとめ

KMSマルチリージョンキーの課題に対して、AWS SDKを用いて、半ば力業で半自動化してみました。
個人的にcrossRegionReferencesの発想はすごく魅力に感じて、そこから着想を得た構成となっております。
作ってみた感想にはなりますが、この方式を利用することにより、CDKの世界観がまた一段と広がったような感覚です。
マルチリージョン系の機能には同じような課題があると思うので、こんな感じで色々作ってみて、今後公開してみようかな?と考えています。
マルチリージョンやAPP分割等のユースケースで値を引き渡したい時に活用できるかと思われますので、似たような課題に直面された際は、ご紹介させて頂いた内容を参考にして頂けると幸いです。
※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1