はじめに
この記事はAWS Advent Calendar 2019 24日目の記事です。
AWS CLIでDynamoDBの項目をサクッと操作したいと思った時、コマンドのオプションや引数の書き方など覚える項目が多く、さらにテーブルの属性なども正確に覚えておく必要があり、そらでポチポチ入力していくのにはかなり無理があると思いませんか?
いざという時に素早く項目を更新したいのに、オプションも引数も属性名も覚えておらず更新に時間が掛かったり、誤った名称の属性を増やしてしまった……!!なんてことが起こります。
というわけで、よく更新するDynamoDBのテーブルの項目をAWS CLIで自由に操作するために、スクリプトを書いて素早く更新ができるようになったというお話です。
TL;DR
- AWS CLIで
aws dynamodb update-item
する時は更新するためのスクリプトを準備しておくと楽 - 既存の項目が多数あり、更新したい属性が毎回違う場合はreadコマンドを使って入力させると柔軟に対応できる
-
update-item
では指定したkeyの項目が存在しない場合には新しい項目を作成してしまうため、項目を作りたくない場合には最初にaws dynamodb get-item
でバリデーションチェックしておくといい - サンプルコード
本題
環境
macOS Catalina 10.15.1
$ sh --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)
Copyright (C) 2007 Free Software Foundation, Inc.
$ aws --version
aws-cli/1.16.300 Python/3.7.5 Darwin/19.0.0 botocore/1.13.36
$ jq --version
jq-1.6
AWS CLIでのUpdateItemの書き方の例
公式のドキュメントに例が書かれていますね。
更新式 - Amazon DynamoDB
$ aws dynamodb update-item \
--table-name ProductCatalog \
--key '{"Id":{"N":"789"}}' \
--update-expression "SET ProductCategory = :c, Price = :p" \
--expression-attribute-values file://values.json \
--return-values ALL_NEW
{
":c": { "S": "Hardware" },
":p": { "N": "60" }
}
覚えられない……😇
-
--key
には一意の項目を取得できるように、パーティションとソートキーを指定する必要がある -
--update-expression
には、SET
orREMOVE
orADD
orDELETE
キーワードを記載したうえ、更新する項目についてアクションを書く必要がある-
ProductCategory = :c
とかPrice = :p
とか
-
-
--expression-attribute-values
でjsonファイルを指定 or jsonを直接書く - オプションの名前が長い
- 必要なオプションが多い
- 属性の名前を間違えると、無駄な属性を増やしてしまう
こんな感じでupdate-itemするにはトラップがたくさんあります。
覚えるのが無理なら、更新頻度が高いテーブルに関しては更新用のスクリプトを作ってみると楽になるかなと思い、次の項目にまとめてみました。
特定の属性を更新するスクリプトを作成する
こんな感じの項目が Fruits
テーブルにあるとします。
{
"name": {"S": "apple"}, // プライマリキー
"amount": {"N": "100"},
"item_count": {"N": "3"},
"production_area": {"S": "Aomori"},
"is_sale": {"BOOL": false}
}
このテーブルのある項目に対してitem_countを更新するスクリプトを書くと以下のような感じになるでしょうか。
#!/bin/sh
name="${1}"
item_count="${2}"
aws dynamodb update-item \
--table-name Fruits \
--key "{\"name\":{\"S\": \"${name}\"}}" \
--update-expression "SET item_count = :item_count" \
--expression-attribute-values "{\":item_count\": {\"N\": \"${item_count}\"}}" \
--return-values ALL_NEW
exit 0
$ ./update_fruits_count.sh apple 4
でappleのitem_countを4に更新できます。
ただし、このスクリプト1つでは Fruits
にある項目の amount
というたった1つの属性しか変更ができません。
他の属性を変更するスクリプトも上記と同じような感じで書けはしますが、同じようなスクリプトを量産するのは美しくないですよね。
ここでパッと思いつく方法として、更新する可能性がある属性が複数ある場合、スクリプトの引数を増やしていけば更新が可能になると思います。
が、そのスクリプトを叩くとき「今回は amount
だけ更新したいけど、他の属性は更新したくない」といった時も引数に値を入れる必要があるため、現在の値がわからないと誤って値を変更してしまう可能性があり危険です。
また、更新する可能性がある属性が例えば5種類あった時に、引数を5つも指定するなんて手間ですよね。
多数のうち任意の属性のみ更新するスクリプトを作成する
であれば、更新したい属性は新しい値を入力し、更新したくない属性は何も入力せずに更新をスキップするということができれば、誤った値を入力することもなく手間も少なくなります。
ここで問題になってくるのは、
- 属性が多くても、更新したい属性とスキップしたい属性をわかりやすく指定する方法
-
--update-expression
と--expression-attribute-values
の引数の生成方法
をどうするか、ということです。
属性が多くても、更新したい属性とスキップしたい属性をわかりやすく指定する方法
そこで使うのが read
コマンドです。
readコマンドはユーザーからの入力を変数に入れるコマンドで、スクリプト実行中にユーザーからの入力を待つことができます。
CLI上で確認を促される時に出てくる 本当によろしいですか?[y/N]
みたいなやつです(多分)。
このreadコマンドを使用して、プライマリキー以外の項目全てについてスクリプト実行中に入力を待ち受ければ良いのです。
#!/bin/sh
read -p "amount: " input_amount
echo You wrote \"${input_amount}\".
exit 0
といったスクリプトを書いて実行すると、
$ ./test_read.sh
amount:
という状態でユーザーからの入力を待ち受けます。ここで100
と入力しreturnキーを押すと、
$ ./test_read.sh
amount: 100
You wrote "100".
といった感じに表示されます。
input_amount
という変数に入力値が入るため、この変数の中身で条件分岐を作ることが出来ます。
このように、更新する可能性がある属性全てに対してユーザーの入力を待ち受けるように作っていけばよいでしょう。
ここで、入力待ち受け中にreturnキーのみを押して何も入力が無い場合は変数の中に空文字が入るため、空文字かどうか判定することで更新するかしないかを判別することができます。
--update-expression
と --expression-attribute-values
の引数の生成方法
さて、readコマンドで入力値を取得できたため、それを用いて--update-expression
と --expression-attribute-values
の引数を生成していきましょう。
action_value="SET"
update_expression="${action_value}"
new_value=""
# set amount
read -p "amount: " input_amount
if [ -n "${input_amount}" ]; then
update_expression="${update_expression} amount = :amount, "
new_value="${new_value}\":amount\": {\"N\": \"${input_amount}\"}, "
fi
# set item_count
read -p "item_count: " input_item_count
if [ -n "${input_item_count}" ]; then
update_expression="${update_expression} item_count = :item_count, "
new_value="${new_value}\":item_count\": {\"N\": \"${input_item_count}\"}, "
fi
といった具合に、属性ごとにreadコマンドで入力値を待ち受け、空文字でなければupdate_expression
とnew_value
という値に新しく値を追加していくようなコードを書きます。
そうすると、amount
とitem_count
を入力し終えたとき、
update_expression
には
SET amount = :amount, item_count = :item_count,
という文字列が、new_value
には
\":amount\": {\"N\": 100}, \":item_count\": {\"N\": 4},
という文字列が入っています。
最後に、update_expression
とnew_value
の最後の,
を取り除き、new_value
には文字列全体を{}
で包みましょう。
# remove ", "
update_expression=$(echo ${update_expression%, })
# add "{}", remove ", "
new_value="{$(echo ${new_value%, })}"
最終的に、update_expression
は
SET amount = :amount, item_count = :item_count
に、new_value
は
{\":amount\": {\"N\": 100}, \":item_count\": {\"N\": 4}}
になりました。これでやっと引数に渡せる形になりました!
作った変数でCLIを叩く
せっかくなので、readコマンドやechoコマンドを使ってユーザーフレンドリーにしてみました。
#!/bin/sh
name="${1}"
if [ -z "${name}" ]; then
echo 'usage: update_fruits `fruits_name`'
echo 'update_fruits apple'
exit ${LINENO}
fi
table_name="Fruits"
action_value="SET"
update_expression="${action_value}"
new_value=""
echo "Input update values. (If you don't update it, press the return key.)"
# set amount
read -p "amount: " input_amount
if [ -n "${input_amount}" ]; then
update_expression="${update_expression} amount = :amount, "
new_value="${new_value}\":amount\": {\"N\": \"${input_amount}\"}, "
fi
# set item_count
read -p "item_count: " input_item_count
if [ -n "${input_item_count}" ]; then
update_expression="${update_expression} item_count = :item_count, "
new_value="${new_value}\":item_count\": {\"N\": \"${input_item_count}\"}, "
fi
# set production_area
read -p "stranded_amount: " input_stranded_amount
if [ -n "${input_stranded_amount}" ]; then
update_expression="${update_expression} stranded_amount = :stranded_amount, "
new_value="${new_value}\":stranded_amount\": {\"S\": \"${input_stranded_amount}\"}, "
fi
# set is_sale
read -p "is_sale: " input_is_sale
if [ -n "${input_is_sale}" ]; then
update_expression="${update_expression} is_sale = :is_sale, "
new_value="${new_value}\":is_sale\": {\"S\": \"${input_is_sale}\"}, "
fi
# remove ", "
update_expression=$(echo ${update_expression%, })
# add "{}", remove ", "
new_value="{$(echo ${new_value%, })}"
# 何も更新しない場合は終了する
if [ "${update_expression}" == "${action_value}" ]; then
echo 'Exit without updating anything.'
exit ${LINENO}
fi
# 更新する属性を表示し、ユーザーに確認を求める
echo "new value: ${new_value}"
read -n1 -p "Are you sure you want to update these? [y/n]: " input
if [[ ${input} = [yY] ]]; then
echo '\n'
else
echo '\nUpdate canceled.'
exit ${LINENO}
fi
# 更新する
aws dynamodb update-item \
--table-name ${table_name} \
--key "{\"name\": {\"S\": \"${name}\"}}" \
--update-expression "${update_expression}" \
--expression-attribute-values "${new_value}" \
--return-values ALL_NEW
exit 0
$ ./update_fruits.sh
Input update values. (If you don\'t update it, press the return key.)
amount:
item_count: 4
stranded_amount:
is_sale:
new value: {":item_count": {"N": "4"}}
Are you sure you want to update these? [y/n]: Y
{
"Attributes": {
"amount": {
"N": "100"
},
"item_count": {
"N": "4"
},
"name": {
"S": "apple"
},
"is_sale": {
"BOOL": false
},
"production_area": {
"S": "Aomori"
}
}
}
新しい項目の生成を防ぐ
Fruits
テーブルの中にnameがbananaの項目が無い時に上記のコマンドを叩き、スキップした属性があった場合、
{
"Item": {
"item_count": {
"N": "1"
},
"name": {
"S": "banana"
},
}
}
といった新項目ができてしまいます。APIなどで使用する時に必須項目としたい属性が存在する場合、このような項目が存在すると困ります。
update-itemには既存の属性の上書きを防止する関数は存在しますが、既存の項目が無い時に新しい項目の生成を防ぐ関数などはありません。
そこで、update-itemする前に、バリデーションチェックとして以下のようにget-itemするようにします。
DynamoDB での項目の操作 - Amazon DynamoDB
get_item=$(aws dynamodb get-item \
--table-name ${table_name} \
--key "{\"name\":{\"S\": \"${name}\"}}" | \
jq -r '.Item | "name: " + .name.S
+ "\namount: " + .amount.N
+ "\nitem_count: " + .item_count.N
+ "\nproduction_area: " + .production_area.S
+ "\nis_sale: " + (.is_sale.BOOL | tostring)')
if [ -z "${get_item}" ]; then
echo "${name} is not exist."
exit ${LINENO}
fi
echo "${get_item}"
get-itemの標準出力をそのまま表示しても良いのですが、後ほどreadコマンドで入力する時と同じように表示されるようにjqコマンドで整形しておきます。
指定したプライマリキーの項目が存在しない場合は変数get_item
が空文字となるため、testコマンドを使って弾きます。
まとめ
- AWSCLIで
aws dynamodb update-item
する時は更新するためのスクリプトを準備しておくと楽 - 既存の項目が多数あり、更新したい属性が毎回違う場合はreadコマンドを使って入力させると柔軟に対応できる
-
update-item
では指定したkeyの項目が存在しない場合には新しい項目を作成してしまうため、項目を作りたくない場合には最初にaws dynamodb get-item
でバリデーションチェックしておくといい
今回のスクリプトはGitHubに公開していますので、よかったらご覧ください。
https://github.com/kurapy-n/UpdateDynamoDBTableItem/blob/master/update_fruits.sh