DuckDBでAWS Config導入時の費用を見積もる
要点
- AWS Configの事前見積はCloudTrailのログから行う
- DuckDBはS3のログを直接FROMに指定して分析できる
- DuckDBでログ分析することで、AthenaやCloudTrail Lakeなしで集計が完了した。
やりたいこと
要するにコレをLakeもAthenaも使わずlocalのDuckDBでやりたい。
もう少し具体的には
- あるAWSアカウントにAWS Configを導入する必要があり、事前に概算費用を見積もりたい
- Configの費用は対象リソース数に依存するが、CloudTrailのログから概算が出せるらしい(上のブログ)
- 長期間運用しているアカウントでありCloudTrailのログはあるが、LakeとAthenaは設定してない
- 単発作業のために準備するの手間だし、余計なリソース生まれて鬱陶しい
- localで動くツールでサクッとやりたい
AWS Configの見積
AWS Config では、定期的な記録と継続的な記録の 2 つの頻度で設定項目を配信できます。定期的な記録では、変更が発生した場合にのみ 24 時間に 1 回設定データが配信されるため、運用計画や監査などのユースケースに役立つ場合があります。連続的な記録では、変更が発生するたびに設定項目が配信されます。これはセキュリティとコンプライアンスの要件を満たし、すべての設定変更を追跡するのに役立ちます。
要するに、連続的な記録の場合は対象イベント数 * 0.003 USD になります。
(2025/04/30現在)
DuckDBについて
DB特性以上に便利な機能として、CSVやParquet, JSONなどのファイルを直接FROMに指定して分析することができます。
更に、 FROM 's3://foo-bucket/*/bar.json'
のように書くことでS3のファイルを直接取り込むことができます。
今回の用途ではDBの特性自体は重要ではなく、CLIツール的に使える特徴と上記のS3から直接取り込める機能が目的です。
実践
CloudTrailのログをS3から取り込む
S3のjson.gzからテーブルに読み込む部分のクエリはこちらの記事をそのまま使わせていただきました。
JSON形式のままテーブル化
CREATE TABLE ct_202503 AS
WITH raw_data AS (
SELECT *
FROM read_json(
's3://{your-bucket}/AWSLogs/{your-account}/CloudTrail/ap-northeast-1/2025/03/*/*.json.gz',
maximum_depth=2
)
)
SELECT unnest(Records) AS Event
FROM raw_data;
取込結果
SELECT * FROM ct_202503 LIMIT 10;
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ Event │
│ json │
├───────────────────────────────────────────────────────────────────────────────────────────┤
│ {"eventVersion":"1.08","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.10","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.10","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.09","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.10","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.09","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.08","userIdentity":{"type":"AWSService","invokedBy":"autoscaling.ama… │
│ {"eventVersion":"1.10","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.10","userIdentity":{"type":"AssumedRole","principalId":"xxx… │
│ {"eventVersion":"1.08","userIdentity":{"type":"AWSService","invokedBy":"AWS Internal"},… │
├───────────────────────────────────────────────────────────────────────────────────────────┤
│ 10 rows │
└───────────────────────────────────────────────────────────────────────────────────────────┘
JSONからテーブルの形に変換
CREATE TABLE ct_extracted_202503 AS
SELECT
-- 基本的なイベント情報
json_extract_string(Event, '$.eventVersion') AS eventVersion,
json_extract_string(Event, '$.eventTime') AS eventTime,
json_extract_string(Event, '$.eventSource') AS eventSource,
json_extract_string(Event, '$.eventName') AS eventName,
json_extract_string(Event, '$.awsRegion') AS awsRegion,
json_extract_string(Event, '$.sourceIPAddress') AS sourceIPAddress,
json_extract_string(Event, '$.userAgent') AS userAgent,
-- ユーザー情報
json_extract_string(Event, '$.userIdentity.type') AS userType,
json_extract_string(Event, '$.userIdentity.principalId') AS principalId,
json_extract_string(Event, '$.userIdentity.arn') AS userArn,
json_extract_string(Event, '$.userIdentity.accountId') AS accountId,
json_extract_string(Event, '$.userIdentity.accessKeyId') AS accessKeyId,
json_extract_string(Event, '$.userIdentity.userName') AS userName,
-- セッション情報
json_extract_string(Event, '$.userIdentity.sessionContext.attributes.creationDate') AS sessionCreationDate,
json_extract_string(Event, '$.userIdentity.sessionContext.attributes.mfaAuthenticated') AS mfaAuthenticated,
-- リクエストパラメータ(インスタンスIDセットの展開)
json_extract_string(json_extract(Event, '$.requestParameters.instancesSet.items[0]'), '$.instanceId') AS instanceId1,
json_extract_string(json_extract(Event, '$.requestParameters.instancesSet.items[1]'), '$.instanceId') AS instanceId2,
-- レスポンス要素(インスタンスの状態)
json_extract_string(json_extract(Event, '$.responseElements.instancesSet.items[0]'), '$.instanceId') AS responseInstanceId1,
json_extract_string(json_extract(Event, '$.responseElements.instancesSet.items[0]'), '$.currentState.name') AS responseCurrentState1,
json_extract_string(json_extract(Event, '$.responseElements.instancesSet.items[0]'), '$.previousState.name') AS responsePreviousState1,
json_extract_string(json_extract(Event, '$.responseElements.instancesSet.items[1]'), '$.instanceId') AS responseInstanceId2,
json_extract_string(json_extract(Event, '$.responseElements.instancesSet.items[1]'), '$.currentState.name') AS responseCurrentState2,
json_extract_string(json_extract(Event, '$.responseElements.instancesSet.items[1]'), '$.previousState.name') AS responsePreviousState2,
-- その他のフィールド
json_extract_string(Event, '$.requestID') AS requestID,
json_extract_string(Event, '$.eventID') AS eventID,
json_extract_string(Event, '$.readOnly') AS readOnly,
json_extract_string(Event, '$.eventType') AS eventType,
json_extract_string(Event, '$.managementEvent') AS managementEvent,
json_extract_string(Event, '$.recipientAccountId') AS recipientAccountId,
json_extract_string(Event, '$.eventCategory') AS eventCategory,
-- TLSの詳細
json_extract_string(Event, '$.tlsDetails.tlsVersion') AS tlsVersion,
json_extract_string(Event, '$.tlsDetails.cipherSuite') AS cipherSuite,
json_extract_string(Event, '$.tlsDetails.clientProvidedHostHeader') AS clientProvidedHostHeader
FROM ct_202503;
取込結果
SELECT * FROM ct_extracted_202503 LIMIT 10;
┌──────────────┬──────────────────────┬──────────────────────┬───┬───────────────┬────────────┬──────────────────────┬──────────────────────┐
│ eventVersion │ eventTime │ eventSource │ … │ eventCategory │ tlsVersion │ cipherSuite │ clientProvidedHost… │
│ varchar │ varchar │ varchar │ │ varchar │ varchar │ varchar │ varchar │
├──────────────┼──────────────────────┼──────────────────────┼───┼───────────────┼────────────┼──────────────────────┼──────────────────────┤
│ 1.09 │ 2025-02-28T23:53:53Z │ elasticloadbalanci… │ … │ Management │ NULL │ NULL │ NULL │
│ 1.10 │ 2025-02-28T23:54:01Z │ ec2.amazonaws.com │ … │ Management │ NULL │ NULL │ NULL │
│ 1.11 │ 2025-02-28T23:53:47Z │ logs.amazonaws.com │ … │ Management │ TLSv1.3 │ TLS_AES_128_GCM_SH… │ logs.ap-northeast-… │
│ 1.10 │ 2025-02-28T23:54:05Z │ ec2.amazonaws.com │ … │ Management │ NULL │ NULL │ NULL │
│ 1.09 │ 2025-02-28T23:54:01Z │ elasticloadbalanci… │ … │ Management │ NULL │ NULL │ NULL │
│ 1.11 │ 2025-02-28T23:54:04Z │ ssm.amazonaws.com │ … │ Management │ TLSv1.2 │ ECDHE-RSA-AES128-G… │ ssm.ap-northeast-1… │
│ 1.11 │ 2025-02-28T23:54:00Z │ ssm.amazonaws.com │ … │ Management │ TLSv1.2 │ ECDHE-RSA-AES128-G… │ ssm.ap-northeast-1… │
│ 1.09 │ 2025-02-28T23:54:05Z │ elasticloadbalanci… │ … │ Management │ NULL │ NULL │ NULL │
│ 1.08 │ 2025-02-28T23:53:43Z │ sts.amazonaws.com │ … │ Management │ NULL │ NULL │ NULL │
│ 1.11 │ 2025-02-28T23:54:09Z │ ssm.amazonaws.com │ … │ Management │ TLSv1.2 │ ECDHE-RSA-AES128-G… │ ssm.ap-northeast-1… │
├──────────────┴──────────────────────┴──────────────────────┴───┴───────────────┴────────────┴──────────────────────┴──────────────────────┤
│ 10 rows 33 columns (7 shown) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
対象のイベントを集計する
SQLの標準的な集計なので、公式ブログのクエリそのままでいけます。
SELECT
recipientAccountId,
awsRegion,
eventSource,
count(*) AS TotalPossibleCI
FROM
ct_extracted_202503
WHERE
(
eventSource LIKE 'eks%'
OR eventSource LIKE 'ec2%'
OR eventSource LIKE 'vpc%'
OR eventSource LIKE 'ecs%'
OR eventSource LIKE 'iam%'
OR eventSource LIKE 'autoscaling%'
OR eventSource LIKE 's3%'
OR eventSource LIKE 'rds%'
OR eventSource LIKE 'backup%'
OR eventSource LIKE 'athena%'
OR eventSource LIKE 'cloudtrail%'
OR eventSource LIKE 'cloudfront%'
OR eventSource LIKE 'cloudformation%'
OR eventSource LIKE 'code%'
OR eventSource LIKE 'ecr%'
OR eventSource LIKE 'lambda%'
OR eventSource LIKE 'efs%'
)
AND readOnly = 'false'
AND managementEvent = 'true'
AND eventTime >= '2025-03-01T00:00:00Z'
AND eventTime < '2025-04-01T00:00:00Z'
GROUP BY
recipientAccountId,
awsRegion,
eventSource
ORDER BY
recipientAccountId DESC,
TotalPossibleCI DESC;
┌────────────────────┬────────────────┬───────────────────────────┬─────────────────┐
│ recipientAccountId │ awsRegion │ eventSource │ TotalPossibleCI │
│ varchar │ varchar │ varchar │ int64 │
├────────────────────┼────────────────┼───────────────────────────┼─────────────────┤
│ {your-account} │ ap-northeast-1 │ ec2.amazonaws.com │ 19069 │
│ {your-account} │ ap-northeast-1 │ athena.amazonaws.com │ 2359 │
│ {your-account} │ ap-northeast-1 │ autoscaling.amazonaws.com │ 1446 │
│ {your-account} │ ap-northeast-1 │ codebuild.amazonaws.com │ 216 │
│ {your-account} │ ap-northeast-1 │ s3.amazonaws.com │ 186 │
└────────────────────┴────────────────┴───────────────────────────┴─────────────────┘
合計 23,276件となりました。
実際にはConfigの対象外のイベントソースがあるはずなので、件数が多いEC2だけ内訳も見てみます。
SELECT
recipientAccountId,
awsRegion,
eventSource,
eventName,
count(*) AS TotalPossibleCI
FROM
ct_extracted_202503
WHERE
eventSource LIKE 'ec2%'
AND readOnly = 'false'
AND managementEvent = 'true'
GROUP BY
recipientAccountId,
awsRegion,
eventSource,
eventName
ORDER BY
recipientAccountId DESC,
TotalPossibleCI DESC;
┌────────────────────┬────────────────┬───────────────────┬───────────────────────────────┬─────────────────┐
│ recipientAccountId │ awsRegion │ eventSource │ eventName │ TotalPossibleCI │
│ varchar │ varchar │ varchar │ varchar │ int64 │
├────────────────────┼────────────────┼───────────────────┼───────────────────────────────┼─────────────────┤
│ {your-account} │ ap-northeast-1 │ ec2.amazonaws.com │ CreateTags │ 12806 │
│ {your-account} │ ap-northeast-1 │ ec2.amazonaws.com │ TerminateInstances │ 1742 │
│ {your-account} │ ap-northeast-1 │ ec2.amazonaws.com │ RunInstances │ 1707 │
│ {your-account} │ ap-northeast-1 │ ec2.amazonaws.com │ SharedSnapshotVolumeCreated │ 1317 │
-- 以下省略
CreateTagsが大半を占めてますが、これはConfigの対象リソースではないはずです。
ということで、対象は23,276- 12,806 = 10,470件。
もっと削れるとは思いますが概算なのでこれでよしとします。
見積
連続的な記録の対象1件につき $0.003 なので
10,470 * 0.003 = 31.41 USD/月 = 4,476円/月 となりました。
(2025/4/30現在)
実績(WIP)
導入されて実績値が出たら追記します。たぶん。
まとめ
冒頭に書いた通り、DuckDBを使うことでCloudTrailのログを集計してAWS Configの概算見積ができました。
CloudTrailのログだけがあれば作業可能であり、AthenaやCloudTrail Lakeなどの準備や追加費用が不要な点は大きな利点です。
この記事がConfig導入を検討している方の一助になれば幸いです。
Discussion