はじめに
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コード
// マルチリージョンキー用Stack
const sourceKms = new SourceKmsStack(app, 'SourceKms', {
env: {
region: "ap-northeast-1",
account: "xxxxx"
},
});
Stackコード
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コード
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実装編~」
// パラメータファイルを置換する処理
//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コード
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コード
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
});
パラメータコード
export const kms = {
sourceArn: "multi-kms-ARN",
};
sample-replica-kms実行結果
cdk synthesizeを実行するとパラメータファイルが以下のようになります。
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分割等のユースケースで値を引き渡したい時に活用できるかと思われますので、似たような課題に直面された際は、ご紹介させて頂いた内容を参考にして頂けると幸いです。
※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。