【Step Functions】パッチ適用状況の定期取得

# 実現したいこと
[前回](https://qiita.com/yiz00247/items/45b49c9b609d53b77c1f)、停止しているインスタンスに対するセキュリティパッチスキャンの定期実施として、Systems Managerを利用した方法を試しました。AWSコンソール上から最新の状態については確認できますが、過去時点の確認ができないため、定期的にパッチ適用状態を取得する方法を試してみました。

# 概要
Step Functionsを利用して、パッチ適用状態を取得しS3にログとして保管する。

1. Step Functionsのステートマシン作成
1. EventBridgeによる定期実施

# Step Functionsのステートマシン作成
Step Functionsを利用してパッチ適用状態を取得します。

## インスタンスのパッチ状態の詳細確認
インスタンスのパッチ状態の詳細を取得するには、**aws ssm describe-instance-patches**コマンドを実施することでScan結果を取得することができます。

``` bash
aws ssm describe-instance-patches \ 
  --instance-id "i-1234567890abcdef0"
```

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/example_ssm_DescribeInstancePatches_section.html

コマンドでは最新のScan結果は見ることができますが、過去の状態については確認することができません。そのため定期的にコマンドを実施し結果をS3に保管することで過去の状態を確認できるようにします。

## S3の保管先
S3の保管先としては下記パスで保管します。日付のパスはStep Functionsで生成します。ファイル名はコマンド結果が複数にわかれるためUUIDを元に作成します。

```
s3://[バケット名]/[パス]/year=[yyyy]/month=[mm]/day=[dd]/instanceid=[instance-id]/(ランダムなファイル名)
```

## ワークフローの内容
作成するStep Functionsは以下のフローになります

![stepfunctions_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3996044/17b15d11-c9d8-45c7-b642-e2aaed04dd2b.png)

- [**date_prefix**]<br>日付をパスで利用するため文字列の生成
- [**PatchesFilters**]<br>describe-instance-patchesのフィルタ条件指定<br>今回はClassification(分類)が"Security"、"CriticalUpdates"、"SecurityUpdates"を対象としています
- [**DescribeInstances**]<br>EC2のタグにCheckPatchが存在するインスタンス一覧を取得し、Mapを利用してループ処理をします
- [**DescribeInstancePatches**]<br>describe-instance-patches を実行
- [**PutObject**]<br>結果をS3に保管
- [**DescribeInstancePatches-NextToken**]<br>1度のコマンドの結果が50件までしか出力できないため、NextTokenを指定して取得します

作成したStep Functionsのコードは下記になります。

<details><summary>サンプルコード</summary>

```
{
  "QueryLanguage": "JSONata",
  "Comment": "SSM Patch Report",
  "StartAt": "date_prefix",
  "States": {
    "date_prefix": {
      "Type": "Pass",
      "Next": "PatchesFilters",
      "Assign": {
        "s3_bucket": "{% $states.input.s3_bucket %}",
        "s3_path": "{% $states.input.s3_path %}",
        "date_prefix": "{% $fromMillis($millis(), 'year=[Y0001]/month=[M01]/day=[D01]') %}"
      }
    },
    "PatchesFilters": {
      "Type": "Pass",
      "Next": "DescribeInstances",
      "Assign": {
        "DescribeInstancePatchesFilters": [
          {
            "Key": "Classification",
            "Values": [
              "Security",
              "CriticalUpdates",
              "SecurityUpdates"
            ]
          }
        ]
      }
    },
    "DescribeInstances": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
      "Next": "ForEachInstance",
      "Output": {
        "Instances": "{% $states.result.Reservations[*].Instances[*].InstanceId %}"
      },
      "Arguments": {
        "Filters": [
          {
            "Name": "tag-key",
            "Values": [
              "CheckPatch"
            ]
          }
        ]
      }
    },
    "ForEachInstance": {
      "Type": "Map",
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "INLINE"
        },
        "StartAt": "DescribeInstancePatches",
        "States": {
          "DescribeInstancePatches": {
            "Type": "Task",
            "Arguments": {
              "Filters": "{% $DescribeInstancePatchesFilters %}",
              "InstanceId": "{% $states.input %}"
            },
            "Resource": "arn:aws:states:::aws-sdk:ssm:describeInstancePatches",
            "Output": {
              "Instance": "{% $states.input %}",
              "result": "{% $states.result %}"
            },
            "Next": "PutObject"
          },
          "PutObject": {
            "Type": "Task",
            "Arguments": {
              "Body": {
                "Patches": "{% $states.input.result.Patches %}"
              },
              "Bucket": "{% $s3_bucket %}",
              "Key": "{% $s3_path& '/' &$date_prefix& '/instanceid=' &$states.input.Instance& '/' &$uuid() %}"
            },
            "Resource": "arn:aws:states:::aws-sdk:s3:putObject",
            "Next": "IfNextTokenExists",
            "Output": "{% $states.input %}"
          },
          "DescribeInstancePatches-NextToken": {
            "Type": "Task",
            "Arguments": {
              "Filters": "{% $DescribeInstancePatchesFilters %}",
              "InstanceId": "{% $states.input.Instance %}",
              "NextToken": "{% $states.input.NextToken %}"
            },
            "Resource": "arn:aws:states:::aws-sdk:ssm:describeInstancePatches",
            "Output": {
              "Instance": "{% $states.input.Instance %}",
              "result": "{% $states.result %}"
            },
            "Next": "Wait"
          },
          "Wait": {
            "Type": "Wait",
            "Seconds": 1,
            "Next": "PutObject"
          },
          "IfNextTokenExists": {
            "Type": "Choice",
            "Choices": [
              {
                "Next": "DescribeInstancePatches-NextToken",
                "Condition": "{% $states.input.result.NextToken != null %}",
                "Output": {
                  "Instance": "{% $states.input.Instance %}",
                  "NextToken": "{% $states.input.result.NextToken %}"
                }
              }
            ],
            "Default": "成功"
          },
          "成功": {
            "Type": "Succeed"
          }
        }
      },
      "End": true,
      "MaxConcurrency": 2,
      "Items": "{% $states.input.Instances %}"
    }
  }
}
```
</details>

:::note info
Step FunctionsでJSONataが利用できるようになり以前よりも使いやすくなりました。コードも試しでJSONataを利用しています。
:::

## Step Functionsの設定
サンプルコードを利用したStep Functionsの設定を下記のように行います

1. Step Functions画面から[**ステートマシンの作成**]を選択します
1. コードから作成する場合は[**空白から作成**]を選択します
1. ステートマシンの[**名前**]を入力し、ステートマシンのタイプは[**標準**]を選択します
1. 作成画面でコードタブを選択し、コード記述欄にサンプルコードを貼り付けます
1. 作成を選択すると確認画面表示されるので[**作成**]を選択します
1. Step Functionsと一緒にIAM Roleが作成されますが、動作に権限が足りないので作成されたIAM Roleに権限を追加します
      
    ```
    作成されるIAM Role: 
    StepFunctions-[ステートマシン名]-role-[ランダムな文字列]

    追加する権限:
    ・AmazonEC2ReadOnlyAccess (インスタンス一覧の取得用途)
    ・AmazonS3FullAccess（S3に保管用）
    ・AmazonSSMReadOnlyAccess (aws ssm describe-instance-patchesの取得用途)
    ```
  
    :::note warn
    実際の利用時は必要に応じて権限の制限をします。
    

1. テストとして手動でStep Functionsを実行します。作成したステートマシン画面から[**実行の開始**]を選択し、保管先用のパラメータを入力値として渡します

    ```
    # 入力パラメータ： 保管先のS3情報を入力値として登録
    {
      "s3_bucket": "[バケット名]/",
      "s3_path": "[パス]"
    }
    ```

1. [**実行**]を開始するとStep Functionsが実行されます
1. 出力先のS3に"aws ssm describe-instance-patches"結果がJSONで保管されます

# EventBridgeによる定期実施
作成したStep FunctionsをEventBridge Schedulerから定期実行します。

1. Amazon EventBridge画面のスケジューラから、[**スケジュールを作成**]を選択します
1. [**スケジュール名**]を入力し、[**スケジュールグループ**]を選択します
1. スケジュールパターンとして、[**定期的なスケジュール**]を選択しタイムゾーンを指定します
1. 週一回実行するため、[**rateベースのスケジュール**]を選択し、rateとして "7 days"を指定します
1. フレックスタイムウィンドウを指定します
1. 時間枠については特に指定なしとします
1. ターゲットの選択で[**Step Functions**]を選択します
1. ステートマシン欄で作成した[**ステートマシン**]を選択し、入力に保存先バケットの情報を入力します

    ```
    # 入力パラメータ： 保管先のS3情報を入力値として登録
    {
      "s3_bucket": "[バケット名]/",
      "s3_path": "[パス]"
    }
    ```

1. スケジュールの状態でスケジュールを[**有効化**]にします
1.  スケジュール完了後のアクションは[**NONE**]を選択します
1.  再試行ポリシーとデッドレターキューについて、今回はなしを選択します
1.  暗号化についてデフォルトの暗号化を利用するためチェックなし
1.  アクセス許可では、[**このスケジュールの新しいロールを作成**]を選択します
1.  スケジュールを作成し、Step Functionsが自動で実行されていることを確認します

# 出力結果の確認
S3に "aws ssm describe-instance-patches" 結果のJSONが保存されます。

```json
◆結果のサンプル◆
{
    "Patches": [
        {
            "Classification": "Security",
            "InstalledTime": "yyyy-mm-ddThh:mm:ssZ",
            "KbId": "〇〇〇",
            "Severity": "required",
            "State": "Installed",
            "Title": "〇〇〇"
        },
        {
            "Classification": "Security",
            "InstalledTime": "yyyy-mm-ddThh:mm:ssZ",
            "KbId": "〇〇〇",
            "Severity": "Critical",
            "State": "Installed",
            "Title": "〇〇〇"
        },
        {
            "Classification": "Security",
            "InstalledTime": "yyyy-mm-ddThh:mm:ssZ",
            "KbId": "〇〇〇",
            "Severity": "optional",
            "State": "Missing",
            "Title": "〇〇〇"
        }
    ]
}
```

内容としては下記となります。

|項目|内容|
|:---|:--|
|Title|タイトル|
|Classification|SecurityUpdates、Updatesなどのパッチの分類|
|Severity|Critical、Importantなどのパッチの重大度|
|State|状態<br>「INSTALLED」→ インストール済み<br>「MISSING」→ 未インストール <br>「FAILED」→ 失敗     など|
|InstalledTime|インストール時間|

# 次回
Step Functionsを利用してコマンドの実行結果をS3に保管しました。コマンドを別のものにすれば色々な情報を定期取得したりする仕組みが簡単に実装できます。

S3にログとして保管しましたが、JSON形式であり複数ファイルに分割されているので、次回は見やすい形式に変換したいと思います。

#### 修正
- S3への出力フォーマットを修正するため、Step Functionsのコードを変更