【AWS CDK】Parameter Storeからのパラメータ参照やリソース定義までの流れとTips

# この記事について
本記事では、AWS CDKを使用してパラメータストアからパラメータを取得し、それをもとにAWSリソースを定義する方法について紹介します。パラメータストアに値がある場合とない場合、それぞれどのように取得するかについても考えてみました。また、パラメータのキャッシュと更新の仕組み、パラメータ取得時に発生する可能性のあるエラーの解決方法についても触れています。

# 動作環境
- Node.js 20.15.0
- TypeScript 5.5.2
- AWS CDK v2(2.156.0)

# パタメータの取得方法
パラメータストアから値を取得する方法は以下の3種類があります。
- valueFromLookup
- valueForStringParameter
- SecretValue.ssmSecure

:::note warn
2024/09/23時点でvalueForSecureStringParameterは非推奨になっていて、代わりにSecretValue.ssmSecureを使うことが推奨されています。
:::

### valueFromLookup
- CloudFormationテンプレートの合成時に指定したパラメータをパラメータストアから取得し、その値をスタックの一部として埋め込みます
- 合成時にパラメータストアに値が存在している必要があり、値が存在しない場合はエラーが発生します
- プレーンな文字列のみ取得可能で、Secure Stringはサポートされていません
- 取得した値は`cdk.context.json`にキャッシュされます
    - キャッシュがある場合、手動でパラメータ値を変更してもキャッシュされた値が使用されます
    - キャッシュがない場合は、パラメータストアから値を取得します
- パラメータのバージョンは指定できず、常に最新バージョンの値を取得します

試しにVPCを作成してVPCのIDをパラメータストアに格納した状態で、以下のコードを実行してみます。

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ssm from 'aws-cdk-lib/aws-ssm';

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

    const vpcId = ssm.StringParameter.valueFromLookup(this, '/VpcId');
    console.log('VpcId:', vpcId);
  }
}
```

`cdk synth`を実行しテンプレートを生成した時点でVPCのIDが取得できていることが確認できます。

```
$ cdk synth
VpcId: vpc-0ba07ca4b3eeb6e38
```

また、cdk.context.jsonには以下のようにキャッシュされた値が保存されています。

```cdk.context.json
{
  "ssm:account=${AccountID}:parameterName=/VpcId:region=ap-northeast-1": "vpc-0ba07ca4b3eeb6e38"
}

```

次に、キャッシュがない場合どのような挙動になるか確認してみます。
キャッシュを削除して、AWSコンソールからパラメータ値を適当な値に変更してみます。
キャッシュの削除は以下のコマンドで行います。

```
cdk context --reset ssm:account=${AccountID}:parameterName=/VpcId:region=ap-northeast-1
```

再度`cdk synth`を実行すると、キャッシュの値が更新されていました。

```cdk.context.json
{
  "ssm:account=${AccountID}:parameterName=/VpcId:region=ap-northeast-1": "vpc-0ba07ca4b3eeb6e38-v2"
}

```

これで、`cdk.context.json`にキャッシュがある場合はその値を使用し、キャッシュがない場合はパラメータストアから値を取得するということが確認できました。

### valueForStringParameter
- デプロイ時にパラメータストアから値を取得します
- プレーンな文字列のみ取得可能で、Secure Stringを取得する場合は後述のSecretValue.ssmSecureを使用します
- このメソッドは、実際の値ではなくトークンを返します
- パラメータ値のバージョンを指定することも可能で、デフォルトでは最新バージョンが参照されます


実際の値でなくトークンを返すという部分がピンとこなかったため、以下のようなコードで実際に確認してみます。

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from 'aws-cdk-lib/aws-ssm';

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

    const vpcId = ssm.StringParameter.valueForStringParameter(this, '/VpcId');
    console.log(vpcId);
  }
}

```

`cdk synth`を実行すると、VPCのIDではなく以下のようにトークンを取得できます。
詳細は割愛しますが、この時点ではまだ値がわかっていないため（実際にデプロイしていないため）、デプロイ時に利用可能となるトークンを返すようになっています。

```
$ cdk synth
${Token[TOKEN.83]}
```

### SecretValue.ssmSecure
- `valueForStringParameter`と同様、リソースデプロイ時に指定したパラメータをパラメータストアから取得する
- Secure Stringを取得する場合に使用する
- パラメータ値のバージョンを指定することもでき、defaultをlatestを参照する

使い方はvalueForStringParameterとほぼ同じで、SecureStringを使用したい場合にはこちらを用います。ちなみに`cdk synth`を実行すると、トークンではなく以下のようなSecretValueクラスを返します。

```
$ cdk synth
SecretValue {
  creationStack: [ 'stack traces disabled' ],
  value: CfnDynamicReference {
    creationStack: [ 'stack traces disabled' ],
    value: '{{resolve:ssm-secure:/VpcId}}',
    typeHint: 'string'
  },
  typeHint: 'string',
  rawValue: CfnDynamicReference {
    creationStack: [ 'stack traces disabled' ],
    value: '{{resolve:ssm-secure:/VpcId}}',
    typeHint: 'string'
  }
}
```

# パラメータストアから取得した値を参照にリソースを定義する方法
続いては、パラメータストアから取得した値をもとにAWSリソースを定義する方法について紹介します。
パラメータストアから取得したVPCのIDからVPCを定義し、そのVPCに紐づくSecurity Groupを作成するケースを考えてみます。

### パラメータストアに値が格納されている場合
パラメータストアにパラメータを格納されている場合、`valueFromLookup`を使用してVPC IDを取得し、その値を使ってVPCを定義します。

```ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from 'aws-cdk-lib/aws-ssm';

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

    const vpcId = ssm.StringParameter.valueFromLookup(this, '/VpcId');
    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', {
      vpcId: vpcId,
    });
    new ec2.SecurityGroup(this, 'SecurityGroup', {
      vpc: vpc,
    });
  }
}

```

### パラメータストアへ値を格納できない（格納しない）場合
パラメータストアから値を参照するが、テンプレート合成時もしくはデプロイ時にはまだパラメータが存在しない場合（例えば、別のStackで定義した値をパラメータストアに格納し、その値を参照してリソースを定義するパターン）どうするか試してみました。

以下のように`valueForStringParameter`と`fromLookup`を組み合わせるとエラーが発生します。これは、`valueForStringParameter`はトークン値であり実際の値ではないためです。

```ts:エラーが発生するパターン
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from 'aws-cdk-lib/aws-ssm';

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

    const vpcId = ssm.StringParameter.valueForStringParameter(this, '/VpcId');
    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', {
      vpcId: vpcId,
    });
    new ec2.SecurityGroup(this, 'SecurityGroup', {
      vpc: vpc,
    });
  }
}
```

トークン値をもとにリソースを定義するには`fromVpcAttributes`を使うことで解決できます。 `vpcId`と`avalilabiliyZones`が必須のプロパティとなっています。

```ts:fromXXXAttributesを使うパターン
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from 'aws-cdk-lib/aws-ssm';

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

    const vpcId = ssm.StringParameter.valueForStringParameter(this, '/VpcId');
    const vpc = ec2.Vpc.fromVpcAttributes(this, 'Vpc', {
      vpcId: vpcId,
      availabilityZones: ['ap-northeast-1a', 'ap-northeast-1c'],
    });
    new ec2.SecurityGroup(this, 'SecurityGroup', {
      vpc: vpc,
    });
  }
}

```
# まとめ
### パラメータを取得するとき
- パラメータストアに値が格納されている場合は、`valueFromLookup`を使う
- パラメータストアに値が格納されていない場合（別Stackで作成した値をパラメータストア経由で受け渡す場合）は、`valueForStringParameter`や`SecretValue.ssmSecure`を使う

### パラメータ値をもとにAWSリソースを定義するとき
- パラメータストアに値が格納されている場合は、`valueFromLookup`を使う
- パラメータストアに値が格納されていない場合は、`fromXXXAttributes`を使う


本記事では、AWS CDKを使用してパラメータストアから値を取得し、それをもとにリソースを定義する方法を解説しました。パラメータの取得方法やパラメータ値をもとにAWSリソースを定義する方法はいくつかあり、パラメータストアに値が格納されているかどうかで使用するメソッドが違うということがわかりました。
不明点や誤っている箇所ございましたら、ご指摘いただけると助かります。

# 参考
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/get_ssm_value.html

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html#static-fromwbrvpcwbrattributesscope-id-attrs