AWS CDKでクイック作成リンク用のテンプレートを合成する

## クイック作成リンクとは

CloudFormationのスタック作成用マネジメントコンソールへリンクするURLを使って，GUIで簡単にスタックを作成する仕組み．URLのクエリパラメータにスタック名やスタックのパラメータを埋め込むことができるので，それらはページを開いたときに入力済となるので，最短１クリックでスタックを作成可能なページを準備できます．

（[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-console-create-stacks-quick-create-links.html)）

[<img width = 400 src = "https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/images/console-quick-create-link-example.png">](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-console-create-stacks-quick-create-links.html)

## 使いどころ

顧客など非開発者がGUIを操作してスタックを作成する際などに便利です．クイック作成リンクあらかじめ作成しお渡ししておけば，誰でもCloudFormationスタックを作成可能です．

## CDKを使ってクイック作成リンクを作成する手順

### 前提

- `cdk init` 実施済
- `cdk bootstrap` 実施済

### 手順

1. CDKを使って，スタックパラメータを含むテンプレートを合成する（鬼門）
1. `cdk-assets`を使って，テンプレートをS3にpushする
1. クイック作成リンクを生成する

## STEP1：CDKを使って，スタックパラメータを含むテンプレートを合成する

[CfnParameterクラス](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.CfnParameter.html)を使って，スタックパラメータを含むCDKのコードを実装していきます．CfnParameterクラスはCDKの一般的なユースケースにおいて非推奨とされていますが，クイック作成リンクのユースケースにおいては使わざる得ないです．
（[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/parameters.html)にて一般的にはメリットがないと明記されており，constractの利用が推奨されています．）

本記事では，下記のサンプルコードを例に部分的に抜粋しながら解説します．パラメータをもとに2つのSQSの作成するCDKのサンプルです．
※サンプルコード全体は，[GitHub](https://github.com/h-masano/cdk-quick-create-links)上に公開しているのでご参照ください．

``` TypeScript
// サンプルコード
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sqs from 'aws-cdk-lib/aws-sqs';

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

    // params
    const fifoQueue = new cdk.CfnParameter(this, 'ParamFifoQueue', {
      allowedValues: ['true', 'false'],
      default: 'false'
    });
    const contentBasedDeduplication = new cdk.CfnParameter(this, 'ParamContentBasedDeduplication', {
      allowedValues: ['true', 'false'],
      default: 'false'
    });
    const visibilityTimeout = new cdk.CfnParameter(this, 'ParamVisibilityTimeout', {
      type: 'Number',
      default: 300
    })

    // example resource (SQS-1)
    const queue1 = new sqs.Queue(this, 'CdkQuickCreateLinksQueue1', {
      visibilityTimeout: cdk.Duration.seconds(visibilityTimeout.valueAsNumber)
    });

    // set bool params to prop as L1 construct (SQS-1)
    const cfnQueue1 = queue1.node.defaultChild as sqs.CfnQueue;
    const fifoCondition = new cdk.CfnCondition(this, 'FifoCondition', {
      expression: cdk.Fn.conditionEquals(fifoQueue, 'true')
    })
    cfnQueue1.addPropertyOverride('FifoQueue',
      cdk.Fn.conditionIf(fifoCondition.logicalId, true, cdk.Fn.ref("AWS::NoValue"))
    );

    // example resource (SQS-2)
    const queue2 = new sqs.Queue(this, 'CdkQuickCreateLinksQueue2', {
      fifo: true,
      visibilityTimeout: cdk.Duration.seconds(visibilityTimeout.valueAsNumber)
    });

    // set bool params to prop as L1 construct (SQS-2)
    const cfnQueue2 = queue2.node.defaultChild as sqs.CfnQueue;
    cfnQueue2.addPropertyOverride('ContentBasedDeduplication',
      cdk.Fn.ref(contentBasedDeduplication.logicalId)
    );

    // output for check params
    new cdk.CfnOutput(this, 'OutputFifoQueue', {
      value: fifoQueue.valueAsString
    })
    new cdk.CfnOutput(this, 'OutputContentBasedDeduplication', {
      value: contentBasedDeduplication.valueAsString
    })
    new cdk.CfnOutput(this, 'OutputVisibilityTimeout', {
      value: visibilityTimeout.valueAsString
    })
  }
}
```

### パラメータの定義

``` TypeScript
// パラメータ定義抜粋
const fifoQueue = new cdk.CfnParameter(this, 'ParamFifoQueue', {
      allowedValues: ['true', 'false'],
      default: 'false'
    });
const contentBasedDeduplication = new cdk.CfnParameter(this, 'ParamContentBasedDeduplication', {
  allowedValues: ['true', 'false'],
  default: 'false'
});
const visibilityTimeout = new cdk.CfnParameter(this, 'ParamVisibilityTimeout', {
  type: 'Number',
  default: 300
})
```

今回用意するパラメータは3つ．2つはbool型（実際にはtrue/falseの文字列）と，数値のパラメータが１つです．

### パラメータをリソースのpropsに引き渡し

#### i. 数値や文字列の場合

``` TypeScript
// SQS作成部分抜粋（数値パラメータの場合）
const queue1 = new sqs.Queue(this, 'CdkQuickCreateLinksQueue1', {
  visibilityTimeout: cdk.Duration.seconds(visibilityTimeout.valueAsNumber)
});
```

数値や文字列のパラメータの場合，`valueAsNumber`や`valueAsString`を用いてL2コンストラクタのpropsにそのままパラメータを引き渡すことが可能です．
`cdk synth`実行後に合成されるテンプレートをみると，組み込み関数`Ref`を用いて，パラメータがスタック作成時に後から参照されるようにテンプレートが合成されます．

``` json
{
    "Parameters": {
        "ParamVisibilityTimeout": {
            "Type": "Number",
            "Default": 300
        }
    },
    "Resources": {
        "CdkQuickCreateLinksQueue254C8F5FB": {
            "Type": "AWS::SQS::Queue",
            "Properties": {
                "VisibilityTimeout": {
                 "Ref": "ParamVisibilityTimeout"
                }
            }
        }
    }
}
```
#### ii. boolの場合

boolの場合が鬼門で，`valueAsBool`が用意されておらず，L2コンストラクトのbool型のpropに文字列を引き渡すことはできません．
また，`CfnParameter`はテンプレート合成時に実際のValueが取得できないので，例えば下記のような条件文を使った制御はできません．（このあたりが一般的に`CfnParameter`が非推奨とされている理由のようです．）

``` TypeScript
// NGコード
const queue1 = new sqs.Queue(this, 'CdkQuickCreateLinksQueue1', {
  fifo: fifoQueue.valueAsString === 'true'
});
```

そこでbool型の場合，L2コンストラクトをL1コンストラクトに変換したうえで，合成後のCloudFormationテンプレートをそのまま記述する要領で，リソースのpropsにパラメータの参照情報を上書きしていきます．その設定方法は複数あります．
第一に，true or nullを設定する場合は，下記にように実装する方法があります．例えばSQSのFifoQueue設定の場合，標準キューを作成する際に明示的にfalseを設定するとスタックの作成に失敗してしまうので，その場合は`AWS::NoValue`が設定されるようにconditionを組み立てていきます．
（[参考](https://github.com/aws/aws-cdk/issues/8550)）

``` TypeScript
// SQS設定上書き部分抜粋（boolパラメータ設定例１）
const cfnQueue1 = queue1.node.defaultChild as sqs.CfnQueue;
const fifoCondition = new cdk.CfnCondition(this, 'FifoCondition', {
  expression: cdk.Fn.conditionEquals(fifoQueue, 'true')
})
cfnQueue1.addPropertyOverride('FifoQueue',
  cdk.Fn.conditionIf(fifoCondition.logicalId, true, cdk.Fn.ref("AWS::NoValue"))
);
```

一方で，SQSのコンテンツに基づく重複排除設定の例の場合，単純にtrue/falseを設定することができるので，conditionの組み立ては不要です．

```TypeScript
// SQS設定上書き部分抜粋（boolパラメータ設定例２）
const cfnQueue2 = queue2.node.defaultChild as sqs.CfnQueue;
cfnQueue2.addPropertyOverride('ContentBasedDeduplication',
  cdk.Fn.ref(contentBasedDeduplication.logicalId)
);
```

最後に，CDKの実装が終わったら`cdk synth`を実行してテンプレートを合成します．

## STEP2：`cdk-assets`を使って，テンプレートをS3にpushする

テンプレートを合成したら，CDKプロジェクト配下で`npm install cdk-assets`を実行し，テンプレートをS3にpushするパッケージをインストールのうえ，`cdk-assets publish --path cdk.out/<XXXX: synthで合成されたテンプレート>Stack.assets.json -v` を実行します．これにより，`cdk bootstrap`時に生成されたS3バケットに対してテンプレートがpushされます．
（本記事のサンプルコードの場合は，実際は`npx cdk-assets publish --path cdk.out/CdkDeploymentControllerTestStack.assets.json -v`というコマンドになります．）

cdk-assetsが何者かについては，[こちら](https://www.ogis-ri.co.jp/otc/hiroba/technical/cdk-concepts/part6.html#:~:text=%E3%81%82%E3%82%8B%E3%82%8F%E3%81%91%E3%81%A7%E3%81%99%E3%80%82-,%E2%91%A3%20%E3%83%91%E3%83%96%E3%83%AA%E3%83%83%E3%82%B7%E3%83%A5%E3%81%A8%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4,-%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%AA)で一部解説されています．通常のCDKフローにおいては，`cdk deploy`コマンドでテンプレートのS3 pushとスタックの作成が同時に行われますが，cdk-assetsを用いることでテンプレートのS3 pushのみを実行できます．

## STEP3：クイック作成リンクを生成する

クイック作成リンクのURLの構文は，[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-console-create-stacks-quick-create-links.html)に記載されています．

templateURLに設定すべきクエリですが，デフォルトのまま`cdk bootstrap`を行った場合，`cdk-XXXXXXX-assets-<aws account number>-<region>`といった見た目のS3バケットにテンプレートがpushされます．`cdk assets`コマンドの実行結果からも，push先のバケットとオブジェクトが確認可能です．

※ クイックパラメータのイメージ
> https://(region).console.aws.amazon.com/cloudformation/home?region=(region)#/stacks/create/review
> ?stackName=(your Stack name)
> &templateURL=(your template S3 URL)
> &param_ParamContentBasedDeduplication=(true/false)
> &param_ParamFifoQueue=(true/false)
> &param_ParamVisibilityTimeout=(123)

クエリパラメータ（param_Xxxxxx）を省略した場合，CDKのCfnParameterで設定したデフォルト値が入ります．

あとは，生成したクイック作成リンクを実際にスタックを作成する人に教えたうえで，マネジメントコンソールログイン状態でアクセスしてもらうことで，スタック名やパラメータをGUI上で確認しながら，1クリックでスタックを作成できます．
もちろん，GUI上のデフォルトで入力されている値を後から変更することは可能です．

## まとめ（クイック作成リンク×CDKの惜しいところ）

CDKは素のCloudFormationよりも「よしなに」AWSリソースを構築してくれるため大変便利ですが，クイック作成リンクを使う場合は本来非推奨のCFnParameterを使わざる得ないため，特にbool型のパラメータを使う場合に苦労する可能性があります．
また，クイック作成リンクはスタックを新規で作成する際に使うことができますが，既存スタックの更新する際には使えないなど，少々惜しい点が残ります．

## 参考リンク

- 今回のサンプルコード（GitHub）:  https://github.com/h-masano/cdk-quick-create-links
- [小ネタ]CloudFormationのクイック作成リンクの作り方（classmethod）: https://dev.classmethod.jp/articles/cfn-quick-create-link/
- もっとじっくり AWS CDK のコンセプト（オブジェクトの広場）: https://www.ogis-ri.co.jp/otc/hiroba/technical/cdk-concepts/part6.html#:~:text=%E3%81%82%E3%82%8B%E3%82%8F%E3%81%91%E3%81%A7%E3%81%99%E3%80%82-,%E2%91%A3%20%E3%83%91%E3%83%96%E3%83%AA%E3%83%83%E3%82%B7%E3%83%A5%E3%81%A8%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4,-%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%AA
- JAWS-UG CDK支部 #7（Youtube）: https://www.youtube.com/live/_TliSbx1DRw?si=E4h-Axj-AmVpXxZq&t=5352