AWS CDKで複数のWindowsインスタンスにそれぞれ一意の環境変数を設定する


## はじめに
CDKやAWSについてある程度知識があり、使ったことがあるかたを読者として想定しています。
ですので、一つ一つの要素についての説明はありませんので、必要に応じてAWSの公式ドキュメントなどを参考に調べていただけるとありがたいです。

[AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/home.html)

[Amazon EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/concepts.html)

[Amazon Machine Image(AMI)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html)

[EC2 ユーザーデータ](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html)

[Windows EC2でのSysprep](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ami-create-win-sysprep.html)

## CDKについて
AWS Cloud Development Kit (AWS CDK) はAWSのインフラを定義し、プロビジョニングできるサービスです。
CDKではインフラの定義を記述するのにプログラミング言語が使用できるため、従来のYAMLやJSONベースのCloudFormationよりも柔軟で再利用性の高いコードが書けます。たとえば、ループを使って複数のインスタンスを簡単に定義することができます。

## やりたいこと

- 多数のWindowsインスタンスを構築したい。
- それぞれのインスタンスで実行するWindowsプログラムにインスタンスごとに違う実行条件を与えたい。

例えば、100個のインスタンス上で動くWindowsプログラムが、それぞれ異なる一意のユーザーIDを持って動くようなことを実現することを考えています。

## 実現の仕方

CDKで複数のWindowsインスタンスをループ文で構築する。
EC2のユーザーデータを使用し、それぞれのインスタンスの環境変数にインスタンスごとに異なる値を設定する。
EC2のプログラムは起動時に環境変数から値を取得する。

## ユーザーデータについて
ユーザーデータはAmazon EC2 インスタンスの起動時にEC2インスタンスのOS上で任意のコマンドを実行させることができる仕組みです。
Windowsの場合はいわゆるバッチファイルとPowerShellのコマンドが実行できます。

ユーザーデータはbase64でエンコードして渡す必要があります。

## 実践
CDKが実行できる環境の準備までは割愛します。
今回はCDKの言語にPythonを使用しました。
CDKのバージョンは以下です。

```shell-session
$ cdk --version
2.1004.0 (build f0ad96e)
```


### PowerShellでのシステム環境変数の設定

PowerShellでは以下のように実行するとシステム環境変数の設定が可能です。

```PowerShell:システム環境変数の設定
[Environment]::SetEnvironmentVariable("環境変数のキー","設定値", [EnvironmentVariableTarget]::Machine)
```

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-7.5#set-environment-variables-with-setenvironmentvariable


環境変数のキーを"userid"、設定値をインスタンスごとに異なる値"userxxxx"(xxxxは4桁の数値)
とすることを考えます。
引数最後の"Machine"は「システム環境変数」に設定することを表しています。
ユーザー環境変数に設定するのであれば、"User"を指定します。


### 異なる値の作り方
可変値"userxxxx"のxxxxの部分の4桁の数値は今回は単純に1ずつ増加する数値としました。
※外部ファイルからユーザーIDのリストを与える方法もあるかもしれませんが、未検証です。

### 事前準備
サブネットやセキュリティグループは事前に作成しておきます。
また大量のWindowsインスタンスで初回ログイン時のパスワード取得をインスタンスごとに実行するのは難儀ですので、パスワード設定済みのAMIを作成してそれを使って起動します。

### 複数のインスタンスの作成(CDK)

```python:インスタンス作成部分
        for i in range(1, 3):
            # ループ変数iを使用して、環境変数設定コマンドの文字列を作成する
            cmdstring1 = '[Environment]::SetEnvironmentVariable("userid","user{:0>4}", [EnvironmentVariableTarget]::Machine)'.format(i)

            # Windows用のユーザーデータを作成し、文字列をユーザーデータに設定する。
            userdata = ec2.UserData.for_windows(persist=True)
            userdata.add_commands(cmdstring1)

            # EC2の作成 、すでに存在するサブネット、セキュリティグループなどを使って作成する。
            ec2.CfnInstance(self,
            "testec2" + str(i),
            availability_zone="eu-west-1c",
            image_id="ami-xxxxxxxxxxxx",
            instance_type="c7a.medium",
            security_group_ids=["sg-xxxxxxxxxxxxxx"],
            subnet_id="subnet-xxxxxxxxxxxxxxxx",
            # ユーザーデータをBase64でエンコードして設定する。
            user_data= Fn.base64(userdata.render()),
            # EC2が区別できるように連番のNameタグを付与する。
            tags=[
                    {
                    "key": "Name",
                    "value": "testec2-{:0>4}".format(i)
                    }
                ]
            )
            
```

:::note info
今回はユーザーデータに「persist=True」を設定しています。この設定でユーザーデータのスクリプトはEC2の初回起動時だけでなく、EC2起動時に毎回実行されます。AMIから起動した時にユーザーデータスクリプトが実行されなかったためこの設定を入れています。
なおWindowsの場合AMI取得時にsysprepを実行すれば、AMIからの起動時でもユーザーデータスクリプトが実行されるようです。今回はsysprepを実行しないで取得したAMIだったので、persist=trueが必要だったようです。
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html
:::
:::note info
EC2に渡すユーザーデータはBase64でエンコードする必要があるため`Fn.base64`を使用しています。コンソールから設定する場合は入力した文字列をBase64でエンコードするチェックボックスがありますが、CDKのコードで記載する場合は明示的にエンコードします。
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
:::



## 実行結果
`cdk deploy`を実行しました。作成するインスタンスは二つです。

### 起動確認(コンソール)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/c16d905d-2057-46e3-8d48-e6a25754cf03.png)

&rarr;インスタンスが二つ起動しています。

### ユーザーデータ確認(コンソール)
それぞれのユーザーデータをチェックします。
インスタンスを選択し、アクション&rarr;インスタンスの設定&rarr;ユーザーデータの編集を選びます。

・一つ目のEC2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/a357e8e1-816f-433d-a5d2-344f41832910.png)

・二つ目のEC2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/4457565e-1e0c-4953-9c9d-0e20a58377a8.png)


&rarr;`userid`にそれぞれ`user0001`、`user0002`を設定するPowerShellが設定されています。
`persist=True`も設定されています。

### 実機確認
実機の環境変数を確認します。
Control Panel&rarr;System & Security&rarr;System&rarr;Advanced system settings&rarr;Environment Variablesです。

・一つ目のEC2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/7223d196-a444-4f07-8c35-bda6a9552021.png)

・二つ目のEC2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/2da06335-e9d7-4a58-83c4-539cd0028127.png)

それぞれのEC2のシステム環境変数で異なる値が設定されています。

# まとめ
CDKを使って、複数のインスタンスを起動し、それぞれのインスタンスのOSに異なるシステム環境変数を渡すことができました。

大量のWindowsインスタンスを起動し、各インスタンス上で動く同じスクリプトが、それぞれ違う設定を持って動く環境が必要になったので検証しました。
今回はインスタンスに与える値を数値の連番で作成しましたが、これだと汎用性がないので、外部の設定ファイルに作ったIDリストを入力に使用するようなことができないか検証したいと考えています。