LambdaでWindows インスタンスの Nitro 世代移行を自動スケジューリング化してみた

# はじめに
r4からr5など、XenベースからNitroベースの最新世代へのEC2インスタンスタイプの変更時には
ドライバーのアップグレード等ダウンタイムが生じるOS内部作業が伴います。

必然的にダウンタイムが長くなるため平日日中帯を避けたいケースも多いのではないでしょうか。
Lambda(python)で自動スケジューリング化に挑戦してみました。


# 環境
EC2インスタンスタイプ：r4.largeからr5.largeへ変更
EC2のOS：Windows server 2012 R2
Lambdaで使うランタイム：Python3.9

#作業全体像
今回の作業は以下ドキュメントに沿って行います。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/WindowsGuide/migrating-latest-types.html#auto-upgrade

1. AWS PV ドライバーのインストールとアップグレード
1. ENA のインストールとアップグレード
1. AWS NVMe ドライバーをアップグレード
1. EC2Config および EC2Launch の更新
1. ベアメタルインスタンスのシリアルポートドライバーのインストール
1. 電源管理設定の更新
1. 新しいインスタンスタイプ用のインテルチップセットドライバーの更新

上記のうちパート1〜3はSSMオートメーションの `AWSSupport-UpgradeWindowsAWSDrivers` を使用することで自動化可能です。
`AWSSupport-UpgradeWindowsAWSDrivers` はドライバインストール作業前に変更前AMIを取得してくれる事もありがたいです。

多くの場合パート5、パート7は不要になります。パート4、パート6はSSMのRun Commandから実行可能です。
EC2LaunchはWindows Server 2016 および 2019向けの為、今回はEC2Configの更新になります。

以下の流れをLambdaで実行することでWindows インスタンスの Nitro 世代移行を自動スケジューリング化できます。

1. SSM オートメーションで `AWSSupport-UpgradeWindowsAWSDrivers` を実行
1. EC2を停止⇨タイプ変更⇨起動
1. SSM Run CommandでEC2Configの更新、電源管理設定の更新を実行

#作業前確認
現在のバージョンや設定内容を確認します

NVMEドライバ
<img width="800" alt="PVデバイスマネージャー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/49e51aa4-7a56-5a1d-dce0-977ebe03598b.png">

AWS PV ドライバー

```shell
PS C:\Users\Administrator> Get-ItemProperty HKLM:\SOFTWARE\Amazon\PVDriver | Select-Object Version

Version
-------
8.4.0

```

ENAサポートを確認

```shell
aws ec2 describe-instances --instance-ids インスタンスID  --query "Reservations[].Instances[].EnaSupport" 
[
    true
]
```
    

電源設定

```shell
PS C:\Users\Administrator> powercfg -q 381b4222-f694-41f0-9685-ff5bb260df2e 7516b95f-f776-4464-8c53-06167f40cc99
電源設定の GUID: 381b4222-f694-41f0-9685-ff5bb260df2e  (バランス)
  GUID エイリアス: SCHEME_BALANCED
  サブグループの GUID: 7516b95f-f776-4464-8c53-06167f40cc99  (ディスプレイ)
    GUID エイリアス: SUB_VIDEO
    電源設定の GUID: 3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e  (次の時間が経過後ディスプレイの電源を切る)
      GUID エイリアス: VIDEOIDLE
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0xffffffff
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: 秒
    現在の AC 電源設定のインデックス: 0x00000258
    現在の DC 電源設定のインデックス: 0x0000012c
```

#IAMロール、Lambdaの設定
作業対象のEC2にSSMから操作できるようにIAMロールを付与します。
ロールに付与する権限はAmazonSSMManagedInstanceCoreを使用しました
![スクリーンショット 2021-11-15 17.46.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/1ab48d09-3451-2649-33c3-3269c6d7d739.png)

ここからはLambda関数の作成です
ランタイムはpython3.9を使用しました
![スクリーンショット 2021-11-15 17.55.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/52b22d06-fc66-cab0-e058-03d5bee7230c.png)

実行ロール作成後に作成したロールがSSMを利用可能なように権限を修正します
設定>アクセス権限から作成したロール名がリンクになっているのでクリックします
![スクリーンショット 2021-11-15 17.57.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/288913ec-cffe-5877-25ef-58977c595d4c.png)

IAMロールにAmazonEC2FullAccess,AmazonSSMFullAccessを付与します
![スクリーンショット 2021-11-15 18.05.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/6e7faa2a-a8a9-8f8a-b849-2889cdd0451a.png)

信頼関係を追加します
![スクリーンショット 2021-11-15 18.02.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/0c6a2004-bf9d-6f33-23c0-7509982dab14.png)

Lambdaはデフォルトのタイムアウト時間が3秒です。
今回は１０分以上かかる見込みのため最大値の15分に設定します
![スクリーンショット 2021-11-15 18.07.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/88e23494-3a7c-61a0-5f88-489ed29e9316.png)




#実装

Lambda関数のコードには以下を記述します
今回の作業ではAWSSupport-UpgradeWindowsAWSDriversの実行に8分程度かかった為、
AWSSupport-UpgradeWindowsAWSDriversの実行ステータスを確認して終了次第、次のStepに進むようにしています

EC2のスペック変更も停止状態のステータスを確認後に実施するようにしています
作業を自動化する場合、特に失敗時には通知が欲しいのでSNSに通知するようにしました。

```python
import boto3
import os 
import time

ssm = boto3.client('ssm')
sns = boto3.client('sns')
ec2 = boto3.client('ec2')
#変更対象のEC2
Instance = 'インスタンスID'
disired_type = '変更後のインスタンスタイプ'

def lambda_handler(event, context):
    try:
        upgrade_drivers()
    except:
        sns.publish(
            TopicArn='通知先SNSのARN',
            Subject=os.environ['AWS_LAMBDA_FUNCTION_NAME'] + "のエラーが発生しました",
            Message='詳細はCloudwatch logsを参照'
            ) 

def ssm_hundler():
    ec2_ssm_response = ssm.describe_instance_information(
    InstanceInformationFilterList=[
            {
            'key': 'InstanceIds',
            'valueSet': [
                    Instance,]},
                    ])

    if any(ec2_ssm_response['InstanceInformationList']):
        update_config()
        update_power_settings()
    else:
        print('SSMが実行可能な状態まで待機します')
        time.sleep(30)
        ssm_hundler()

def update_config():
    ec2_config_response = ssm.send_command(
    InstanceIds=[f'{Instance}'],
    DocumentName='AWS-UpdateEC2Config',
    DocumentVersion='1'
    )

def update_power_settings():
    ec2_config_response = ssm.send_command(
    InstanceIds=[f'{Instance}'],
    DocumentName='AWS-RunPowerShellScript',
    DocumentVersion='1',
    Parameters={
    'commands': [
        'powercfg /setacvalueindex 381b4222-f694-41f0-9685-ff5bb260df2e 7516b95f-f776-4464-8c53-06167f40cc99 3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e 0',
        'powercfg /setacvalueindex 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c 7516b95f-f776-4464-8c53-06167f40cc99 3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e 0',
        'powercfg /setacvalueindex a1841308-3541-4fab-bc81-f71556f20b4a 7516b95f-f776-4464-8c53-06167f40cc99 3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e 0'
    ]   } )

def change_instance_type():
    ec2_stop_responce = ec2.stop_instances(
    InstanceIds=[Instance,])
    time.sleep(90)

    ec2_stop_status = ec2_stop_responce['StoppingInstances'][0]['CurrentState']['Name']

    while ec2_stop_status == 'stopping' or ec2_stop_status ==  'pending':
        time.sleep(30)
        ec2_current_response = ec2.describe_instances(InstanceIds=[Instance,])
        ec2_stop_status = ec2_current_response['Reservations'][0]['Instances'][0]['State']['Name']
        print('EC2の状態は'+ec2_stop_status)


    if ec2_stop_status == 'stopped':
        ec2_modify_response = ec2.modify_instance_attribute(
        InstanceId=Instance,
        InstanceType={'Value': disired_type})
        time.sleep(10)
        ec2_start_responce = ec2.start_instances(
        InstanceIds=[Instance])
        time.sleep(30)
        print('SSMコマンドに移ります')
        ssm_hundler()
    

def upgrade_drivers():
    automation_response = ssm.start_automation_execution(
    DocumentName='AWSSupport-UpgradeWindowsAWSDrivers',
    DocumentVersion='$DEFAULT',
    Parameters={
        'InstanceId': [Instance]
        }
    )
    automation_id = automation_response['ResponseMetadata']['RequestId']
    

    automation_check = ssm.describe_automation_executions(
    Filters=[
        {
            'Key': 'ExecutionId',
            'Values': [
                automation_id
                ]},
        ]
        )
    automation_status = automation_check['AutomationExecutionMetadataList'][0]['AutomationExecutionStatus']
      
    while automation_status == 'Pending' or automation_status == 'InProgress':
        time.sleep(60)
        automation_check = ssm.describe_automation_executions(
        Filters=[
            {
                'Key': 'ExecutionId',
                'Values': [
                    automation_id
                    ]},
            ])
        automation_status = automation_check['AutomationExecutionMetadataList'][0]['AutomationExecutionStatus']
        print(automation_status)

    
    if automation_status == 'Success':
        change_instance_type()
```

#スケジューリング設定
トリガーを追加します
![スクリーンショット 2021-11-15 18.10.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/bce77309-032c-faf5-ec3b-0d356b3c1f29.png)

Event Bridge(Cloudwatch Events)を選びます
選んだ後にcron方式などで日時のスケジューリングができます。
![スクリーンショット 2021-11-15 18.14.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/8c9b790c-d25c-a1f0-3216-13d6f52a70ab.png)


ここまでで設定は完了です！
#作業後確認
作業後のバージョンや設定内容を確認します

NVMEドライバー

<img width="800" alt="作業後PVデバイスマネージャー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2242298/55162c56-ae3e-54b1-bc7d-dfb7899022fb.png">


AWS PV ドライバー

```shell
PS C:\Users\Administrator> Get-ItemProperty HKLM:\SOFTWARE\Amazon\PVDriver | Select-Object Version

Version
-------
8.4.0

```

ENAサポートを確認

```shell
aws ec2 describe-instances --instance-ids インスタンスID  --query "Reservations[].Instances[].EnaSupport" 
[
    true
]
```

電源設定

```shell
PS C:\Users\Administrator> powercfg -q 381b4222-f694-41f0-9685-ff5bb260df2e 7516b95f-f776-4464-8c53-06167f40cc99
電源設定の GUID: 381b4222-f694-41f0-9685-ff5bb260df2e  (バランス)
  GUID エイリアス: SCHEME_BALANCED
  サブグループの GUID: 7516b95f-f776-4464-8c53-06167f40cc99  (ディスプレイ)
    GUID エイリアス: SUB_VIDEO
    電源設定の GUID: 3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e  (次の時間が経過後ディスプレイの電源を切る)
      GUID エイリアス: VIDEOIDLE
      利用可能な設定の最小値: 0x00000000
      利用可能な設定の最大値: 0xffffffff
      利用可能な設定の増分: 0x00000001
      利用可能な設定の単位: 秒
    現在の AC 電源設定のインデックス: 0x00000000
    現在の DC 電源設定のインデックス: 0x00000000

```


#おわりに
普段の業務ではプログラミングを使わないのですが、自己学習していたPythonを活用したい！と思い挑戦してみました。
boto3のレスポンスは多次元配列になっていて、最初は初心者の自分には取扱が難しかったです。
根気強く取り組む内にboto3のDocumentのレスポンスシンタックスから取得したい値を抽出できるようになってきました。
今後もSSMやLambdaをうまく使って作業の自動化を進めていきたいと思います。