estie SRE の sugitak です。今日はね、 JSON を扱っていこうと思います。
何の記事?
手元で bash
や zsh
を使っていると、環境変数ってこういう形ですよね。ペアで扱います。
Environment=production Product=estie ... DD_ENV=production
ところが AWS はこういうふうに保持しています。これは人間の目に優しくないので、できれば手元と同じく Key=Value
っぽい形で取り扱えたら嬉しいな。そのように思っているわけです。
[ { "Key": "Environment", "Value": "production" }, { "Key": "Product", "Value": "estie" }, ... { "Key": "DD_ENV", "Value": "production" } ]
これを jq などのツールを使って変換し、人間と AWS とのインターフェイスをいい感じにしていこう、というのが今回の記事です。
jq
jq はもう説明不要ですね。 CLI で JSON を扱う処理系の決定版です。
最初の例では、その jq を使用して、 EC2 インスタンス一覧からタグだけを取り出してみましょう。
EC2 タグのデータ構造
EC2 タグは、 aws ec2 describe-instances
の情報から取得できます。このとき API から返却される値から tag を抽出すると、以下のような場所にあることがわかります:
{ "Reservations": [ "Instances": [ { ... "Tags": [ { "Key": "Name", "Value": "Tokyo" } ], ... }, { ... "Tags": [ { "Key": "Name", "Value": "Osaka" }, { "Key": "DD_ENV", "Value": "production" }, ... ], ... } ] ] }
ちょっとわかりにくいので jq / JMESPath のパス表現を使うと、
.Reservations[].Instances[].Tags // jq の場合 Reservations[].Instances[].Tags // JMESPath の場合
はい。わかりやすくなりました。
jq でタグ一覧を取り出してみる
先ほどのパス表現を使って取り出してみると、こうなります:
$ aws ec2 describe-instances --output json | jq '.Reservations[].Instances[].Tags' [ { "Key": "Name", "Value": "Tokyo" } ] [ { "Key": "Name", "Value": "Osaka" }, { "Key": DD_ENV", "Value": "production" }, ... ] ...
はい。取り出せましたね。
取り出すだけでなく、もう少し実用的なことをしてみましょう。任意のタグ、たとえば Name
に相当するタグの Value を抽出するにはどうすればいいでしょうか?
EC2 のインスタンス名一覧を取得する
EC2 のインスタンス名、実体は Name
タグです。インスタンス名一覧を表示しようと思ったら、 .Key == "Name"
となる要素だけに絞り込んだうえで、その .Value
を表示させれば OK です。要素の絞り込みは select
を使えばできます。
$ aws ec2 describe-instances --output json | jq -r '.Reservations[].Instances[] | (.Tags[]? | select(.Key == "Name")).Value' Tokyo Osaka ...
Reservations[].Instances[]
の中から Tags
を持っているものを取り出し、そのうち .Key == "Name"
となるものを絞り込んだうえでその Value
を並べれば名前の一覧のできあがり、という内容。 Straightforward ですね。
インスタンスIDとのペアを出力したければ、さらに少し組み替えてこんな感じ。
$ aws ec2 describe-instances --output json | jq '.Reservations[].Instances[] | [ .InstanceId, (.Tags[]? | select(.Key == "Name")).Value ]' [ "i-0xxxxxxxxxxxxxxxx", "Tokyo" ] [ "i-0yyyyyyyyyyyyyyyyy", "Osaka" ] ...
やりましたね!
本題: Key:Value
形式に組み替える
さて、いよいよ本題です。タグをシンプルな辞書形式へと組み替えていきましょう。
おおむねこんな構造をしているものを:
"Tags": [ { "Key": "Name", "Value": "Tokyo" } ], "Tags": [ { "Key": "Name", "Value": "Osaka" }, { "Key": "DD_ENV", "Value": "production" }, ],
こういうふうにしたい、ということです。
[ { "Name": "Tokyo" }, { "Name": "Osaka", "DD_ENV": "production", }, ... ]
はい。こんな感じ↓でできます:
$ aws ec2 describe-instances --output json | jq '.Reservations[].Instances[] | [(.Tags[]? | {key:.Key, value:.Value})] | from_entries' { "Name": "Tokyo" } { "Name": "Osaka", "DD_ENV": "production" } ...
やったね!
解説
jq には、そのものずばり from_entries
という関数があります。この関数に [{key: xxx, value: yyy}]
という Array を渡すと、 {xxx: yyy}
という Object として返してくれます。
ここで、小さな問題がひとつあります。 AWS から返ってくる Object は Key
Value
と大文字になっているのです。 from_entries
が受け付けるのは key
value
、小文字の場合だけなので、このままでは from_entries
に渡したところでうまく変換できません。そこで、{key:.Key, value:.Value}
という呼び出しをし、 from_entries
に渡せる形へと変換しているのですね。
はい!これで目的達成です!
さらに便利に使う
先ほどはタグ一覧を見ましたが、それだけだとどのインスタンスかわかりにくいです。インスタンスIDとIPアドレスも加えてみましょう。
$ aws ec2 describe-instances --output json | jq '.Reservations[].Instances[] | {"InstanceId": .InstanceId, "IPAddr": .PrivateIpAddress, "Tags": ([(.Tags[]? | {key:.Key, value:.Value})] | from_entries)}' { "InstanceId": "i-0xxxxxxxxxxxxxxxx", "IPAddr": "10.0.0.51", "Tags": { "Name": "Tokyo" } } { "InstanceId": "i-0yyyyyyyyyyyyyyyy", "IPAddr": "10.0.0.17", "Tags": { "Name": "Osaka", "DD_ENV": "production", } } ...
最高!取り扱いやすくなりました。
JMESPath
ここまで jq での取り扱いを説明しました。 aws
コマンドに組み込まれた JSON 変換ツールである JMESPath ではどうなるでしょうか?
JMESPath も jq と同じく JSON 処理系です。 jq と比べると若干柔軟性が低い面がある一方で、 aws
CLI に組み込まれているという利点がなかなか大きく、クエリの仕方を覚えると aws
コマンドをより便利に使えるようになるため、知っておく価値のある言語となっています。
なお JMESPath は CLI でスタンドアロンで利用することもできます。そのときのコマンドは jp です。
https://github.com/jmespath/jp
JMESPath で取り出してみる
先ほどの jq の例と同じように、 JMESPath を用いてタグ一覧を取得してみます。
$ aws ec2 describe-instances --output json --query 'Reservations[].Instances[].Tags' [ [ { "Key": "Name", "Value": "Tokyo" } ], [ { "Key": "Name", "Value": "Osaka" }, { "Key": DD_ENV", "Value": "production" } ... ] ... ]
jq でも同じことはできていましたが、 JMESPath の場合はパイプが不要で aws
コマンドだけで完結しているところがポイントです。
Name
タグで絞り込む
Reservations[].Instances[].Tags
まで追いかけたあと、その projection 結果を ?Key=='Name'
で絞り込み、その結果の Key, Value
ペアを配列として作れば、以下のような結果が得られます。
$ aws ec2 describe-instances --output json --query "Reservations[].Instances[].Tags[?Key=='Name'][Key,Value][]" [ [ "Name", "Tokyo" ], [ "Name", "Osaka" ], ... ]
Name
タグだけの配列を作る
絞り込みまでは同様に実施して、最後に Value
だけを表示すれば、 Name
タグの一覧が得られます。
$ aws ec2 describe-instances --output json --query "Reservations[].Instances[].Tags[?Key=='Name'].Value[]" [ "Tokyo", "Osaka", ... ]
自分が JMESPath でできたのはこのあたりまででした。 JMESPath には jq の from_entries
相当のビルトイン関数がないようなので、そうなると jq ほど自在に JSON を組み替えることは難しそうです。
とはいえやはり JMESPath は aws
CLI に組み込まれている利点が圧倒的に強く、ちょっとしたことなら JMESPath でササっと絞り込みでき非常に便利です。 aws コマンドさえあれば動くということで、社内用マニュアルとかシェルスクリプトとかに気軽に組み込めますからね。
なお、上では --output json
で出力していますが、これを --output text
形式で出力するようにすると、 Name
タグの内容が水平タブ 0x09
区切りで出力されるようになるので、 CLI 的にとっても便利です。シェル芸たのしい!たのしい! 🤤
$ aws ec2 describe-instances --output text --query "Reservations[].Instances[].Tags[?Key=='Name'].Value[]" Tokyo Osaka ...
Jsonnet
話変わって Jsonnet です。 Jsonnet は、 JSON を生成することを主な目的とした言語です。
estie ではデプロイに ecspresso を使用しています。 ecspresso では ECS の Task Definition の JSON ファイルをそのまま設定ファイルとして使用するのですが、実はこれを Jsonnet として扱うこともできます。 Jsonnet は他ファイルからの読み込みができるので、環境変数をはじめとした共通の設定を一括管理できるようになります。 Jsonnet はとても便利なんですね。
https://github.com/kayac/ecspresso
さて、そんな Task Definition の環境変数ですが、普通にやれば {Key: "Key", Value: "Value"}
と冗長な書き方をすることになります。できるなら Key: Value
形式でもって短く書きたいですよね。今回 Jsonnet の例として、そのような Task Definition の書き方を紹介します。
まずは、元となる Task Definition の書き方から見てみましょう。
変更前
普通に書くと、下のようになります。環境変数を毎回4行ずつ書くので縦長になりコードの見通しが悪く、「何を設定したか」がひとめでわかりません。
{ environment: [ { name: "Environment", value: "production" }, { name: "DD_ENV", value: "production" }, { name: "NO_COLOR", value: "true" }, ... ], image: image, ... }
変更後
環境変数を envs
という辞書として外出しすると、これをうまいところ並べられるようになります。
local envs = { Environment: production, DD_ENV: production, NO_COLOR: true, ... } { environment: std.map(function(k){ name: k, value: envs[k] }, std.objectFields(envs)), image: image, }
Jsonnet には jq のように from_entries
相当の関数がありません。なので、多少無理やりですが、変数の Key 一覧を std.objectFields
関数で呼び出したのちその Key を再度同じ変数に Key として突っ込んで Value を得る、ということをすれば、なんとか解決できます。
このくらいの行数だと「変更後」の方が無駄に凝っていてわかりにくい感はありますが、環境変数が15個を超えてくると「ひとめで見える」利点が大きくなってきます。どのみち下の environment
以下は普段は見る必要すらなくて、環境変数の追加削除時は local envs
だけ気にしていればいいですからね。この記法を使う利点は少なからずあるかと思います。
おわりに
estie では JSON や Jsonnet に限らず、 YAML, HCL, CUE, Pkl など設定言語に対して熱意ある仲間を募集しています。一緒に産業の真価をさらに拓いていきましょう!