
JSONata式でJSONデータを抽出加工できるCLIツールjfqを試してみた
こんにちは。サービス開発室の武田です。
AWS Step Functionsで使うためにJSONataを覚え始めましたが、やってみるとおもしろいですね。
AWS CLIのレスポンスの加工などにもJSONataを使えないかなということで、調べてみたらjfq
というツールが公開されていたので使ってみました。
インストール
READMEにも記載がありますが、npm
でインストールします。
npm install -g jfq
AWS CLIのレスポンスを加工してみよう
AWS CLIで取得する加工元のデータは、このエントリ最後に記載しているサンプルデータを使用しました。おそらく違和感のないデータとなっているはずですが、おかしいなと思ったらご一報ください。
1. EC2インスタンスID一覧を取得
一番シンプルな使い方で、目的のプロパティだけを取得します。
# jq
aws ec2 describe-instances | jq -r '.Reservations[].Instances[].InstanceId'
# JSONata
aws ec2 describe-instances | jfq 'Reservations.Instances.InstanceId'
結果。
i-0123456789abcdef0
i-0fedcba9876543210
i-0a1b2c3d4e5f6a7b8
2. EC2インスタンスのタグ「Name」を取得
プロパティの値を指定して絞り込みをします。個人的にJSONataの書式は結構好きです。
# jq
aws ec2 describe-instances | jq -r '.Reservations[].Instances[].Tags[] | select(.Key=="Name") | .Value'
# JSONata
aws ec2 describe-instances | jfq 'Reservations.Instances.Tags[Key="Name"].Value'
結果。
web-server-01
db-server-01
batch-worker
3. EC2インスタンスの状態(State)ごとにIDをグループ化
グルーピングという高度な操作をしています。jqはあれやこれやとこねくり回していますが、JSONataはスッキリ書けているのがすごい。
# jq
aws ec2 describe-instances | jq '[.Reservations[].Instances[] | {state: .State.Name, id: .InstanceId}] | group_by(.state) | map({(.[0].state): map(.id)}) | add'
# JSONata
aws ec2 describe-instances | jfq 'Reservations.Instances{State.Name: [InstanceId]}'
結果。
{
"running": [
"i-0123456789abcdef0",
"i-0a1b2c3d4e5f6a7b8"
],
"stopped": [
"i-0fedcba9876543210"
]
}
4. CloudWatchアラームの名前と状態をペアで取得
プロパティの値を文字列として連結する操作です。JSONataには文字列の埋め込み機能はありません。
# jq
aws cloudwatch describe-alarms | jq -r '.MetricAlarms[] | "\(.AlarmName): \(.StateValue)"'
# JSONata
aws cloudwatch describe-alarms | jfq 'MetricAlarms.(AlarmName & ": " & StateValue)'
結果。
HighCPUUtilization: ALARM
LowDiskSpace: OK
5. EC2インスタンスのパブリックIPアドレス一覧(存在するもののみ)
jqではnull
の値はnull
として扱うため、null値に対する対応は必要です。一方でJSONataは、ないものはスルーするため特別な対応が不要です。
# jq
aws ec2 describe-instances | jq -r '.Reservations[].Instances[] | select(.PublicIpAddress != null) | .PublicIpAddress'
# JSONata
aws ec2 describe-instances | jfq 'Reservations.Instances.PublicIpAddress'
結果。
203.0.113.10
198.51.100.25
6. EC2インスタンスのセキュリティグループID一覧を取得(インスタンスごとに配列で)
考え方としては3のグルーピング処理と同じです。jqではやはりエッジケースの対応が必要ですが、JSONataではシンプルに記述できています。なおJSONataのクエリで、[SecurityGroups.GroupId]
のブラケットはなくても動くのですが、結果が単一値だった場合、配列ではなく文字列値が直接入ります。ブラケットをつけることで、単一値でも配列になるため挙動が少し違うことに注意。
# jq
aws ec2 describe-instances | jq '[.Reservations[].Instances[] | {InstanceId, SecurityGroups: [(.SecurityGroups // [])[].GroupId]}]'
# JSONata
aws ec2 describe-instances | jfq 'Reservations.Instances.{"InstanceId": InstanceId, "SecurityGroups": [SecurityGroups.GroupId]}'
結果。
[
{
"InstanceId": "i-0123456789abcdef0",
"SecurityGroups": [
"sg-0a1b2c3d4e5f6a7b8",
"sg-1234567890abcdef0"
]
},
{
"InstanceId": "i-0fedcba9876543210",
"SecurityGroups": [
"sg-0fedcba9876543210"
]
},
{
"InstanceId": "i-0a1b2c3d4e5f6a7b8",
"SecurityGroups": []
}
]
7. CloudTrailイベントの中から特定ユーザーのイベントだけ抽出
複数の文字列比較をor
したいケースです。JSONataだとin
が使えるのでやや簡潔に書けています。もちろんJSONataでもor
を使って書いてもOKです。
# jq
aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=alice | jq '.Events[] | select(.EventName == "StartInstances" or .EventName == "StopInstances")'
# JSONata
aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=alice | jfq 'Events[EventName in ["StartInstances", "StopInstances"]]'
結果。
[
{
"EventId": "11111111-2222-3333-4444-555555555555",
"EventName": "StartInstances",
"EventSource": "ec2.amazonaws.com",
"Username": "alice",
"EventTime": "2024-06-01T10:00:00Z",
"Resources": [
{
"ResourceType": "AWS::EC2::Instance",
"ResourceName": "i-0123456789abcdef0"
}
],
"CloudTrailEvent": "{\"eventVersion\":\"1.08\",\"userIdentity\":{\"type\":\"IAMUser\",\"principalId\":\"AIDAEXAMPLE1\",\"arn\":\"arn:aws:iam::123456789012:user/alice\",\"accountId\":\"123456789012\",\"userName\":\"alice\"},\"eventTime\":\"2024-06-01T10:00:00Z\",\"eventSource\":\"ec2.amazonaws.com\",\"eventName\":\"StartInstances\",\"awsRegion\":\"ap-northeast-1\",\"sourceIPAddress\":\"203.0.113.5\",\"userAgent\":\"aws-cli/2.15.0\",\"requestParameters\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0123456789abcdef0\"}]}},\"responseElements\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0123456789abcdef0\",\"currentState\":{\"code\":0,\"name\":\"pending\"},\"previousState\":{\"code\":80,\"name\":\"stopped\"}}]}},\"requestID\":\"12345678-1234-1234-1234-123456789012\",\"eventID\":\"11111111-2222-3333-4444-555555555555\",\"readOnly\":false,\"eventType\":\"AwsApiCall\",\"managementEvent\":true,\"recipientAccountId\":\"123456789012\"}"
},
{
"EventId": "22222222-3333-4444-5555-666666666666",
"EventName": "StopInstances",
"EventSource": "ec2.amazonaws.com",
"Username": "alice",
"EventTime": "2024-06-01T11:15:00Z",
"Resources": [
{
"ResourceType": "AWS::EC2::Instance",
"ResourceName": "i-0fedcba9876543210"
}
],
"CloudTrailEvent": "{\"eventVersion\":\"1.08\",\"userIdentity\":{\"type\":\"IAMUser\",\"principalId\":\"AIDAEXAMPLE1\",\"arn\":\"arn:aws:iam::123456789012:user/alice\",\"accountId\":\"123456789012\",\"userName\":\"alice\"},\"eventTime\":\"2024-06-01T11:15:00Z\",\"eventSource\":\"ec2.amazonaws.com\",\"eventName\":\"StopInstances\",\"awsRegion\":\"ap-northeast-1\",\"sourceIPAddress\":\"203.0.113.5\",\"userAgent\":\"aws-cli/2.15.0\",\"requestParameters\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0fedcba9876543210\"}]}},\"responseElements\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0fedcba9876543210\",\"currentState\":{\"code\":64,\"name\":\"stopping\"},\"previousState\":{\"code\":16,\"name\":\"running\"}}]}},\"requestID\":\"87654321-4321-4321-4321-210987654321\",\"eventID\":\"22222222-3333-4444-5555-666666666666\",\"readOnly\":false,\"eventType\":\"AwsApiCall\",\"managementEvent\":true,\"recipientAccountId\":\"123456789012\"}"
}
]
8. IAMユーザーの作成日とユーザー名を一覧で取得
やっていることは実質4と同じですね。JSONataでは、文字列の連結は&
でもできますし、$join
でも可能です。
# jq
aws iam list-users | jq -r '.Users[] | "\(.UserName) created at \(.CreateDate)"'
# JSONata
aws iam list-users | jfq 'Users.$join([UserName, " created at ", CreateDate])'
結果。
alice created at 2023-01-15T08:30:00Z
bob created at 2022-12-20T14:45:00Z
9. VPCのタグ「Name」がある場合、そのNameとVPC IDを取得
絞り込み、nullチェック、オブジェクトの組み立てと、複雑なことをしています。
# jq
aws ec2 describe-vpcs | jq -r '.Vpcs[] | {VpcId, Name: (.Tags[]? | select(.Key=="Name") | .Value)} | select(.Name != null)'
#JSONata
aws ec2 describe-vpcs | jfq 'Vpcs.{"VpcId": VpcId, "Name": Tags[Key="Name"].Value}[Name != null]'
結果。
{
"VpcId": "vpc-0a1b2c3d4e5f6a7b8",
"Name": "production-vpc"
}
10. Lambda関数の環境変数キー一覧を取得
オブジェクトのキーのみを取り出す操作です。jqのkeys
は自動的にソートするようで、jfqもその挙動に合わせました。逆にjfqの$keys()
は、順序をそのまま返すようですね。
# jq
aws lambda get-function-configuration --function-name MyLambdaFunction | jq -r '.Environment.Variables | keys[]'
# JSONata
aws lambda get-function-configuration --function-name MyLambdaFunction | jfq 'Environment.Variables.$keys()^($)'
結果。
CACHE_ENABLED
DB_HOST
LOG_LEVEL
まとめ
jqはJSONデータに対するクエリツールとしてデファクトの地位を築いていますが、JSONataいいので使っていきましょう!ちなみにjfqにはCSV形式で出力するようなモードはないため、そういうケースでは素直にjqを使った方がいいです。
いっそjqがJSONataをサポートしてくれないかな。
参考情報
今回のブログで使用したサンプルデータです。
aws ec2 describe-instances
{
"Reservations": [
{
"ReservationId": "r-0abcd1234efgh5678",
"OwnerId": "123456789012",
"Groups": [],
"Instances": [
{
"InstanceId": "i-0123456789abcdef0",
"ImageId": "ami-0abcd1234efgh5678",
"InstanceType": "t3.medium",
"LaunchTime": "2024-06-01T08:00:00.000Z",
"State": {"Code": 16, "Name": "running"},
"Placement": {"AvailabilityZone": "ap-northeast-1a"},
"Architecture": "x86_64",
"VirtualizationType": "hvm",
"PublicIpAddress": "203.0.113.10",
"PrivateIpAddress": "10.0.1.10",
"SecurityGroups": [
{"GroupName": "web-sg", "GroupId": "sg-0a1b2c3d4e5f6a7b8"},
{"GroupName": "default", "GroupId": "sg-1234567890abcdef0"}
],
"Tags": [
{"Key": "Name", "Value": "web-server-01"},
{"Key": "Environment", "Value": "production"}
]
},
{
"InstanceId": "i-0fedcba9876543210",
"ImageId": "ami-0fedcba9876543210",
"InstanceType": "t3.large",
"LaunchTime": "2024-06-01T07:30:00.000Z",
"State": {"Code": 80, "Name": "stopped"},
"Placement": {"AvailabilityZone": "ap-northeast-1c"},
"Architecture": "x86_64",
"VirtualizationType": "hvm",
"PrivateIpAddress": "10.0.1.20",
"SecurityGroups": [
{"GroupName": "db-sg", "GroupId": "sg-0fedcba9876543210"}
],
"Tags": [
{"Key": "Name", "Value": "db-server-01"},
{"Key": "Environment", "Value": "staging"}
]
}
]
},
{
"ReservationId": "r-0wxyz9876mnop5432",
"OwnerId": "123456789012",
"Groups": [],
"Instances": [
{
"InstanceId": "i-0a1b2c3d4e5f6a7b8",
"ImageId": "ami-0a1b2c3d4e5f6a7b8",
"InstanceType": "t3.micro",
"LaunchTime": "2024-06-01T09:15:00.000Z",
"State": {"Code": 16, "Name": "running"},
"Placement": {"AvailabilityZone": "ap-northeast-1b"},
"Architecture": "x86_64",
"VirtualizationType": "hvm",
"PublicIpAddress": "198.51.100.25",
"PrivateIpAddress": "10.0.1.30",
"Tags": [{"Key": "Name", "Value": "batch-worker"}]
}
]
}
]
}
aws ec2 describe-vpcs
{
"Vpcs": [
{
"VpcId": "vpc-0a1b2c3d4e5f6a7b8",
"State": "available",
"CidrBlock": "10.0.0.0/16",
"IsDefault": false,
"Tags": [
{"Key": "Name", "Value": "production-vpc"},
{"Key": "Env", "Value": "prod"}
]
},
{
"VpcId": "vpc-0fedcba9876543210",
"State": "available",
"CidrBlock": "192.168.0.0/16",
"IsDefault": false,
"Tags": [
{"Key": "Env", "Value": "staging"}
]
}
]
}
aws cloudwatch describe-alarms
{
"MetricAlarms": [
{
"AlarmName": "HighCPUUtilization",
"AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:HighCPUUtilization",
"StateValue": "ALARM",
"StateReason": "Threshold Crossed: 1 datapoint [90.0 (01/Jun/2024 10:00:00)] was greater than or equal to the threshold (80.0).",
"MetricName": "CPUUtilization",
"Namespace": "AWS/EC2",
"Statistic": "Average",
"Period": 300,
"Threshold": 80.0,
"ComparisonOperator": "GreaterThanOrEqualToThreshold"
},
{
"AlarmName": "LowDiskSpace",
"AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:LowDiskSpace",
"StateValue": "OK",
"StateReason": "Threshold Crossed: 1 datapoint [40.0 (01/Jun/2024 09:55:00)] was greater than the threshold (30.0).",
"MetricName": "FreeStorageSpace",
"Namespace": "AWS/RDS",
"Statistic": "Average",
"Period": 300,
"Threshold": 30.0,
"ComparisonOperator": "LessThanThreshold"
}
]
}
aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=alice
{
"Events": [
{
"EventId": "11111111-2222-3333-4444-555555555555",
"EventName": "StartInstances",
"EventSource": "ec2.amazonaws.com",
"Username": "alice",
"EventTime": "2024-06-01T10:00:00Z",
"Resources": [
{
"ResourceType": "AWS::EC2::Instance",
"ResourceName": "i-0123456789abcdef0"
}
],
"CloudTrailEvent": "{\"eventVersion\":\"1.08\",\"userIdentity\":{\"type\":\"IAMUser\",\"principalId\":\"AIDAEXAMPLE1\",\"arn\":\"arn:aws:iam::123456789012:user/alice\",\"accountId\":\"123456789012\",\"userName\":\"alice\"},\"eventTime\":\"2024-06-01T10:00:00Z\",\"eventSource\":\"ec2.amazonaws.com\",\"eventName\":\"StartInstances\",\"awsRegion\":\"ap-northeast-1\",\"sourceIPAddress\":\"203.0.113.5\",\"userAgent\":\"aws-cli/2.15.0\",\"requestParameters\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0123456789abcdef0\"}]}},\"responseElements\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0123456789abcdef0\",\"currentState\":{\"code\":0,\"name\":\"pending\"},\"previousState\":{\"code\":80,\"name\":\"stopped\"}}]}},\"requestID\":\"12345678-1234-1234-1234-123456789012\",\"eventID\":\"11111111-2222-3333-4444-555555555555\",\"readOnly\":false,\"eventType\":\"AwsApiCall\",\"managementEvent\":true,\"recipientAccountId\":\"123456789012\"}"
},
{
"EventId": "22222222-3333-4444-5555-666666666666",
"EventName": "StopInstances",
"EventSource": "ec2.amazonaws.com",
"Username": "alice",
"EventTime": "2024-06-01T11:15:00Z",
"Resources": [
{
"ResourceType": "AWS::EC2::Instance",
"ResourceName": "i-0fedcba9876543210"
}
],
"CloudTrailEvent": "{\"eventVersion\":\"1.08\",\"userIdentity\":{\"type\":\"IAMUser\",\"principalId\":\"AIDAEXAMPLE1\",\"arn\":\"arn:aws:iam::123456789012:user/alice\",\"accountId\":\"123456789012\",\"userName\":\"alice\"},\"eventTime\":\"2024-06-01T11:15:00Z\",\"eventSource\":\"ec2.amazonaws.com\",\"eventName\":\"StopInstances\",\"awsRegion\":\"ap-northeast-1\",\"sourceIPAddress\":\"203.0.113.5\",\"userAgent\":\"aws-cli/2.15.0\",\"requestParameters\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0fedcba9876543210\"}]}},\"responseElements\":{\"instancesSet\":{\"items\":[{\"instanceId\":\"i-0fedcba9876543210\",\"currentState\":{\"code\":64,\"name\":\"stopping\"},\"previousState\":{\"code\":16,\"name\":\"running\"}}]}},\"requestID\":\"87654321-4321-4321-4321-210987654321\",\"eventID\":\"22222222-3333-4444-5555-666666666666\",\"readOnly\":false,\"eventType\":\"AwsApiCall\",\"managementEvent\":true,\"recipientAccountId\":\"123456789012\"}"
},
{
"EventId": "33333333-4444-5555-6666-777777777777",
"EventName": "CreateBucket",
"EventSource": "s3.amazonaws.com",
"Username": "alice",
"EventTime": "2024-06-01T12:00:00Z",
"Resources": [
{
"ResourceType": "AWS::S3::Bucket",
"ResourceName": "my-new-bucket"
}
],
"CloudTrailEvent": "{\"eventVersion\":\"1.08\",\"userIdentity\":{\"type\":\"IAMUser\",\"principalId\":\"AIDAEXAMPLE1\",\"arn\":\"arn:aws:iam::123456789012:user/alice\",\"accountId\":\"123456789012\",\"userName\":\"alice\"},\"eventTime\":\"2024-06-01T12:00:00Z\",\"eventSource\":\"s3.amazonaws.com\",\"eventName\":\"CreateBucket\",\"awsRegion\":\"ap-northeast-1\",\"sourceIPAddress\":\"203.0.113.5\",\"userAgent\":\"aws-cli/2.15.0\",\"requestParameters\":{\"bucketName\":\"my-new-bucket\"},\"responseElements\":null,\"requestID\":\"ABCDEF123456\",\"eventID\":\"33333333-4444-5555-6666-777777777777\",\"readOnly\":false,\"eventType\":\"AwsApiCall\",\"managementEvent\":true,\"recipientAccountId\":\"123456789012\"}"
}
]
}
aws iam list-users
{
"Users": [
{
"Path": "/",
"UserName": "alice",
"UserId": "AIDAEXAMPLE1",
"Arn": "arn:aws:iam::123456789012:user/alice",
"CreateDate": "2023-01-15T08:30:00Z"
},
{
"Path": "/",
"UserName": "bob",
"UserId": "AIDAEXAMPLE2",
"Arn": "arn:aws:iam::123456789012:user/bob",
"CreateDate": "2022-12-20T14:45:00Z"
}
]
}
aws lambda get-function-configuration --function-name MyLambdaFunction
{
"FunctionName": "MyLambdaFunction",
"Runtime": "python3.11",
"Role": "arn:aws:iam::123456789012:role/lambda-execution-role",
"Handler": "lambda_function.lambda_handler",
"CodeSize": 123456,
"Description": "Example Lambda function",
"Timeout": 15,
"MemorySize": 128,
"LastModified": "2024-05-30T12:00:00.000+0000",
"CodeSha256": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"Version": "$LATEST",
"Environment": {
"Variables": {
"LOG_LEVEL": "info",
"DB_HOST": "database.internal",
"CACHE_ENABLED": "true"
}
}
}