概要
環境変数を管理するためにSecrets Manager
を利用していますが、運用上の課題として①プレーンテキストでの直接編集によるオペレーションミスのリスクや、②変更履歴が確認しにくいということがありました。
これらの課題を解決するために、Secrets Manager
の値を安全に管理・操作できるシェルツールを作成してみました。
解決したい課題
その1 プレーンテキストの環境変数を修正したくない
数値や真偽値など文字列以外を保存しているSecrets Manager
を変更する場合、コンソールからだとプレーンテキストを修正することになると思います。
このケースだと手動でJSONを修正することになるので、形が崩れたりするオペミスを引き起こす懸念があります。
その2 万が一に備えて過去の値を確認できるようにしたい
誤って値を削除してしまったり、誤った値で更新してしまったり。
このようなケースが発生したときに備えて、Secrets Manager
の値がいつどのように変更されたのか、変更履歴を確認できるようにしておきたいたいです。
コンソールから編集を行うとAWSPREVIOUS
とAWSCURRENT
がどんどん上書きされていきますが、タイムスタンプ情報を残すことでいつどのような更新が行われたかを振り返れるようにしておきたいです。
作成したもの
対話形式でSecrets Manager
の値をCRUDできるシェルを作成しました。
基本的にはコンソール上からSecrets Manager
は触らずに、全てシェル経由で操作するという運用方針です。
現在の環境変数の確認
環境変数の追加/修正
シェルを起動して1
で値の追加、修正をできるようにしました。
新規のKEYを入力すると追加、既存のKEYを入力すると更新します。
不用意に更新が行われないように、更新内容のdiffを表示して本当に更新するかを尋ねてから更新を実行します。
ここで下記のようにAWSCURRENT
のほかにタイムスタンプで--version-stages
を保存することによって、変更履歴として保存しています。
TIMESTAMP=$(date +"%Y-%m-%d-%H:%M:%S")
# 変更履歴を残すためにステージ情報用の値を保存
aws secretsmanager put-secret-value \
--secret-id "$SECRETS_NAME" \
--secret-string "$updated_secret" \
--version-stages "$TIMESTAMP" \
--profile "$AWS_PROFILE"
if [[ $? -ne 0 ]]; then
echo "エラー: '$TIMESTAMP'ステージのシークレット更新に失敗しました"
exit 1
fi
# AWSCURRENT用の値を保存
aws secretsmanager put-secret-value \
--secret-id "$SECRETS_NAME" \
--secret-string "$updated_secret" \
--version-stages "AWSCURRENT" \
--profile "$AWS_PROFILE"
if [[ $? -ne 0 ]]; then
echo "エラー: AWSCURRENTステージのシークレット更新に失敗しました"
exit 1
fi
環境変数の削除
シェルを起動して2
で値の削除をできるようにしました。
動きはほとんど追加/更新と変わりません。
削除したいKEYを入力して削除します。
不用意に削除が行われないように、削除内容のdiffを表示して本当に削除するかを尋ねてから削除を実行します。
追加/更新と同じようにタイムスタンプで--version-stages
を保存することによって、こちらも変更履歴として保存しています。
変更履歴の確認
シェルを起動して3
で変更履歴の確認をできるようにしました。
いつ保存されたのか、バージョンID、を確認をできます。
ここで表示されたバージョンIDを次の箇所に入力することで、そのバージョンID時の環境変数を振り返ることができます。
シェルコード全文
#!/bin/bash
# 必須環境変数のチェック
if [[ "$AWS_PROFILE" == "" ]] || [[ "$SECRETS_NAME" == "" ]]; then
echo "必須の環境変数が設定されていません。"
echo "AWS_PROFILE: $AWS_PROFILE"
echo "SECRETS_NAME: $SECRETS_NAME"
exit 1
fi
# 最新の Secrets Manager の値を取得する関数
get_current_secret() {
echo ""
echo "現在のシークレットを取得中..."
CURRENT_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "$SECRETS_NAME" \
--query SecretString \
--output text \
--profile "$AWS_PROFILE" | jq -c '.')
if [[ $? -ne 0 ]]; then
echo "エラー: シークレット '$SECRETS_NAME' を JSON としてパースできませんでした。"
exit 1
fi
}
# Secrets Manager の更新処理を行う関数
update_secret() {
local updated_secret=$1
TIMESTAMP=$(date +"%Y-%m-%d-%H:%M:%S")
# 変更履歴を残すためにステージ情報用の値を保存
aws secretsmanager put-secret-value \
--secret-id "$SECRETS_NAME" \
--secret-string "$updated_secret" \
--version-stages "$TIMESTAMP" \
--profile "$AWS_PROFILE"
if [[ $? -ne 0 ]]; then
echo "エラー: '$TIMESTAMP'ステージのシークレット更新に失敗しました"
exit 1
fi
# AWSCURRENT用の値を保存
aws secretsmanager put-secret-value \
--secret-id "$SECRETS_NAME" \
--secret-string "$updated_secret" \
--version-stages "AWSCURRENT" \
--profile "$AWS_PROFILE"
if [[ $? -ne 0 ]]; then
echo "エラー: AWSCURRENTステージのシークレット更新に失敗しました"
exit 1
fi
echo "シークレット '$SECRETS_NAME' をバージョンステージ '$TIMESTAMP' と 'AWSCURRENT' に正常に更新しました。"
}
# 変更差分を表示する関数
calculate_diff() {
local old_secret=$1
local new_secret=$2
local diff_type=$3 # "added/changed" または "removed"
case $diff_type in
"added/changed")
jq -n --argjson old "$old_secret" --argjson new "$new_secret" '
{
added: ([$new | to_entries[] | select(.key as $k | $old[$k] == null)] | from_entries // {}),
changed: ([$new | to_entries[] | select(.key as $k | $old[$k] != null and $old[$k] != .value)]
| map({(.key): {old: $old[.key], new: .value}})
| add // {})
} | del(.added | select(length == 0)) | del(.changed | select(length == 0))
'
;;
"removed")
jq -n --argjson old "$old_secret" --argjson new "$new_secret" '
{
removed: ([$old | to_entries[] | select(.key as $k | $new[$k] == null)] | from_entries)
}
'
;;
*)
echo "{}"
;;
esac
}
while true; do
# 操作メニューの表示
echo "Secrets Manager 操作メニュー"
echo "0: 現在の値を確認"
echo "1: 環境変数の追加・更新 (KEYとVALUEを入力)"
echo "2: 環境変数の削除"
echo "3: バージョンを指定して値を取得"
echo "4: 終了"
echo "選択してください (0-4):"
read -r ACTION
# 選択によって分岐
case $ACTION in
0)
get_current_secret
echo "現在の値:"
echo "$CURRENT_SECRET" | jq
echo ""
;;
1)
echo ""
echo "環境変数の追加・更新を行います。"
get_current_secret
NEW_VALUES="{}"
while true; do
echo ""
echo "環境変数のKEYを入力してください:"
read -r INPUT_KEY
KEY=$(echo "$INPUT_KEY" | xargs)
echo ""
echo "環境変数のVALUEを入力してください(数値や真偽値を文字列として扱う場合はダブルクオーテーションで囲んでください):"
read -r INPUT_VALUE
VALUE=$(echo "$INPUT_VALUE" | xargs)
if [[ "$VALUE" =~ ^\".*\"$ ]]; then
# ダブルクオーテーションで囲まれている場合は文字列として扱う
PROCESSED_VALUE=$VALUE
elif [[ "$VALUE" =~ ^[0-9]+$ ]]; then
# 数値として扱う
PROCESSED_VALUE=$VALUE
elif [[ "$VALUE" == "true" || "$VALUE" == "false" ]]; then
# 真偽値として扱う
PROCESSED_VALUE=$VALUE
else
# その他の場合は文字列として扱う(自動的にダブルクオーテーションで囲む)
PROCESSED_VALUE="\"$VALUE\""
fi
NEW_VALUES=$(echo "$NEW_VALUES" | jq ". + {\"$KEY\": $PROCESSED_VALUE}")
while true; do
echo ""
echo "まだ追加/更新しますか? (yes/no):"
read -r CONTINUE
if [[ "$CONTINUE" == "no" ]]; then
break 2 # ループを抜けて追加/更新を終了
elif [[ "$CONTINUE" == "yes" ]]; then
break # ループを抜けて次の入力を求める
else
echo "無効な入力です。yes または no を入力してください。"
fi
done
done
UPDATED_SECRET=$(echo "$CURRENT_SECRET" | jq ". + $NEW_VALUES")
DIFF=$(calculate_diff "$CURRENT_SECRET" "$UPDATED_SECRET" "added/changed")
echo "更新する内容:"
echo "$UPDATED_SECRET" | jq
echo "変更された部分:"
echo "$DIFF" | jq
echo ""
echo "シークレット '$SECRETS_NAME' をこの内容で更新しますか? (yes/no):"
read -r CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "操作がキャンセルされました。"
echo ""
continue
fi
update_secret "$UPDATED_SECRET"
;;
2)
echo "シークレットに保存された環境変数を削除します。"
get_current_secret
echo "現在の値:"
echo "$CURRENT_SECRET" | jq
echo "削除したい環境変数のKEYを入力してください:"
read -r INPUT_KEY
KEY=$(echo "$INPUT_KEY" | xargs)
if ! echo "$CURRENT_SECRET" | jq -e "has(\"$KEY\")" > /dev/null; then
echo "エラー: 指定されたKEY '$KEY' は存在しません。"
echo ""
continue
fi
UPDATED_SECRET=$(echo "$CURRENT_SECRET" | jq "del(.\"$KEY\")")
DIFF=$(calculate_diff "$CURRENT_SECRET" "$UPDATED_SECRET" "removed")
echo "更新する内容:"
echo "$UPDATED_SECRET" | jq
echo "変更された部分:"
echo "$DIFF" | jq
echo ""
echo "シークレット '$SECRETS_NAME' をこの内容で更新しますか? (yes/no):"
read -r CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "操作がキャンセルされました。"
echo ""
continue
fi
update_secret "$UPDATED_SECRET"
;;
3)
echo "バージョン一覧を取得中..."
NEXT_TOKEN=""
ALL_VERSIONS="[]"
while : ; do
# NEXT_TOKENを利用して全てのデータを取得する
RESULT=$(aws secretsmanager list-secret-version-ids \
--secret-id "$SECRETS_NAME" \
--output json \
--profile "$AWS_PROFILE" \
${NEXT_TOKEN:+--next-token $NEXT_TOKEN})
# ページング結果を結合
PAGE_VERSIONS=$(echo "$RESULT" | jq '.Versions | map({VersionId, CreatedDate, VersionStages})')
ALL_VERSIONS=$(echo "$ALL_VERSIONS $PAGE_VERSIONS" | jq -s 'add')
# 次のトークンを取得
NEXT_TOKEN=$(echo "$RESULT" | jq -r '.NextToken // empty')
# トークンが空ならループを終了
if [[ -z "$NEXT_TOKEN" ]]; then
break
fi
done
# 全データをソートして表示
SORTED_VERSIONS=$(echo "$ALL_VERSIONS" | jq 'sort_by(.CreatedDate)')
echo "$SORTED_VERSIONS" | jq
echo ""
echo "特定バージョンIDを指定して値を取得します。"
echo "VersionIdを入力してください:"
read -r INPUT_VERSION_ID
VERSION_ID=$(echo "$INPUT_VERSION_ID" | xargs)
VALUE=$(aws secretsmanager get-secret-value \
--secret-id "$SECRETS_NAME" \
--version-id "$VERSION_ID" \
--query SecretString \
--output text \
--profile "$AWS_PROFILE")
if [[ $? -ne 0 ]]; then
echo "指定されたバージョンの値が見つかりませんでした"
echo ""
continue
fi
echo "指定されたバージョンの値:"
echo "$VALUE" | jq
;;
4)
echo "終了します。"
exit 0
;;
*)
echo "無効な値です。正しい値を入力してください"
echo ""
;;
esac
done