AWS Organizations環境でなるべく楽にSSMインベントリデータを集約したい

2023.01.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Systems Manager(SSM)インベントリ はSSM管理下のインスタンスの棚卸しに役立つ機能です。 インスタンスからメタデータを収集して、それらの情報をコンソールから確認できます。 OSやアプリケーション、ネットワーク設定などの情報を収集してくれます。 ※なお、これらの情報はメタデータのみです。機密情報や実データへのアクセスは無いです。

また、 リソースデータの同期 機能を使って、収集したメタデータ(インベントリデータ)を S3バケットへ送信できます。 S3バケットのインベントリデータに対してAmazon AthenaやQuickSight を使って分析ができます。

リソースデータの同期はマルチアカウント対応です。 適切なバケットポリシーを設定することで 1バケットに他アカウントからのインベントリデータを集約できます。 AWS Organizationsにも対応しています。

今回はAWS Organizations環境下で「SSMインベントリデータのS3バケット集約」を、なるべく楽にセットアップすべく、トライしてみました。

セットアップ全体像

img

インベントリの集約〜活用までの流れは以下のとおりです。

  1. 集約アカウントにS3バケットを作成する
  2. 「インベントリ収集」設定をメンバーアカウント群へ展開する
  3. 「組織ベースのリソースデータ同期」設定をメンバーアカウント群へ展開する
  4. (オプション) Athenaなど分析ツールを使ってS3バケットに対してクエリを行う

今回は 1.〜3. までのセットアップ部分が解説範囲です。

セットアップしてみる

(前提) 今回のセットアップ範囲は以下とします。

  • 東京リージョンのみ
  • 特定OU配下にあるメンバーアカウント群

S3バケット作成〜バケットポリシー 設定

img

上記S3バケットを作ります。 まずはマネジメントコンソールから、基本デフォルト設定でS3バケットを作成しました。

img

作成したS3バケットのバケットポリシーを編集します。 以降に示すサンプルポリシーを適宜置き換えます。

  • DOC-EXAMPLE-BUCKET を作成したバケット名に置換
  • bucket-prefix/ をバケットプレフィクスに置換 (オプション)
  • organization-id をAWS Organizationsの組織IDに置換
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SSMBucketPermissionsCheck",
      "Effect": "Allow",
      "Principal": {
        "Service": "ssm.amazonaws.com"
      },
      "Action": "s3:GetBucketAcl",
      "Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET"
    },
    {
      "Sid": " SSMBucketDelivery",
      "Effect": "Allow",
      "Principal": {
        "Service": "ssm.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": [
        "arn:aws:s3:::DOC-EXAMPLE-BUCKET/bucket-prefix/*/accountid=*/*"
      ],
      "Condition": {
        "StringEquals": {
          "s3:x-amz-acl": "bucket-owner-full-control",
          "s3:RequestObjectTag/OrgId": "organization-id"
        }
      }
    },
    {
      "Sid": " SSMBucketDeliveryTagging",
      "Effect": "Allow",
      "Principal": {
        "Service": "ssm.amazonaws.com"
      },
      "Action": "s3:PutObjectTagging",
      "Resource": [
        "arn:aws:s3:::DOC-EXAMPLE-BUCKET/bucket-prefix/*/accountid=*/*"
      ]
    }
  ]
}

今回は bucket-prefix/default/ としました。

※「AWS Organizationsの組織ID」は管理アカウントのOrganizationsコンソールから確認できます。

img

SSMインベントリ収集 設定

img

上記「インベントリ収集」設定をしていきます。 この設定には、 SSM 高速セットアップ が便利なので使っていきます。 高速セットアップはSSM周辺の設定を簡単に適用できる機能です。

※この設定は AWS Organizations の管理アカウント 上で行います。

マネジメントコンソールから [高速セットアップ] を選択して、 Host Management [作成] を選択します。

img

以降でオプションを指定していきます。

設定オプションでは最低限「インベントリ収集」設定はONにします。 その他は用途に合わせて、ついでにONにしても(しなくても)構いません。

img

次にターゲットを選択します。 今回は東京リージョンのみの前提なので「カスタム」を選択します。 ターゲットのOU、リージョンを指定しましょう。

img

インスタンスプロファイルのオプションはONにしても(しなくても)構いません。

img

問題なければ [作成] を選択しましょう。

img

これで「インベントリ収集」設定は完了です。

リソースデータ同期 設定

img

最後に「リソースデータ同期」設定を行います。

これは高速セットアップでは設定できません。 なおかつ今(2023/01)時点では、「組織ベースのリソースデータ同期」は CloudFormationのResourceDataSyncリソースでは作成できません。

なので、この設定は CLI(SDK)で実施する必要があります。

全アカウントに同じ設定を投入することを考えて、今回は Lambda-backed カスタムリソース を活用します。 これは簡潔に言うと、CloudFormation展開時に 指定したLambda関数を実行できるリソース(機能)です。 ※詳細は「参考」章の記載リンクをご覧ください。

以下のようなCloudFormationテンプレートを作成しました。 「組織ベースのリソースデータ同期」を作成する Lambda-backed カスタムリソースを記述しています。

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  BucketName:
    Type: String
  BucketPrefix:
    Type: String
    Default: default
  SyncName:
    Type: String
    Default: org-sync
  RoleName:
    Type: String
    Default: LambdaSetupRole-SSMResourceDataSync

Resources:
  SSMResourceDataSync:
    Type: Custom::SSMResourceDataSync
    Properties:
      ServiceToken: !GetAtt "LambdaFunction.Arn"
      BucketName: !Ref BucketName
      BucketPrefix: !Ref BucketPrefix
      SyncName: !Ref SyncName

  ### Lambda function and role for Create/Delete ResourceDataSync
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt "LambdaExecutionRole.Arn"
      Runtime: "python3.9"
      Handler: index.lambda_handler
      Timeout: 60
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          from logging import getLogger, INFO, log

          logger = getLogger()
          logger.setLevel(INFO)

          def lambda_handler(event, context):
              logger.info('[START] lambda_handler')
              bucket_name = event['ResourceProperties']['BucketName']
              bucket_prefix = event['ResourceProperties']['BucketPrefix']
              sync_name = event['ResourceProperties']['SyncName']
              logger.info(f'BucketName:{bucket_name}')
              logger.info(f'BucketPrefix:{bucket_prefix}')
              logger.info(f'SyncName:{sync_name}')
              if event['RequestType'] == 'Create':
                  logger.info('running ssm.create_resource_data_sync')
                  try:
                      ssm  = boto3.client('ssm')
                      ssm.create_resource_data_sync(
                          SyncName=sync_name,
                          S3Destination={
                              'BucketName': bucket_name,
                              'Prefix': bucket_prefix,
                              'SyncFormat': 'JsonSerDe',
                              'Region': 'ap-northeast-1',
                              'DestinationDataSharing': {
                                  'DestinationDataSharingType': 'Organization'
                              }
                          }
                      )
                  except Exception as e:
                      logger.error(e)
                      cfnresponse.send(event, context, cfnresponse.FAILED,
                                       {'Response': 'Failure'})
                      exit()
                  else:
                      cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                       {'Response': 'Success'})
              if event['RequestType'] == 'Update':
                  pass
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                   {'Response': 'Success'})
              if event['RequestType'] == 'Delete':
                  logger.info('running ssm.delete_resource_data_sync')
                  try:
                      ssm  = boto3.client('ssm')
                      ssm.delete_resource_data_sync(
                          SyncName=sync_name
                      )
                  except Exception as e:
                      logger.error(e)
                      cfnresponse.send(event, context, cfnresponse.FAILED,
                                       {'Response': 'Failure'})
                      exit()
                  else:
                      cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                       {'Response': 'Success'})
              logger.info('[END] lambda_handler')

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref RoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: "lambda.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        - PolicyName: SSMCreateResourceDataSync
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "ssm:CreateResourceDataSync"
                  - "ssm:DeleteResourceDataSync"
                Resource: "*"

管理アカウントもしくはCloudFormation委任先アカウントから、このテンプレートをStackSetsとして展開しましょう。 ※組織単位でStackSets を展開するための詳しい手順は省きます。「参考」章記載のリンクをご覧ください。

以下パラメータを指定します。

  • BucketName : (集約先S3バケットの名前)
  • BucketPrefix : (集約先S3バケットのプレフィクス)
  • SyncName : (リソースデータ同期の名前)
  • RoleName : (Lambda関数の実行ロール名)

高速セットアップで指定したOU、リージョンと同じターゲットを指定して、展開します。 スタックインスタンスのステータスが全て「SUCCEEDED」となればOKです。

確認

test.json

StackSets展開が成功している場合、S3バケットの (prefix)/AWS:InstanceInformation/accountid=(各アカウントID)/test.json というファイルができているはずです。念のため確認しておきましょう。

img

EC2インスタンスを作成してみる

メンバーアカウント上にSSM管理下のEC2インスタンスを1つ作成してみます。

集約S3バケットのあるアカウントへログインして確認すると、以下JSONが格納されていました。

(プレフィクス)/AWS:AWSComponent/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:Application/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:BillingInfo/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:ComplianceItem/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:ComplianceSummary/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:InstanceDetailedInformation/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:InstanceInformation/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:Network/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json
(プレフィクス)/AWS:Tag/accountid=(アカウントID)/region=ap-northeast-1/resourcetype=ManagedInstanceInventory/(インスタンスID).json

img

1つピックアップして見てみます。 以下キャプチャは AWS:Application のJSONです。 該当インスタンスにインストールされているアプリケーション情報が格納されています。

img

AWS:ApplicationのJSON中身(サンプル)

{
  "ApplicationType": "System Environment/Libraries",
  "InstalledTime": "2022-12-15T21:53:40Z",
  "Architecture": "x86_64",
  "Version": "1.0.4",
  "Summary": "String library, very low memory overhead, simple to import",
  "PackageId": "ustr-1.0.4-16.amzn2.0.3.src.rpm",
  "Publisher": "Amazon Linux",
  "Release": "16.amzn2.0.3",
  "URL": "http://www.and.org/ustr/",
  "Name": "ustr",
  "resourceId": "i-0ac46e13720305980",
  "captureTime": "2023-01-10T02:39:10Z",
  "schemaVersion": "1.1"
}
{
  "ApplicationType": "System Environment/Base",
  "InstalledTime": "2022-12-15T21:53:27Z",
  "Architecture": "x86_64",
  "Version": "2",
  "Epoch": "1",
  "Summary": "Amazon Linux release files",
  "PackageId": "system-release-2-14.amzn2.src.rpm",
  "Publisher": "Amazon Linux",
  "Release": "14.amzn2",
  "URL": "https://amazonlinux.com/",
  "Name": "system-release",
  "resourceId": "i-0ac46e13720305980",
  "captureTime": "2023-01-10T02:39:10Z",
  "schemaVersion": "1.1"
}
# (略)

それぞれの内容について詳細または分析の深堀りは本ブログでは割愛します。

補足や注意点

「組織ベースのリソースデータ同期」の作成制限について

「組織ベースのリソースデータ同期」は1つのみ作成できます(1リージョン ✕ 1アカウント単位)。 既に「組織ベースのリソースデータ同期」を作っている場合、 紹介したテンプレートは展開に失敗します。

You can only have one organization-based resource data sync.

Configuring resource data sync for Inventory - AWS Systems Manager

リソースデータ同期の特性について

これはOrganizations関係なく、リソースデータ同期の特性の話です。

以下のような挙動でS3へ同期されます。

  • マネージドノードを削除しても、削除されたノードのインベントリファイルは維持される
  • 実行中のノードが更新された場合、インベントリファイルは自動的に上書きされる

なので S3にあるインベントリデータ = 現在アクティブなインスタンス ではない ことに注意してください。

アクティブかどうかは AWS:InstanceInformation テーブルから確認できます。

おわりに

SSMインベントリデータのS3バケット集約を試してみました。

リソースデータ同期の部分はブログ投稿時点では少し工夫が必要ですが、 それ以外は比較的容易にセットアップできるかと思います。

実際に格納されているデータの内容や分析周りは以下公式ドキュメントが参考になります。 機会があれば実際に試してブログ化もしたいです。

以上、参考になれば幸いです。

参考