【AWS CDK (Python)】 CDKスタックへパラメータを渡す方法

## はじめに
本記事は、私自身の備忘録を兼ねてAWS CDKをこれから始める方の一助になればと思い、AWS CDKの使い方等をまとめたものです。
今回は、AWS CDKを実行する際にCDKスタックにパラメータを渡してスタックやリソースの設定を制御する方法を確認しています。
なお、本記事は私自身の経験を基に記載していますが、間違いがあったらすみません。

## CDKスタックにパラメータを渡したい状況
AWS CDKでリソースをデプロイする際にスタックにパラメータを渡したくなることがあります。
例えば、
・デプロイする環境を分けたい場合（例：dev、prodなど）
・デプロイするリージョンをパラメータで指定したい場合
などです。
もちろんコードにべた書きでも良いのですが、実行するたびにコードを書き換えるのも手間なので実行時にパラメータで渡せるようにする方法を確認します。

## 環境
本記事は以下の環境を使用して記載しています。
* AWS Cloud9
* AWS CDK：2.80.0
* Python： 3.10.11
* Node.js： 16.20.0

また、以下の記事に基づいてAWS CDKの環境を作成しています。
* [【AWS CDK/Python】 AWS CDKの始め方](https://qiita.com/takataka987/items/4448dbf06d8c70173305)

## CDKスタックへパラメータを渡す方法
では、本題のCDKスタックへパラメータを渡す方法を確認します。今回は、環境を分けるパラメータとして`ENV`を渡したいと思います。
スタックへパラメータを渡す方法は、2通りありますので、それぞれ確認していきます。
* [cdkコマンドで渡す方法](#cdkコマンドで渡す方法)
* [コンテキスト変数で渡す方法](#コンテキスト変数で渡す方法)


### cdkコマンドで渡す方法
cdkコマンドで`ENV`を指定するのですが、まずはcdkコマンドで渡すパラメータをCDKのプログラムで受け取るところを確認します。
パラメータは、`cdk.App().node.try_get_context("パラメータ名")`で受け取れます。
app.pyを以下のように変更して`ENV`パラメータを取得するようにしました。
取得した`ENV`パラメータをスタックをロードする際にスタック名に追加し、加えてスタックにパラメータとして渡すようにしています。
```python:app.py
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_app_stack import CdkAppStack

app = cdk.App()

# ↓↓ コマンドからパラメータを取得 ↓↓
param_env = app.node.try_get_context("ENV")
# ↑↑ コマンドからパラメータを取得 ↑↑

# ↓↓ スタック名に追加&パラメータで渡す ↓↓
CdkAppStack(app, "CdkAppStack-" + param_env,
        param=param_env
    )
# ↑↑ スタック名に追加&パラメータで渡す ↑↑

app.synth()
```

なお、スタックのプログラムは以下のようにS3バケットを作成するようになっていて、リソースの論理ID、S3バケットの名前にパラメータとして渡された`ENV`パラメータを付けるようにしています。
```python:cdk_app/cdk_app_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    RemovalPolicy
)
from constructs import Construct

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, param_env, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket-" + param_env,
            bucket_name = "cdk-test-bucket-" + param_env,
            removal_policy=RemovalPolicy.DESTROY
        )
```

この状態でデプロイします。コマンドでパラメータを渡す際は`-c パラメータ名=パラメータ値`を付けて実行します。ここでは、パラメータ値として`dev`を指定しました。
app.pyで`ENV`パラメータをスタックをロードする際にスタック名に追加したので、スタック名に`dev`が付いていることが確認できます。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy -c ENV=dev

✨  Synthesis time: 18.12s

(中略)
CdkAppStack-dev: deploying... [1/1]
CdkAppStack-dev: creating CloudFormation changeset...

 ✅  CdkAppStack-dev

✨  Deployment time: 36.81s

Stack ARN:
(中略)

✨  Total time: 54.93s
```

マネージメントコンソールでS3バケットを確認してみます。バケット名にも`dev`が付いてますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/ae81e33d-9b48-f63f-0bb3-b4fd60c9d3b0.png)

今度は、パラメータ値として`prod`を指定して実行してみます。パラメータ値をスタック名に付けるようにしているので、先ほどとは別スタックとして起動することが確認できます。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy -c ENV=prod

✨  Synthesis time: 17.4s

(中略)
CdkAppStack-prod: deploying... [1/1]
CdkAppStack-prod: creating CloudFormation changeset...

 ✅  CdkAppStack-prod

✨  Deployment time: 36.65s

Stack ARN:
(中略)

✨  Total time: 54.05s
```

再度、S3バケットを確認してみると、`prod`が付いたバケットが追加されてました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/0321b513-1dfd-72f7-7684-30933226a185.png)

これで、cdkコマンドでパラメータを渡す方法が確認できました。次の確認をする前にS3バケットを削除しておきます。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk destroy -c ENV=dev
(.venv) user_name:~/environment/cdk-app (master) $ cdk destroy -c ENV=prod
```

### コンテキスト変数で渡す方法
次にコンテキスト変数を利用してパラメータを取得します。コンテキスト変数は、CDKの設定ファイルであるcdk.jsonに設定します。cdk.json内の`"context"`に`"ENV": "dev"`を追加します。
```json:cdk.json
{
  "app": "python3 app.py",
  "watch": {
    "include": [
      "**"
    ],
    "exclude": [
      "README.md",
      "cdk*.json",
      "requirements*.txt",
      "source.bat",
      "**/__init__.py",
      "python/__pycache__",
      "tests"
    ]
  },
  "context": {
    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
    "@aws-cdk/core:checkSecretUsage": true,
    "@aws-cdk/core:target-partitions": [
      "aws",
      "aws-cn"
    ],
    "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
    "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
    "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
    "@aws-cdk/aws-iam:minimizePolicies": true,
    "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
    "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
    "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
    "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
    "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
    "@aws-cdk/core:enablePartitionLiterals": true,
    "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
    "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
    "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
    "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
    "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
    "@aws-cdk/aws-route53-patters:useCertificate": true,
    "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
    "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
    "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
    "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
    "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
    "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
    "@aws-cdk/aws-redshift:columnId": true,
    "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
    "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
    "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
    "# comment": "# ↓↓ パラメータ追加 ↓↓",
    "ENV": "dev",
    "# comment": "# ↑↑ パラメータ追加 ↑↑"
  }
}
```

コンテキスト変数で渡す場合も`cdk.App().node.try_get_context("パラメータ名")`で受け取れるので、app.pyやcdk_app/cdk_app_stack.pyは先ほどのままです。
```python:app.py(再掲)
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_app_stack import CdkAppStack

app = cdk.App()

# ↓↓ コマンドからパラメータを取得 ↓↓
param_env = app.node.try_get_context("ENV")
# ↑↑ コマンドからパラメータを取得 ↑↑

# ↓↓ スタック名に追加&パラメータで渡す ↓↓
CdkAppStack(app, "CdkAppStack-" + param_env,
        param=param_env
    )
# ↑↑ スタック名に追加&パラメータで渡す ↑↑

app.synth()
```

```python:cdk_app/cdk_app_stack.py(再掲)
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    RemovalPolicy
)
from constructs import Construct

class CdkAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, param_env, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3bucket = s3.Bucket(
            self,
            "MyS3Bucket-" + param_env,
            bucket_name = "cdk-test-bucket-" + param_env,
            removal_policy=RemovalPolicy.DESTROY
        )
```

この状態でデプロイします。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy
```

マネージメントコンソールでS3バケットを確認してみます。バケット名に`dev`が付きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/54e907de-2737-3d90-b1a3-68c3c16b1321.png)

これで、コンテキスト変数でパラメータを渡す方法が確認できました。S3バケットを削除しておきます。
```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk destroy
```

最後に、cdkコマンドとコンテキスト変数両方で同じパラメータ名に別のパラメータ値を渡すとどうなるか試してみます。
コンテキスト変数で`ENV`パラメータに`dev`を指定した状態で、cdkコマンドで`ENV`パラメータに`prod`を指定して実行します。
```json:cdk.json(再掲)
{
  "app": "python3 app.py",
  "watch": {
(中略)
  },
  "context": {
    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
(中略)
    "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
    "# comment": "# ↓↓ パラメータ追加 ↓↓",
    "ENV": "dev",
    "# comment": "# ↑↑ パラメータ追加 ↑↑"
  }
}
```

```console
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy -c ENV=prod

✨  Synthesis time: 15.92s

(中略)
CdkAppStack-prod: deploying... [1/1]
CdkAppStack-prod: creating CloudFormation changeset...

 ✅  CdkAppStack-prod

✨  Deployment time: 36.76s

Stack ARN:
(中略)

✨  Total time: 52.68s
```

デプロイすると`CdkAppStack-prod`スタックが作成され、S3バケットにも`prod`が付いてますね。cdkコマンドでの指定が優先されるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3445559/e4b2f800-19e7-097f-a323-84128181448b.png)

## まとめ
AWS CDKでスタックにパラメータを渡す方法を確認しました。用途によってcdkコマンドから渡す方法とコンテキスト変数で渡す方法と使い分ける必要がありそうですね。
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。
#### 参考文献
* [AWS Cloud Development Kit (AWS CDK) v2デベロッパーガイド](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html)
* [AWS CDK Python Reference](https://docs.aws.amazon.com/cdk/api/v2/python/index.html)