マルチアカウント構成でのSSM Inventoryの活用方法と課題

## はじめに
あまり活用している情報をきいたことがないSystems Manager Inventoryについて、機能紹介から応用的な活用方法まで紹介したいと思います。

# Systems Manager Inventoryとは
SSM Inventoryは、SSM AgentをインストールしたEC2インスタンスやオンプレのサーバからメタデータを集約し、可視化することができるサービスです。

![{D5FBD97F-A560-4680-9B00-A48B808CBAFD}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/8428a548-45e7-4ecb-87be-f56098dce8af.png)


デフォルトでは以下のデータを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/64f2b4da-d07c-4215-ad48-965598914fc4.png)

ユーザ側でカスタムインベントリを取得することでさらに追加の情報を取得することもできます。
取得可能な情報としては、パッケージで管理されていないミドルウェア（MySQL、PostgreなどのDBエンジンのバージョン）や、Log4jなどオープンソースのAPIの利用有無、OS上のファイル情報等を収集することができます。

## SSM Inventoryの構成
今回、マルチアカウントでSSM Inventoryを活用し、中央アカウントへインベントリを集約する構成をとりました。

![{01C787D7-2CFF-42D7-B2C0-03853161B863}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/6b0450fd-0ced-4335-b8e7-14a5a36218e0.png)

構築手順については、以下のブログを参照ください。

https://qiita.com/kuma_matsu/items/6c6ce6902674151bc26a

## 実際に活用してわかったメリット
### 組織のサーバ群を簡単に管理できる
複数のアカウントからインベントリを集約することができるため、ミドルウェアやソフトウェアに脆弱性が発見された際に組織として迅速な対応をとることができます。

実際に脆弱性情報を受け取った際は、SSM Inventoryから該当のミドルウェア・ソフトウェアを検索し、利用中のサーバを特定し、管理者へ即座に連絡しましょう。

### ミドルウェア・ソフトウェアのバージョンを棚卸できる
インベントリにはミドルウェア・ソフトウェアのバージョンが記載されています。
それぞれのバージョンをSSM Inventoryから確認することで、ソフトウェアの互換性を確保し、バグや脆弱性が含まれる古いバージョンの使用を回避することで、安全で最適な環境を維持できます。

また、アップデートの計画を立てやすくなり、適切なタイミングで新機能の導入やパフォーマンス向上を図ることが可能になります。

## SSM Inventoryを運用する上での課題

### ① インベントリを取得可能なミドルウェア・ソフトウェアの条件
パッケージ管理システムまたはインストーラでインストールしたソフトウェアのみSSM Inventoryで集約できます。

そのため、以下のような場合にはミドルウェア・ソフトウェアのインベントリを収集できません。
・ファイル群を直接ディレクトリに配置した場合
・ソースコードから直接インストールした場合
・ファイル群を直接ディレクトリに配置した場合

### ② カスタムインベントリの取得難易度
カスタムインベントリを取得するためには、ユーザがShellスクリプトやPowerShellスクリプトを実行する必要があります。収集する項目ごとにスクリプトを作成する必要があり、導入工数が高くなります。

また、マルチアカウントの構成では、収集元と収集先で同じスクリプトを利用してカスタムインベントリを取得しないと、中央アカウントのダッシュボードに表示されないという課題もあります。
新たにカスタムインベントリを追加する場合や、ミドルウェアの仕様変更の場合に、スクリプトの修正作業を各アカウントで実施すると工数が高くなってしまいます。

ただ、カスタムインベントリはユーザ側で柔軟に取得できるため、収集が必要な項目がある場合やアカウント数が少ない場合に導入を検討してみるといいと思います。

### ③ SSM Inventoryのダッシュボードの検索性
**課題**
SSM Inventoryでは、検索フィルタとしてリージョンとAWSアカウントIDしか使用できないため、大量のデータを効率的に絞り込むのが難しいという課題があります。

![{1F9F81D9-AD95-4472-87FA-F5FE720D2515}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/05fa0b81-fad2-4e45-908c-8f6f700d31fa.png)

**対策**
対策として、インベントリデータをCSVにエクスポートするか、「Run Advanced Queries」からAthenaでクエリを実行することを推奨します。

![{FD8474EA-6DB2-46A9-A21F-4C12D2A40DC6}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/6f453212-4f3d-496a-bc7b-b5377b2f2673.png)


また、中央アカウント以外のAWSアカウントでは、SSM Fleet Managerを利用することでサーバ1台ごとにインベントリを確認することができます。
（Fleet Managerのインベントリ検索機能は充実していて便利です。）

![{BBD1421F-F5EC-4479-96FD-9D5BE3E2CED4}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/f5833f16-13a7-4d16-97b2-1e3791ed27c9.png)


### ④ 管理するインベントリの正確性
**課題**
インベントリの管理について、削除されたEC2のインベントリがSSM Inventoryのダッシュボードに表示され続けるという課題があります。
そのため、脆弱性が発見されたソフトウェアをインストールしているEC2を発見したとしても、それが起動されているのか、停止中のものなのか、はたまた削除されたものなのかの判断がつきません。

原因はインベントリを集約する構成にあります。
SSM Inventoryのダッシュボードは、S3からGlueやAthenaを利用してデータを表示しているため、EC2が削除されたところで、S3に保存されているデータも一緒に削除されるわけではなく、残り続けてしまいます。

![{CE005666-56EC-4CF0-BB27-07B8B6759775}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/5965ee3c-6c9f-4c96-9624-8b64534ba2d1.png)

**対策**
対策として、EC2が削除された際にS3バケットを自動で削除する仕組みを取り入れます。
筆者は以下の構成を取り入れてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/80cceee0-618f-415c-a37c-0c7d02948d17.png)

**◯　Lambdaの設定**
インベントリ収集先となる中央アカウントで、EventBridgeの起動に基づき、削除されたEC2インスタンスに関するオブジェクトを削除するLambda関数を作成します。

SSM Inventoryから集約されたインベントリは、デフォルトでは「s3://バケット名/AWS:インベントリ項目/accountid=アカウントID/region=リージョン/resourcetype=ManagedInstanceInventory/EC2インスタンスID.json」という形でS3に配置されます。
複数のオブジェクトを削除する必要があることに注意が必要です。

また、削除したEC2インスタンスのみのオブジェクトを削除するために、EventBridgeからインスタンスIDを受け取り、設定したプレフィックスと合わせS3オブジェクトのキーを作成します。

なお、Lambdaの実行ロールにS3のオブジェクトを削除するための権限を付け忘れないよう注意してください。

```python:delete-inventory.py
import boto3
import logging
import json

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

# S3バケットとプレフィックスの設定    
    bucket = "[S3バケット名]"    
    prefixes = [
        "AWS:Application/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:BillingInfo/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:AWSComponent/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:ComplianceItem/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:ComplianceSummary/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:InstanceDetailedInformation/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:Network/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:Service/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:Tag/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:WindowsRole/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/",
        "AWS:WindowsUpdate/accountid=[アカウントID]/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/"
    ]
    
# インスタンスIDの取得   
    instance_id = event.get("detail", {}).get("instance-id")   

    if not instance_id:
        logger.error("Instance ID is missing from the event.")
        return {
            "statusCode": 400,
            "body": "Instance ID is missing from the event."
        }

# EC2インスタンスIDを元にS3オブジェクトのキーを作成(プレフィックスもつける)
    object_keys = [f"{prefix}{instance_id}.json" for prefix in prefixes]

# S3クライアントの作成
    s3_client = boto3.client('s3')

# 削除成功カウントの初期化
    deleted_count = 0

# S3からオブジェクトを削除
    for object_key in object_keys:
        try:
            s3_client.delete_object(Bucket=bucket, Key=object_key)
            deleted_count += 1
            logger.info(f"Deleted S3 object: s3://{bucket}/{object_key}")
        except Exception as e:
            logger.error(f"Error deleting S3 object {object_key}: {str(e)}")

# 削除したオブジェクトの情報をログに出力    
    return {
        "statusCode": 200,
        "body": f"Successfully deleted {deleted_count} objects."
    }
```
テストをしてみると実際に該当のEC2インスタンスのインベントリのみが削除されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/0bdbc50c-a97d-4aac-8450-06a4c8332ffe.png)


**◯　EventBridgeの設定**
インベントリ収集元アカウントで、EC2インスタンスの削除時にLambdaを実行するルールを作成します。

・ルールタイプ
「イベントパターンを持つルール」を選択します。

・イベントパターン
以下を設定します。

|設定項目  |設定値  |
|---|---|
|作成のメソッド　|パターンフォームを使用する |
|イベントソース|AWSのサービス|
|AWSのサービス|EC2|
|イベントタイプ|EC2 Instance State-change Notification|
|イベントタイプの仕様|特定の状態 → terminated|
|イベントタイプの仕様2|「任意のインスタンス」|


![{A75F5F07-0117-4532-9D04-0A2E9B363385}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/ad702728-3ef1-4fab-a01b-ab8dfb6005ac.png)

・ターゲットを選択
以下を設定します。
権限については特に問題がなければ新しいロールを作成ください。

|設定項目  |設定値  |
|---|---|
|ターゲットタイプ　|AWSのサービス |
|ターゲットを選択|Lambda関数|
|ターゲットの場所|別のAWSアカウントのターゲット|
|関数|中央アカウントで作成したLambdaのARN|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115411/368729e6-999c-4c30-b3bc-0e067bf21cdd.png)

これで設定は完了です。
無事に終了したEC2のインベントリを消すことができました。