JSONata式でJSONデータを抽出加工できるCLIツールjfqを試してみた

JSONata式でJSONデータを抽出加工できるCLIツールjfqを試してみた

Clock Icon2025.06.30

こんにちは。サービス開発室の武田です。

AWS Step Functionsで使うためにJSONataを覚え始めましたが、やってみるとおもしろいですね。

AWS CLIのレスポンスの加工などにもJSONataを使えないかなということで、調べてみたらjfqというツールが公開されていたので使ってみました。

https://github.com/blgm/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"
    }
  }
}

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.