vlusという脆弱性診断ツールを使用していたのですが、configファイルにインスタンスIPを記述する必要がありました。EC2は再起動すると静的IPが変わってしまうので、インスタンスIPを直書きせずに動的に変えられるようにしたいという気持ちになっていました。
そこで、awscliをつかって情報を取得し、configファイルを書き換えるbashスクリプトを作成することにしました。なんとか作り上げたもののbashスクリプトについては浅い理解しかできていないので、自分の理解を深めるために記事にします。
bashってなんやねん??という方は以下の記事をご覧ください
(数か月前の自分はこんなこともわかってなかったのかとびっくりします)
前提
awscli, jqインストール済みであること
bashスクリプト
以下がこれから解説していくbashスクリプトです。
#!/bin/bash
USER="vuls"
SSH_KEY_PATH="/opt/vuls/.ssh/id_rsa"
VULS_CONF_FILE_PATH="/opt/vuls"
aws ec2 describe-instances --output json \
| jq -r '.Reservations[].Instances[] |
select(any(.Tags[]?;
.Key == "Name" and (
.Value | contains("prod-ec2-worker-node") or contains("prod-ec2")
)
)) |
.InstanceId + "," +
(.Tags[] | select(.Key == "Name").Value) + "," + .PrivateIpAddress' \
>"${VULS_CONF_FILE_PATH}/ec2_list.csv"
cp -p "${VULS_CONF_FILE_PATH}/empty.toml" "${VULS_CONF_FILE_PATH}/config.toml"
while IFS=, read -r INSTANS_ID INSTANSE_NAME PRIVATE_IP; do
if [[ "$INSTANSE_NAME" =~ "worker-node" ]]; then
INSTANSE_NAME="prod-eks-worker-node-${INSTANS_ID}"
fi
{
echo ""
echo "[servers.${INSTANSE_NAME}]"
echo "host = \"${PRIVATE_IP}\""
echo "port = \"22\""
echo "user = \"${USER}\""
echo "keyPath = \"${SSH_KEY_PATH}\""
} >>"${VULS_CONF_FILE_PATH}/config.toml"
done <"${VULS_CONF_FILE_PATH}/ec2_list.csv"
解説
#!/bin/bash
ここでは実行するシェルスクリプトのインタプリタを指定しています。今回はシェルスクリプトがbashを使用して実行されるようになります。
ちなみに開始の記号である#!をシバン(shebang)というのですが、その由来は
・#(ハッシュ:hash/シャープ:sharp) と !(バン:bang) を縮めた
・shell bangが変形したもの
等の説があるらしいです。
USER="vuls"
SSH_KEY_PATH="/opt/vuls/.ssh/id_rsa"
VULS_CONF_FILE_PATH="/opt/vuls"
変数を設定しています。
aws ec2 describe-instances --output json
Amazon EC2インスタンスの詳細をJSON形式で取得しています。
jq -r .Reservations[].Instances[]
jqコマンドはJSONデータをフィルタリングや整形するためのコマンドです。-rオプションを使用して、フィルタリングした結果をRaw形式で出力しています。
.Reservations[].Instances[]で全ての「Reservations」オブジェクトとその中の「Instances」オブジェクトを選択します。
jqにおいて"."は「現在のオブジェクト」または「現在のデータ構造」を指し示します。
先頭に"."が存在しているのは少し違和感がありました。しかしJavaScriptなどで、tagsというオブジェクトの配下にある"name"というkeyのvalueを取りたいときにtags.nameとしますが、この"."は特に違和感がないので慣れていないだけって感じがしますね。
select(any(.Tags[]?; .Key=="Name" and \
(.Value | contains("prod-ec2-worker-node") or \
contains("prod-ec2")))) | \
select(any(.Tags[]?; ...))
Tags配列の中で、条件に一致するものが一つでもあればそのInstancesオブジェクトを選択します。
.Key=="Name" and
(.Value | contains("prod-ec2-worker-node") or contains("prod-ec2"))
(Tags配列の中にある1つ1つのオブジェクトのうち、)keyが"key"でvalueが"Name"であるものを含む、かつ、keyが"value"でvalueが("prod-ec2-worker-node", "prod-ec2")のいずれかを含む、を満たす場合がtrueである条件式です。
かなりわかりにくいですが、具体的に渡される値があれば少しわかりやすくなると思います。
以下の場合はInstancesのうち、0番目と2番目の要素が取得されます。
{
"Reservations": [
{
"Instances": [
{
"InstanceId": "i-1111xxxxxxxx",
"Tags": [
{
"Key": "Name",
"Value": "prod-ec2-worker-node"
}
],
"PrivateIpAddress": "192.168.1.1"
},
{
"InstanceId": "i-2222xxxxxxxx",
"Tags": [
{
"Key": "hoge",
"Value": "prod-ec2-worker-node"
}
],
"PrivateIpAddress": "192.168.1.2"
},
{
"InstanceId": "i-3333xxxxxxxx",
"Tags": [
{
"Key": "Name",
"Value": "prod-ec2-01"
}
],
"PrivateIpAddress": "192.168.1.3"
},
{
"InstanceId": "i-4444xxxxxxxx",
"Tags": [
{
"Key": "Name",
"Value": "stg-ec2-01"
}
],
"PrivateIpAddress": "192.168.1.3"
}
]
}
]
}
.InstanceId + "," + \
(.Tags[] | select (.Key == "Name").Value) + "," + .PrivateIpAddress' \
> ${VULS_CONF_FILE_PATH}/ec2_list.csv
基本的には出力しているだけです。
select (.Key == "Name").Value
こちらは、(.Key == "Name")を満たす要素を取りだしたあと、その要素のうちkeyが"value"であるもののvalueを取り出しています。
以下からprod-ec2-worker-nodeが抜き出されるイメージです。
"Tags": [
{
"Key": "Name",
"Value": "prod-ec2-worker-node"
}
{
"Key": "Env",
"Value": "prod"
}
{
"Key": "Region",
"Value": "ap-northeast-1"
}
> ${VULS_CONF_FILE_PATH}/ec2_list.csv
出力を書き込んでいます。
cp -p ${VULS_CONF_FILE_PATH}/empty.toml ${VULS_CONF_FILE_PATH}/config.toml
毎回ファイルを書き直すので、一度空のconfig.tomlファイルを作成しています。
ちなみに、当初は
rm ${VULS_CONF_FILE_PATH}/config.toml
touch ${VULS_CONF_FILE_PATH}/config.toml
と記述していたのですが、先輩からtouchだと実行者の権限が付与されてしまうので、権限が引き継がれるcpの方が良いというレビューをいただき変更しました。
while IFS=, read INSTANS_ID INSTANSE_NAME PRIVATE_IP
do
~~~
done < ${VULS_CONF_FILE_PATH}/ec2_list.csv
IFS=,
Input Field Separator(入力フィールドセパレータ)であり、","でCSVの各フィールドを区切ります。そして、それぞれのフィールドを INSTANS_ID, INSTANSE_NAME, PRIVATE_IP という変数に格納します。
do
while ループが始まることを示します。
done < ${VULS_CONF_FILE_PATH}/ec2_list.csvはec2_list.csv
ファイルから読み込んだ各行に対して上記の処理を繰り返します。
では、~~~の中身を見ていきます。
if [[ "$INSTANSE_NAME" =~ "eks" ]];then
INSTANSE_NAME="prod-eks-worker-node-${INSTANS_ID}"
fi
if [[ "$INSTANSE_NAME" =~ "eks" ]]; then:
インスタンス名(INSTANSE_NAME)に "eks" が含まれていれば、次の処理を実行します。
INSTANSE_NAME="prod-eks-worker-node-${INSTANS_ID}"
INSTANSE_NAME 変数の値を "prod-eks-worker-node-" とインスタンスID(INSTANS_ID)で更新します。
if文のコードブロックはfiで締められます。安直な気もしますがわかりやすくていいですね。
echo "" >> ${VULS_CONF_FILE_PATH}/config.toml
echo "[servers.${INSTANSE_NAME}]" >> ${VULS_CONF_FILE_PATH}/config.toml
echo "host = \"${PRIVATE_IP}\"" >> ${VULS_CONF_FILE_PATH}/config.toml
echo "port = \"22\"" >> ${VULS_CONF_FILE_PATH}/config.toml
echo "user = \"${USER}\"" >> ${VULS_CONF_FILE_PATH}/config.toml
echo "keyPath = \"${SSH_KEY_PATH}\"" >> ${VULS_CONF_FILE_PATH}/config.toml
最後はファイルに書き込みをしています。
感想
シェルスクリプトには少し恐怖を抱いていましたが、丁寧に見ていくとそこまで難しくなかったです。あとオブジェクトの中身を文章で伝えるのはかなり難しかったです。間違いがあればどなたでも修正のリクエスト出してもらえるとありがたいです。
参考