実践!AWS CDK #19 Secrets Manager
はじめに
今回は Secrets Manager にシークレットを保存します。
RDS でのデータベース作成時に指定する マスターパスワード
を自動生成する目的です。パスワードはソースコードに組み込みたくありませんからね!
前回の記事はこちら。
AWS 構成図
未来の構成図です。
今回は一番下にいる RDS のための回です。
設計
Aurora DB クラスターの マスターユーザー名
と マスターパスワード
をシークレットとして保存します。
プロパティは以下の通り。
シークレット名 |
---|
devio-stg-secrets-rds-cluster |
以下 2 つのキーを作成します。(パスワードは自動生成)
シークレットキー | 値 |
---|---|
MasterUsername | admin |
MasterUserPassword | 自動生成 |
MasterUserPassword
項目 | 値 |
---|---|
パスワードの長さ | 16 文字 |
除外文字 | "@/\' |
Aurora DB クラスターのマスターパスワードの仕様は以下の通りです。
- Aurora MySQL の場合、パスワードには 8~41 個の印刷可能な ASCII 文字を使用する必要があります。
- Aurora PostgreSQL の場合は、8~128 個の印刷可能な ASCII 文字を使用する必要があります。
- /、"、@、またはスペースは使用できません。
公式のサンプルを参考にしたのと、コンソール上(上記の画面キャプチャ)で 単一引用符(')はダメ と書かれていたので "
, @
, /
, \
, '
の記号 5 つを含めないように設定します。
シークレットのローテーションは行いません。
実装
Secrets Manager に関する処理を行うクラスはこちら。
import * as cdk from '@aws-cdk/core'; import { CfnSecret } from '@aws-cdk/aws-secretsmanager'; import { Resource } from './abstract/resource'; export const OSecretKey = { MasterUsername: 'MasterUsername', MasterUserPassword: 'MasterUserPassword' } as const; type SecretKey = typeof OSecretKey[keyof typeof OSecretKey]; interface ResourceInfo { readonly id: string; readonly description: string; readonly generateSecretString: CfnSecret.GenerateSecretStringProperty; readonly resourceName: string; readonly assign: (secret: CfnSecret) => void; } export class SecretsManager extends Resource { public rdsCluster: CfnSecret; private static readonly rdsClusterMasterUsername = 'admin'; private readonly resources: ResourceInfo[] = [{ id: 'SecretRdsCluster', description: 'for RDS cluster', generateSecretString: { excludeCharacters: '"@/\\\'', generateStringKey: OSecretKey.MasterUserPassword, passwordLength: 16, secretStringTemplate: `{"${OSecretKey.MasterUsername}": "${SecretsManager.rdsClusterMasterUsername}"}` }, resourceName: 'secrets-rds-cluster', assign: secret => this.rdsCluster = secret }]; constructor() { super(); }; createResources(scope: cdk.Construct) { for (const resourceInfo of this.resources) { const secret = this.createSecret(scope, resourceInfo); resourceInfo.assign(secret); } } public static getDynamicReference(secret: CfnSecret, secretKey: SecretKey): string { return `{{resolve:secretsmanager:${secret.ref}:SecretString:${secretKey}}}`; } private createSecret(scope: cdk.Construct, resourceInfo: ResourceInfo): CfnSecret { const secret = new CfnSecret(scope, resourceInfo.id, { description: resourceInfo.description, generateSecretString: resourceInfo.generateSecretString, name: this.createResourceName(scope, resourceInfo.resourceName) }); return secret; } }
前回同様、まずは Secrets Manager に関する Construct を利用するために @aws-cdk/aws-secretsmanager
をインストールします。
$ npm install @aws-cdk/aws-secretsmanager
今回作成するリソースは 1 つなのですが、このクラスに関してはベタ書きするのが気持ち悪かったので従来のように ResourceInfo
インタフェースを用意し、リソース生成処理をループで回しています。(ループは 1 回で終わるし、今後リソースが増える予定もないのですが)
除外文字に関しては以下の部分で設定しています。(\
はエスケープ文字)
excludeCharacters: '"@/\\\'',
今回のキモはこの部分です。
export const OSecretKey = { MasterUsername: 'MasterUsername', MasterUserPassword: 'MasterUserPassword' } as const; type SecretKey = typeof OSecretKey[keyof typeof OSecretKey];
キー値を列挙するためにこのようなオブジェクト(OSecretKey
)と型(SecretKey
)を宣言しました。
どうやら TypeScript は Enum
が推奨されていないらしいですね。
公式ドキュメントに従って Enum のような処理を行うためにこのコードを書きました。
これで OSecretKey.MasterUsername
や OSecretKey.MasterUserPassword
という記述で各値の文字列にアクセスでき、SecretKey
型の値はここでは 'MasterUsername'
または 'MasterUserPassword'
に縛ることができます。
かなり複雑ですが、一度理解してしまえばその後は深く考えずに利用できそうです。(数学の公式に似てます)
なお、type SecretKey = typeof OSecretKey[keyof typeof OSecretKey];
の部分に関しては以下の記事がとてもわかりやすかったです。ありがとうございました。
次のコードは定数化しているため長くなってしまっているのですが・・・
secretStringTemplate: `{"${OSecretKey.MasterUsername}": "${SecretsManager.rdsClusterMasterUsername}"}`
やっていることはこうです。
SecretStringTemplate: '{"MasterUsername": "admin"}'
こちらは JSON.stringify()
メソッドを使って次のように書くこともできます。
secretStringTemplate: JSON.stringify({ MasterUsername: 'admin' })
保存されたシークレットを取得する際は以下の static メソッドを利用します。
public static getDynamicReference(secret: CfnSecret, secretKey: SecretKey): string { return `{{resolve:secretsmanager:${secret.ref}:SecretString:${secretKey}}}`; }
このメソッドは CfnSecret のインスタンスとシークレットのキーをパラメーターに受け取り、動的な参照(Dynamic References)を作成し、返却します。
上記のコードでは ${secret.ref}
部分にはシークレットの ARN が、${secretKey}
部分には 'MasterUsername'
または 'MasterUserPassword'
が代入されます。
L2 の Construct である Secret を利用すれば fromSecretAttributes()
と secretValueFromJson()
というメソッドで値を取得できるようなのですが、fromSecretAttributes() は使用時に id
を指定しなければならず、かつプロジェクト内で重複が許されないという仕様だったため使うのをやめました。
私はいにしえの方法で進めていきたいと思います。
メインのプログラムはこちら。
ハイライト部分を追記しました。
import * as cdk from '@aws-cdk/core'; import { Vpc } from './resource/vpc'; import { Subnet } from './resource/subnet'; import { InternetGateway } from './resource/internetGateway'; import { ElasticIp } from './resource/elasticIp'; import { NatGateway } from './resource/natGateway'; import { RouteTable } from './resource/routeTable'; import { NetworkAcl } from './resource/networkAcl'; import { IamRole } from './resource/iamRole'; import { SecurityGroup } from './resource/securityGroup'; import { Ec2 } from './resource/ec2'; import { Alb } from './resource/alb'; import { SecretsManager } from './resource/secretsManager'; export class DevioStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // VPC const vpc = new Vpc(); vpc.createResources(this); ~ 省略 ~ // Secrets Manager const secretsManager = new SecretsManager(); secretsManager.createResources(this); } }
シークレットの値取得はこんな感じで。
~ 省略 ~ import { SecretsManager, OSecretKey } from './resource/secretsManager'; export class DevioStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); ~ 省略 ~ // Secrets Manager const secretsManager = new SecretsManager(); secretsManager.createResources(this); const masterUsername = SecretsManager.getDynamicReference(secretsManager.rdsCluster, OSecretKey.MasterUsername); const masterUserPassword = SecretsManager.getDynamicReference(secretsManager.rdsCluster, OSecretKey.MasterUserPassword); } }
テスト
テストコードはこちら。
import { expect, countResources, haveResource } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import * as Devio from '../../lib/devio-stack'; test('SecretsManager', () => { const app = new cdk.App(); const stack = new Devio.DevioStack(app, 'DevioStack'); expect(stack).to(countResources('AWS::SecretsManager::Secret', 1)); expect(stack).to(haveResource('AWS::SecretsManager::Secret', { Description: 'for RDS cluster', GenerateSecretString: { ExcludeCharacters: '"@/\\\'', GenerateStringKey: 'MasterUserPassword', PasswordLength: 16, SecretStringTemplate: '{"MasterUsername": "admin"}' }, Name: 'undefined-undefined-secrets-rds-cluster' })); });
以下を確認しています。
- シークレットのリソースが 1 つあること
- リソースのプロパティが正しいこと
確認
マネジメントコンソール上でリソースを確認してみましょう。
シークレットが作成されています。
キーと値も OK。
パスワードは 16 文字でランダムに作成されました。
値取得の確認
保存したシークレットから正しく値が取得できることも確認しようと思います。
動作確認用のセキュリティグループを 2 つ作成し、説明
の欄に値を設定します。
// Secrets Manager const secretsManager = new SecretsManager(); secretsManager.createResources(this); new CfnSecurityGroup(this, 'test-username', { groupDescription: SecretsManager.getDynamicReference(secretsManager.rdsCluster, OSecretKey.MasterUsername) }); new CfnSecurityGroup(this, 'test-password', { groupDescription: SecretsManager.getDynamicReference(secretsManager.rdsCluster, OSecretKey.MasterUserPassword) });
正しい値が設定されていることが確認できました。(が、セキュリティグループの 説明
は許容されている文字が RDS のパスワードより少なく、何度か作成に失敗しました)
ご注意
はじめはセキュリティグループの 説明
ではなく VPC の タグ
で確認しようと試みたのですが、リソースのタグでは確認不可 でした。(2 時間くらいハマりました)
// Secrets Manager const secretsManager = new SecretsManager(); secretsManager.createResources(this); new CfnVPC(this, 'test-vpc', { cidrBlock: '10.0.0.0/16', // ダメ。ゼッタイ。 tags: [{ key: 'MasterUsername', value: SecretsManager.getDynamicReference(secretsManager.rdsCluster, OSecretKey.MasterUsername) }] });
公式情報は見つけられなかったのですが、CFn や CDK で Secrets Manager の値取得を試したい場合はセキュリティグループなどのプロパティに値を設定して確認するようにしましょう。
少なくともリソースの タグ
を利用するのはやめましょう。
CloudFormation 版
今回のコードを CFn で書くと以下のようになります。
SecretRdsCluster: Type: AWS::SecretsManager::Secret Properties: Description: for RDS cluster GenerateSecretString: ExcludeCharacters: "\"@/\\'" GenerateStringKey: MasterUserPassword PasswordLength: 16 SecretStringTemplate: '{"MasterUsername": "admin"}' Name: devio-stg-secrets-rds-cluster
GitHub
今回のソースコードは コチラ です。
おわりに
Secrets Manager を利用することで「RDS の DB 作成時に必要なパスワード」を自動生成でき、それを AWS Cloud に保存して必要な時に取得するということが可能です。
DB 作成時の パスワードどこに置くか問題 は度々発生すると思うので、一つの方法として参考にしてください。
次回はいよいよ最後のリソース RDS まわりを構築します!が、こんな感じで少し刻んでいこうと思います。
サブネットグループ
パラメータグループ
クラスター
インスタンス
それでは。
リンク
- class CfnSecret (construct) | AWS CDK API Reference
- class Secret (construct) | AWS CDK API Reference
- AWS::SecretsManager::Secret | AWS CloudFormation User Guide
- AWS Secrets Manager とは | AWS Secrets Manager User Guide
- AWS CloudFormation でのシークレット作成の自動化 | AWS Secrets Manager User Guide
- @aws-cdk/aws-secretsmanager module | AWS CDK API Reference
- Get a value from AWS Secrets Manager | AWS CDK Developer Guide
- 動的な参照を使用してテンプレート値を指定する | AWS CloudFormation User Guide
- Amazon Aurora DB クラスターの作成 | Amazon Aurora User Guide
- Enums | TypeScript: Documentation
- [Typescript] typeof "object value" [keyof typeof "object value"] の動作を丁寧に解説してみる | Qiita
- Reading a secret using CloudFormation | Stack Overflow