less is more

心のフルスタックエンジニア👨‍💻バイブスでコードを書いています🤘

CloudFormationで使われていないExportを洗い出す

f:id:bluepixel:20200505223512p:plain

CloudFormationにおいてスタック間で値を受け渡したいときに、参照される側で出力値をエクスポートして、参照する側でFn::ImportValueで読み込む方法があります。

docs.aws.amazon.com

エクスポートされている値は各スタック詳細の出力や[エクスポート]から見ることができます。

f:id:bluepixel:20200505221611p:plain

f:id:bluepixel:20200505221623p:plain

どのスタックからインポートされているかも確認できます。

f:id:bluepixel:20200505221646p:plain

ただし、どこからも参照されていないエクスポート値を調べるのはマネジメントコンソールでは無理なのでAPIを使って洗い出すスクリプトを書きました 。

まず最初にスタックIDとスタック名のリストを作ります。 これは、後に出てくるAPIのレスポンスにスタックIDしか含まれていないので、わかりやすいスタック名を表示するためです。

def list_stacks(result = {}, next_token = nil)
  sleep 1
  resp = @client.list_stacks(:next_token => next_token)
  resp.stack_summaries.each do |stack|
    result[stack.stack_id] = stack.stack_name
  end
  list_stacks(result, resp.next_token) if resp.next_token
  result
end

スタックIDをキー、スタック名をバリューとしたHashが返ります。
この手のAWSAPIトークンベースのページングになっているので、結果がある限りループします。ループの部分は再帰で実装しています。

あとドキュメントが見つからなかったのですが、数が多いとスロットリングが発生するので1秒起きにリクエストを送っています。スタック数、エクスポート数ともに50くらいの環境では1秒の待機でいけました。

次にエクスポート値を全て取得します。

def list_exports(result = {}, next_token = nil)
  sleep 1
  resp = @client.list_exports(:next_token => next_token)
  resp.exports.each do |export|
    result[export.name] = { :value => export.value, :stack_id => export.exporting_stack_id }
  end
  list_exports(resp.next_token) if resp.next_token
  result
end

これも同様にページングです。
エクスポート名をキーにした、エクスポート値と出力しているスタックIDのHashを返します。

次にエクスポートごとにインポート状態の確認を行います。

def list_imports(export_name, result = [], next_token = nil)
  sleep 1
  resp = @client.list_imports(:export_name => export_name, :next_token => next_token)
  result << resp.imports
  list_imports(export_name, result, resp.next_token) if resp.next_token
  result
rescue Aws::CloudFormation::Errors::ValidationError => e
  return [] if e.message.include?("is not imported by any stack")
  raise e
end

戻り値は参照しているスタック名の配列です。

どこからも参照されていない場合、ValidationErrorが発生するのでハンドリングしてます。 これはこのケースに限らない共通のエラークラスなので、一応詳細なエラーメッセージも判定するようにしています。

あとは、最初に取得しておいたスタックIDとスタック名のマップを使ってよしなに変換を行い、どこからもインポートされていないエクスポートをプリントして終わりです。

if __FILE__ == $0
  @client = Aws::CloudFormation::Client.new
  stacks = list_stacks
  exports = list_exports
  puts "following exports value is not used in any stack."
  exports.keys.each do |key|
    exports[key][:importing_stacks] = list_imports(key)
    puts "#{key} (defined in #{stacks[exports[key][:stack_id]]})" if exports[key][:importing_stacks].size == 0
  end
end

思いの外面倒でした。

ソースコードはこちら github.com