AWS Secrets Manager のシークレット値を安全に文字列化
AWS Secrets Manager で取得したシークレットが JSON のプリミティブ型(整数・真偽値・null)であっても文字列化し、kamal secrets extract の #chomp が失敗しないようにしました。この変更により、バッチ取得時に発生していたクラッシュが根本的に解消され、CLI の安定性が向上します。
背景
非文字列シークレットがクラッシュの原因になることは Issue #1816 で報告されていました。JSON.parse が裸の JSON 値(例: "5432")を数値や真偽値として返すため、fetch_secrets が取得した secret_string に対して .each を呼び出すと NoMethodError が発生します。特に kamal secrets fetch が複数シークレットを同時に取得する際、1 件でもプリミティブ型が混在すると全体が例外で終了していました。
技術的な変更
fetch_secrets に型ガードを導入し、ハッシュかどうかで処理を分岐させました。ハッシュの場合は各キー‑値ペアを走査し、ハッシュでない場合はそのまま文字列化して結果に格納します。
変更前:
secret_string.each do |key, value|
results["#{secret_name}/#{key}"] = value
end
変更後:
if secret_string.is_a?(Hash)
secret_string.each do |key, value|
results["#{secret_name}/#{key}"] = stringify_secret_value(value)
end
else
results["#{secret_name}"] = stringify_secret_value(secret_string)
end
新たに追加されたヘルパーメソッド stringify_secret_value は、値が文字列であればそのまま、文字列でなければ JSON.dump で JSON 文字列に変換します。
def stringify_secret_value(value)
value.is_a?(String) ? value : JSON.dump(value)
end
この実装により、整数や真偽値、null などの非文字列型が安全に文字列化され、#chomp が期待通りに動作します。テストファイル test/secrets/aws_secrets_manager_adapter_test.rb に、裸の JSON プリミティブを対象としたテストとハッシュ内の非文字列値が正しく文字列化されるテストが追加され、変更の信頼性が検証されています。
設計判断
JSON.dump を選択した理由は、to_s では JSON 配列やオブジェクトの文字列表現が期待と異なり、null が空文字になるなど予期せぬ振る舞いを引き起こす点を回避できるためです。JSON.dump は Ruby の標準 JSON エンコーダであり、非文字列型を正確に JSON 形式の文字列へ変換します。エッジケースとして null は文字列 "null" として出力され、クォートされた文字列は JSON.parse によって自動的にデコードされるため、余計な加工を行わずにそのまま利用できます。
設計上のトレードオフとして、再帰的な JSON 文書は対象外とし、フラットなシークレットを前提にしました。これにより追加の CLI オプションや複雑な型変換ロジックを導入する必要がなく、既存インターフェースを壊さずに安全性だけを向上させる選択となります。
まとめ
この PR は、AWS Secrets Manager から取得した非文字列シークレットを JSON.dump で安全に文字列化し、kamal secrets extract の #chomp エラーを防止します。型ガードと文字列化ロジックの導入により、バッチ取得時のクラッシュが根本的に解消され、CLI の安定性とユーザー体験が向上しました。