AWS CDKでAWS Systems Manager パラメータストア及びAWS Secrets Managerからパラメータを取り込む方法

AWS CDKでAWS Systems Manager パラメータストア及びAWS Secrets Managerからパラメータを取り込む方法をまとめました。
2020.03.10

はじめに

おはようございます、加藤です。AWS CDKを使っている際に、外部APIのURLやシークレットをハードコートせずに与えたい場合があります。 この様な要望に答える為に、CDKにはAWS Systems Manager パラメータストアやAWS Secrets Managerに保存された値を利用する仕組みがあります。 やや癖のある仕様なので、具体的にどの様に使用する事が適切か解説します。

  • Node.js: 12.14.1
  • TypeScript: 3.7.5
  • AWS CDK: 1.27.0

パラメータストア

パラメータストアから値をCDKに取り込む為のメソッドは3種類用意されています。なぜ3種類あるかと言うと、取得するタイミング及びタイプごとにメソッドが用意されているからです。 また、メソッドごとにCloudFormationのどの機能を使って実現しているかが異なります。実現する為の機能が違うのでメソッドごとに制約も異なります。

メソッド名 取得するタイミング タイプ 実現する為のCFnの機能
valueFromLookup テンプレート生成時 String テンプレートに転記
valueForStringParameter デプロイ時 String SSM パラメータタイプ
valueForSecureStringParameter デプロイ時 SecureString ssm-secureの動的参照

valueFromLookup

このメソッドを使うと生成するテンプレートにパラメータストアに格納されていたStringを転記します。テンプレートに転記する為、SecureStringに対して使用する事ができません。SecureStringに対して使用できてしまうと、CloudFormationに対する読み取り権限があれば誰でもテンプレートを閲覧する事で内容を確認できてしまいます。Stringに対してパラメータストアの階層構造などで権限管理している場合、その管理から外れて閲覧できるユーザーが発生しうるので注意が必要です。 パラメータのバージョンはlatestに強制されます。 実は、SecureStringに対して使用する事は出来ますが、暗号化されたままの文字列で取り出されるので使い道はありません。

import { Construct, RemovalPolicy, Stack, StackProps, Tag } from '@aws-cdk/core';
import { Bucket } from '@aws-cdk/aws-s3';
import { StringParameter } from '@aws-cdk/aws-ssm';

export class CdkGetValueStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

new Bucket(this, 'Bucket', {removalPolicy: RemovalPolicy.DESTROY});

const plain = StringParameter.valueFromLookup(this, '/prj/env/plain');
const secure = StringParameter.valueFromLookup(this, '/prj/env/secure');

Tag.add(this, 'plain', plain, {includeResourceTypes: ['AWS::S3::Bucket']});
Tag.add(this, 'secure', secure, {includeResourceTypes: ['AWS::S3::Bucket']});
}
}
Resources:
Bucket83908E77:
Type: AWS::S3::Bucket
Properties:
Tags:
- Key: plain
Value: Plaintext
- Key: secure
Value: AQICAHjGxLVKJmKpVvWvXVpkAL4oQRmwTG0n2vcsGfl/k9eW+gErdD0rqL4FZyySRLTgD5VJAAAAaDBmBgkqhkiG9w0BBwagWTBXAgEAMFIGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMsKXPrjVWaa9EABdaAgEQgCVtebX5csafsTWvHAOZlDxWJ6iV5dSEE193sxGH4TOF2lwE1Em3

valueForStringParameter

このメソッドを使うとSSM パラメータタイプAWS::SSM::Parameter::Valueを使用しパラメータストアに格納されたStringを任意の箇所で使用する事ができます。 今回は省略していますが、第3引数でパラメータのバージョンを指定する事が可能です。指定しない場合はlatestを参照します。

import { Construct, RemovalPolicy, Stack, StackProps, Tag } from '@aws-cdk/core';
import { Bucket } from '@aws-cdk/aws-s3';
import { StringParameter } from '@aws-cdk/aws-ssm';

export class CdkGetValueStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

new Bucket(this, 'Bucket', {removalPolicy: RemovalPolicy.DESTROY});

const plain = StringParameter.valueForStringParameter(this, '/prj/env/plain');

Tag.add(this, 'plain', plain, {includeResourceTypes: ['AWS::S3::Bucket']});
}
}
Parameters:
SsmParameterValueprjenvplainC96584B6F00A464EAD1953AFF4B05118Parameter:
Type: AWS::SSM::Parameter::Value
Default: /prj/env/plain
Resources:
Bucket83908E77:
Type: AWS::S3::Bucket
Properties:
Tags:
- Key: plain
Value:
Ref: SsmParameterValueprjenvplainC96584B6F00A464EAD1953AFF4B05118Parameter

valueForSecureStringParameter

このメソッドを使うとssm-secureの動的参照を使用しパラメータストアに格納されたSecureStringを下記の特定の箇所で使用する事ができます。 valueForStringParameterと異なりパラメータのバージョンの指定が必須でlatestは指定できません。 使用可能な箇所が限られており、シークレットだけど下記には該当しないという事象が容易に発生しそうです。個人的にはこれの使用は避けたいです。

リソース プロパティタイプ プロパティ
AWS::DirectoryService::MicrosoftAD Password
AWS::DirectoryService::SimpleAD Password
AWS::ElastiCache::ReplicationGroup AuthToken
AWS::IAM::User LoginProfile Password
AWS::KinesisFirehose::DeliveryStream RedshiftDestinationConfiguration Password
AWS::OpsWorks::App 送信元 Password|AWS::OpsWorks::Stack|CustomCookbooksSource|Password
AWS::OpsWorks::Stack RdsDbInstances DbPassword
AWS::RDS::DBCluster MasterUserPassword
AWS::RDS::DBInstance MasterUserPassword
AWS::Redshift::Cluster MasterUserPassword

引用元: https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/dynamic-references.html#template-parameters-dynamic-patterns-resources

import { Construct, SecretValue, Stack, StackProps } from '@aws-cdk/core';
import { User } from '@aws-cdk/aws-iam';
import { StringParameter } from '@aws-cdk/aws-ssm';

export class CdkGetValueStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const secure = StringParameter.valueForSecureStringParameter(this, '/prj/env/secure', 1);
new User(this, 'User', {
password: new SecretValue(secure)
});
}
}
Resources:
User00B015A1:
Type: AWS::IAM::User
Properties:
LoginProfile:
Password: "{{resolve:ssm-secure:/prj/env/secure:1}}"

Secrets Manager

Secrets Managerから値をCDKに取り込む為のメソッドとして、fromSecretAttributesが用意されています。 このメソッドを使うとsecretsmanagerの動的参照を使用しシークレットを任意の箇所で使用する事ができます。 Secrets Managerは、文字列またはJSON文字列を格納できます。このメソッドは取り出す場合、文字列またはJSON文字列のキー指定で取り出す事ができます。

import { Construct, RemovalPolicy, Stack, StackProps, Tag } from '@aws-cdk/core';
import { Bucket } from '@aws-cdk/aws-s3';
import { Secret } from '@aws-cdk/aws-secretsmanager';

export class CdkGetValueStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const secret = Secret.fromSecretAttributes(this, 'Secret', {
secretArn: `arn:aws:secretsmanager:${this.region}:${this.account}:secret:secretmanager-nsUGO2`
});
const secretJson = Secret.fromSecretAttributes(this, 'SecretJson', {
secretArn: `arn:aws:secretsmanager:${this.region}:${this.account}:secret:secretmanagerJson-dQYcXL`
});

new Bucket(this, 'Buckets', {
removalPolicy: RemovalPolicy.DESTROY,
bucketName: `${secret.secretValue.toString()}-${this.account}`
});
new Bucket(this, 'Bucket', {
removalPolicy: RemovalPolicy.DESTROY,
bucketName: `${secretJson.secretValueFromJson('key').toString()}-${this.account}`
});
}
}
Resources:
BucketsF2AD3CC3:
Type: AWS::S3::Bucket
Properties:
BucketName: "{{resolve:secretsmanager:arn:aws:secretsmanager:${region}:${account}:secret:secretmanager-nsUGO2:SecretString:::}}-${account}"

Bucket83908E77:
Type: AWS::S3::Bucket
Properties:
BucketName: "{{resolve:secretsmanager:arn:aws:secretsmanager:${region}:${account}:secret:secretmanagerJson-dQYcXL:SecretString:key::}}-${account}"

まとめ

紹介してパラメータの取り込み方法を表にまとめました。

サービス名 メソッド名 取得するタイミング タイプ 実現する為のCFnの機能 利用先
SSM パラメータストア valueFromLookup テンプレート生成時 String テンプレートに転記 全て
SSM パラメータストア valueForStringParameter デプロイ時 String SSM パラメータタイプ 全て
SSM パラメータストア valueForSecureStringParameter デプロイ時 SecureString ssm-secureの動的参照 限定
Secrets Manager fromSecretAttributes デプロイ時 SecureString, SecureStringJson secretsmanagerの動的参照 全て

Secureが付いていない暗号化されていないから安全ではないという事ではありません。 シークレットをStringとしてパラメータに格納している場合でも、IAMが適切に管理されていれば開発者はシークレットにアクセスする事ができません。

参考: AWSボリュームの暗号化の必要性について - Qiita

考察

個人的な考察です。

ローテーションやJSON形式など、Secrets Managerの方がリッチに使えるので、パラメータストアと比べるとSecrets Managerを積極的に利用したい。

Secrets Managerはアクセスのコストがパラメータストアと比較して高いのがネックですが、今回の様なデプロイ時のみのアクセスであれば、まず問題無い。

パラメータストアの階層構造を使いたい場合はvalueForStringParameterを使い、RDSのシークレットなどもStringで格納してIAMで制御すれば良い。

パラメータの格納は、READMEに手順を作り、初回構築時にAWSマネジメントコンソールかAWS CLIから行えば良さそう。 個人的感想をまとめます。

  • Secrets Managerに全てのパラメータを格納してfromSecretAttributesで取り出す
  • パラメータストアを使いたい場合は全てStringで格納しvalueForStringParameterで取り出す
  • 可能なら方法は併用せず1つだけ使う

あとがき

AWS BlackBelt CDKが面白かったし、CDK MeetUpも良かったのでブログを書きました。 最近、コードが書けるようになったので、CDK使っていて、足りなかった所をコントリビュートしているけど、レビューが当然英語で口語なので、Google翻訳がいい感じに訳してくれず辛いです。。。

参考元