Route 53 Resolver DNSクエリログ(S3)を DuckDB で分析
VPC内で発生したDNSクエリ(およびその応答)を確認するには Route 53 の Resolver DNSクエリログが役に立ちます。
DNSクエリログの送信先として、 CloudWatch Logs ロググループ、 S3バケット、Kinesis Data Firehose 配信ストリームの3つから選べます。 安価にバッチ処理分析基盤を作りたい場合はS3が適しています。
今回は S3に保存している環境で、 DNSクエリログを DuckDB で分析してみます 。 DuckDBはオンライン分析処理(OLAP)に特化したオープンソースのデータベースシステムです。
セットアップ
Resolver DNSクエリログ
DNSクエリログ出力環境は事前にセットアップしている前提で進めます。 以下のように「S3バケットにクエリログを出力する設定」を入れています。
aws route53resolver list-resolver-query-log-configs --output yaml
# ResolverQueryLogConfigs:
# - Arn: arn:aws:route53resolver:ap-northeast-1:111111111111:resolver-query-log-config/rqlc-6548example
# AssociationCount: 1
# CreationTime: '2025-03-07T06:05:45.755793961Z'
# CreatorRequestId: '17411111111111'
# DestinationArn: arn:aws:s3:::EXAMPLE-DOC-BUCKET
# Id: rqlc-6548example
# Name: example-resolver-log
# OwnerId: '111111111111'
# ShareStatus: NOT_SHARED
# Status: CREATED
# TotalCount: 1
# TotalFilteredCount: 1
aws route53resolver list-resolver-query-log-config-associations
# ResolverQueryLogConfigAssociations:
# - CreationTime: '2025-03-07T06:05:46.645329988Z'
# Error: NONE
# ErrorMessage: ''
# Id: rqlca-1e1eexample
# ResolverQueryLogConfigId: rqlc-6548example
# ResourceId: vpc-0243example
# Status: ACTIVE
# TotalCount: 1
# TotalFilteredCount: 1
ログは EXAMPLE-DOC-BUCKET/AWSLogs/111111111111/vpcdnsquerylogs/vpc-0243example/YYYY/MM/DD/
配下に gz 圧縮されて格納されています。
aws s3 ls EXAMPLE-DOC-BUCKET/AWSLogs/111111111111/vpcdnsquerylogs/vpc-0243example/2025/03/10/ | head
# 2025-03-10 09:03:36 27685 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0000Z_9a091111.log.gz
# 2025-03-10 09:08:36 12669 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0000Z_c02a1111.log.gz
# 2025-03-10 09:13:36 14542 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0005Z_dce21111.log.gz
# 2025-03-10 09:08:36 26241 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0005Z_df331111.log.gz
# 2025-03-10 09:13:36 26573 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0010Z_07c11111.log.gz
# 2025-03-10 09:18:37 15084 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0010Z_3af31111.log.gz
# 2025-03-10 09:23:37 13881 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0015Z_59cd1111.log.gz
# 2025-03-10 09:18:36 27553 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0015Z_63721111.log.gz
# 2025-03-10 09:23:37 28174 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0020Z_6bfb1111.log.gz
# 2025-03-10 09:28:36 14041 vpc-02439example_vpcdnsquerylogs_111111111111_20250310T0020Z_b3fa1111.log.gz
gzip -d vpc-0243...略...log.gz
head -n2 vpc-0243...略...log
# {"version":"1.100000","account_id":"111111111111","region":"ap-northeast-1","vpc_id":"vpc-0243example","query_timestamp":"2025-03-10T00:15:00Z","query_name":"xxx.example.com.","query_type":"A","query_class":"IN","rcode":"NOERROR","answers":[{"Rdata":"203.0.113.1","Type":"A","Class":"IN"},{"Rdata":"203.0.113.2","Type":"A","Class":"IN"}],"srcaddr":"192.0.2.1","srcport":"11800","transport":"UDP","srcids":{"instance":"i-0d7cexample"}}
# {"version":"1.100000","account_id":"111111111111","region":"ap-northeast-1","vpc_id":"vpc-0243example","query_timestamp":"2025-03-10T00:15:00Z","query_name":"xxx.example.com.","query_type":"A","query_class":"IN","rcode":"NOERROR","answers":[{"Rdata":"203.0.113.1","Type":"A","Class":"IN"},{"Rdata":"203.0.113.2","Type":"A","Class":"IN"}],"srcaddr":"192.0.2.2","srcport":"11800","transport":"UDP","srcids":{"instance":"i-30ajexample"}}
head -n1 vpc-0243...略...log | jq
# {
# "version": "1.100000",
# "account_id": "111111111111",
# "region": "ap-northeast-1",
# "vpc_id": "vpc-0243example",
# "query_timestamp": "2025-03-10T00:15:00Z",
# "query_name": "xxx.example.com.",
# "query_type": "A",
# "query_class": "IN",
# "rcode": "NOERROR",
# "answers": [
# {
# "Rdata": "203.0.113.1",
# "Type": "A",
# "Class": "IN"
# },
# {
# "Rdata": "203.0.113.2",
# "Type": "A",
# "Class": "IN"
# }
# ],
# "srcaddr": "192.0.2.1",
# "srcport": "11800",
# "transport": "UDP",
# "srcids": {
# "instance": "i-0d7cexample"
# }
# }
DuckDBによる分析準備
以下ガイドのとおりに、 DuckDBをローカルにインストールします。
duckdb --version
# v1.2.1 8e52ec4395
duckdb
# v1.2.1 8e52ec4395
# Enter ".help" for usage hints.
# Connected to a transient in-memory database.
# Use ".open FILENAME" to reopen on a persistent database.
# D
以下コマンドで、特定時間(例: 2025/03/10 11時台(UTC)
)のクエリログを Parquet形式に変換してローカルに保存します。
---- S3連携の設定
INSTALL httpfs;
LOAD httpfs;
CREATE SECRET secret (
TYPE S3,
PROVIDER CREDENTIAL_CHAIN
);
-- ┌─────────┐
-- │ Success │
-- │ boolean │
-- ├─────────┤
-- │ true │
-- └─────────┘
---- Parquet形式に変換してローカルに保存
COPY (
SELECT *
FROM read_json('s3://EXAMPLE-DOC-BUCKET/AWSLogs/111111111111/vpcdnsquerylogs/vpc-0243example/2025/03/10/vpc-0243example_vpcdnsquerylogs_111111111111_20250310T11*.log.gz')
) TO 'querylogs.parquet' (FORMAT PARQUET);
-- 100% ▕████████████████████████████████████████████████████████████▏
---- 確認
select * from 'querylogs.parquet' limit 10;
-- ┌──────────┬──────────────┬────────────────┬───┬─────────┬───────────┬──────────────────────┐
-- │ version │ account_id │ region │ … │ srcport │ transport │ srcids │
-- │ varchar │ varchar │ varchar │ │ varchar │ varchar │ struct(instance va… │
-- ├──────────┼──────────────┼────────────────┼───┼─────────┼───────────┼──────────────────────┤
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 55123 │ UDP │ {'instance': i-0512… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 60123 │ UDP │ {'instance': i-0c12… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 39123 │ UDP │ {'instance': i-0612… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 44123 │ UDP │ {'instance': i-0012… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 34123 │ UDP │ {'instance': i-0512… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 37123 │ UDP │ {'instance': i-0712… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 55123 │ UDP │ {'instance': i-0512… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 46123 │ UDP │ {'instance': i-0d12… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 42123 │ UDP │ {'instance': i-0b12… │
-- │ 1.100000 │ 111111111111 │ ap-northeast-1 │ … │ 59123 │ UDP │ {'instance': i-0512… │
-- ├──────────┴──────────────┴────────────────┴───┴─────────┴───────────┴──────────────────────┤
-- │ 10 rows 14 columns (6 shown) │
-- └───────────────────────────────────────────────────────────────────────────────────────────┘
DNSクエリログを分析する
列情報を確認する
クエリログの列情報を取得します。
-- デフォルト出力(duckbox)だと ウィンドウ幅次第で column_type が省略されるので、markdownテーブルで出力
.mode markdown
DESCRIBE 'querylogs.parquet';
| column_name | column_type | null | key | default | extra |
|-----------------|----------------------------------------------------------|------|------|---------|-------|
| version | VARCHAR | YES | NULL | NULL | NULL |
| account_id | VARCHAR | YES | NULL | NULL | NULL |
| region | VARCHAR | YES | NULL | NULL | NULL |
| vpc_id | VARCHAR | YES | NULL | NULL | NULL |
| query_timestamp | TIMESTAMP | YES | NULL | NULL | NULL |
| query_name | VARCHAR | YES | NULL | NULL | NULL |
| query_type | VARCHAR | YES | NULL | NULL | NULL |
| query_class | VARCHAR | YES | NULL | NULL | NULL |
| rcode | VARCHAR | YES | NULL | NULL | NULL |
| answers | STRUCT(Rdata VARCHAR, "Type" VARCHAR, "Class" VARCHAR)[] | YES | NULL | NULL | NULL |
| srcaddr | VARCHAR | YES | NULL | NULL | NULL |
| srcport | VARCHAR | YES | NULL | NULL | NULL |
| transport | VARCHAR | YES | NULL | NULL | NULL |
| srcids | STRUCT(instance VARCHAR) | YES | NULL | NULL | NULL |
それぞれの列の内容については公式ドキュメントを参照ください。
1レコードのみ取得する
1レコードのみ確認してみます。
.mode line
SELECT * FROM 'querylogs.parquet' LIMIT 1;
version = 1.100000
account_id = 111111111111
region = ap-northeast-1
vpc_id = vpc-0243example
query_timestamp = 2025-03-10 11:00:00
query_name = xxx.example.com
query_type = A
query_class = IN
rcode = NOERROR
answers = [{'Rdata': 203.0.113.1, 'Type': A, 'Class': IN}, {'Rdata': 203.0.113.2, 'Type': A, 'Class': IN}, {'Rdata': 203.0.113.3, 'Type': A, 'Class': IN}]
srcaddr = 192.0.2.1
srcport = 55123
transport = UDP
srcids = {'instance': i-0d5eexample}
answersやsrcidsは構造体です。 以下のように要素を抽出できます。
SELECT
json_extract(answers, '$[*].Rdata') as Rdata,
srcids.instance
FROM 'querylogs.parquet' LIMIT 1;
-- Rdata = ["203.0.113.1", "203.0.113.2", "203.0.113.3"]
-- instance = i-0d5eexample
多くクエリされているドメインを取得する
上位10項目を取得します。
SELECT
query_name,
COUNT(*) as query_count
FROM 'querylogs.parquet'
GROUP BY query_name
ORDER BY query_count DESC
LIMIT 10;
┌────────────────────────────────────────┬─────────────┐
│ query_name │ query_count │
│ varchar │ int64 │
├────────────────────────────────────────┼─────────────┤
│ aaa.example.com. │ 24578 │
│ bbb.example.com. │ 8765 │
│ sqs.ap-northeast-1.amazonaws.com. │ 5643 │
│ config.example.com. │ 4219 │
│ api.example.com. │ 3876 │
│ item.example.com. │ 2543 │
│ order.example.com. │ 1987 │
│ firehose.ap-northeast-1.amazonaws.com. │ 1543 │
│ ccc.example.com. │ 1234 │
│ logs.ap-northeast-1.amazonaws.com. │ 1098 │
├────────────────────────────────────────┴─────────────┤
│ 10 rows 2 columns │
└──────────────────────────────────────────────────────┘
クエリ応答に特定IPアドレスが含まれているものを取得する
SELECT
query_name,
COUNT(*) as query_count
FROM 'querylogs.parquet'
WHERE
CAST(answers AS VARCHAR) LIKE '%203.0.113.1%'
GROUP BY query_name
ORDER BY query_count DESC
LIMIT 10;
┌───────────────────┬─────────────┐
│ query_name │ query_count │
│ varchar │ int64 │
├───────────────────┼─────────────┤
│ item.example.com │ 1048 │
│ order.example.com │ 953 │
│ api.example.com │ 818 │
│ ccc.example.com │ 567 │
└───────────────────┴─────────────┘
特定インスタンスがクエリしたものを取得する
SELECT
query_name,
COUNT(*) as query_count
FROM 'querylogs.parquet'
WHERE
srcids.instance = 'i-0d5eexample'
GROUP BY query_name
ORDER BY query_count DESC
LIMIT 5;
┌────────────────────────────────────────┬─────────────┐
│ query_name │ query_count │
│ varchar │ int64 │
├────────────────────────────────────────┼─────────────┤
│ config.example.com. │ 118 │
│ order.example.com. │ 102 │
│ item.example.com. │ 101 │
│ firehose.ap-northeast-1.amazonaws.com. │ 82 │
│ ccc.example.com. │ 67 │
└────────────────────────────────────────┴─────────────┘
応答無しのクエリログを取得する
SELECT
query_timestamp,
srcids.instance as instance,
query_name,
rcode,
len(answers)
FROM 'querylogs.parquet'
WHERE len(answers) = 0
LIMIT 10;
┌─────────────────────┬───────────────┬──────────────────────────────────────────────────────┬─────────┬──────────────┐
│ query_timestamp │ instance │ query_name │ rcode │ len(answers) │
│ timestamp │ varchar │ varchar │ varchar │ int64 │
├─────────────────────┼───────────────┼──────────────────────────────────────────────────────┼─────────┼──────────────┤
│ 2025-03-10 11:00:02 │ i-097fexample │ example.aaaaaa.clustercfg.apne1.cache.amazonaws.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:00 │ i-0d5eexample │ order.example.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:01 │ i-0439example │ example.aaaaaa.clustercfg.apne1.cache.amazonaws.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:03 │ i-09faexample │ example.aaaaaa.clustercfg.apne1.cache.amazonaws.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:01 │ i-09f1example │ query.example.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:04 │ i-0a85example │ example.aaaaaa.clustercfg.apne1.cache.amazonaws.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:02 │ i-0383example │ order.example.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:02 │ i-0659example │ example.aaaaaa.clustercfg.apne1.cache.amazonaws.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:06 │ i-0796example │ firehose.ap-northeast-1.amazonaws.com. │ NOERROR │ 0 │
│ 2025-03-10 11:00:03 │ i-0f6fexample │ ssm.ap-northeast-1.amazonaws.com. │ NOERROR │ 0 │
├─────────────────────┴───────────────┴──────────────────────────────────────────────────────┴─────────┴──────────────┤
│ 10 rows 5 columns │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
おわりに
Route 53 Resolver DNSクエリログを DuckDB でサクッと分析してみました。
余談ですが 2025/03/12 に新機能「DuckDB Local UI」が出ました。 こちら、とても良いです。
ローカル上で実行したSQLとその結果をノートブックとして確認、整理できます。 気軽にSQLを修正、再実行できます。めっちゃいい体験です。
以上、参考になれば幸いです。
参考