AWS Systems Manager Parameter Storeを便利に使うツール "ssmwrap" がv2になりました

SREチームの長田です。 今回はssmwrapという拙作CLIツールのはなしです。

ssmwrapとは

ssmwrapは、AWS Systems Manager Parameter Store(以下SSM Params)から値を取得し、 環境変数またはファイルに出力した上でコマンドを実行するツールです。

secret類をSSM Paramsに保存している場合、アプリケーション実行時にSSM Paramsから必要な値を取得することになります。 AWSのサービスにアクセスするという操作は、それなりに手間がかかるものですが、 ssmwrapを使えば環境変数とファイルというより簡便な入出力インターフェイスを通してSSM Paramsの値を参照できます。 実装が簡潔になるだけでなく、アプリケーションからのAWS APIへの依存を排除することにもなります。

# SSM Paramsにこんな値が保存されている場合に、
$ aws secretsmanager get-secret-value \
  --secret-id test/foo \
  --query 'SecretString'
"secret value"

$ ssmwrap {"test/foo" を環境変数 "FOO" に出力するオプションを指定して} \
  -- \
  printenv FOO # コマンドを実行すると、
secret value   # 環境変数経由で参照できる!

AWS APIへの依存排除は、ローカル環境やCI環境など、AWSにアクセスしづらく、かつダミーのsecretで事足りる場合にも便利です。 ダミーの(平文で保存しても問題ない)値を環境変数やファイルに書き出して、アプリケーションからはそれを参照すればいいわけです。

この度、このssmwrapが メジャーバージョンアップしv2 になりました *1

何が変わったのか

主な変更点は以下の4つです。

  • aws-sdk-go から aws-sdk-go-v2 への移行
  • CLIインターフェイスの刷新
  • ライブラリインターフェイスの変更
  • パフォーマンス向上

それぞれ詳しく見ていきます。

aws-sdk-go から aws-sdk-go-v2 への移行

sssmwrapのv2化に着手したきっかけは、aws-sdk-go v1のサポート終了アナウンスでした。

ssmwrapもgoで書かれたAWSを利用するツールということで、aws-sdk-goを利用しており、 今回aws-sdk-go-v2に移行しました。

aws-sdk-go-v2の各関数は context.Context を要求するので、 *2 ssmwrapのライブラリインターフェイスも変わることになります。

semantic versioningに則るなら、インターフェイスの変更はメジャーバージョンアップを伴わないとなー v2に上げるなら他のインターフェイスも整理したいなー ということでCLIのインターフェイスも刷新することにしました。

CLIインターフェイスの刷新

ssmwrap v1のCLIとしての使用例は、例えば以下のようなものでした。

# ssmwrap v1
$ ssmwrap \
    -paths /foo/ \
    -prefix FOO_ \
    -file 'Name=/foo/CERT,Path=/path/to/foo.crt,Mode=600' \
    -file 'Name=/foo/KEY,Path=/path/to/foo.key,Mode=600' \
    -- app run

このフラグの意味は以下のとおりです。

  • -paths /foo/ -prefix SSMWRAP_
    • /foo/ 直下にあるパラメータを取得し、 各パラメータ名にプレフィックス FOO_ をつけて環境変数にセットする
  • -file Name=/foo/CERT,Path=/path/to/foo.crt,Mode=600
    • /foo/CERT の値を取得し、 /path/to/foo.crt に書き出す。 ファイルのパーミッションは 600 とする
  • -file Name=/foo/KEY,Path=/path/to/foo.key,Mode=600
    • 同上
  • -- app run
    • コマンド app run を実行する。実行時にssmwrapがセットした環境変数を参照できる

はじめは環境変数への出力しか想定していなかったところに、 ファイルへの出力機能を追加したため、統一感のないインターフェイスになっていました。 そのため、

  • -prefix は環境変数出力専用だが、ファイル出力にも効くように見える、
  • 環境変数として出力する対象のSSM Paramを指定するオプション -paths と ファイル出力先指定である -file 内の Path が同じ単語なので紛らわしい

などの問題を感じていました。

これをssmwrap v2仕様に書き換えると以下のようになります。

# ssmwrap v2
$ ssmwrap \
    -rule 'type=env,path=/foo/*,prefix=FOO_' \
    -rule 'type=file,path=/foo/CERT,to=/path/to/foo.crt,mode=600' \
    -rule 'type=file,path=/foo/KEY,to=/path/to/foo.key,mode=600' \
    -- app run

-env -file フラグが -rule に統一されました。 SSM Paramsの対象パラメータを指定する方法も以下のように変更されいます。

対象パラメータ v1 v2
/foo/value という名前のパラメータのみを処理対象とする -names /foo/value -rule 'type=...,path=/foo/value,...'
/foo/ 直下にあるパラメータのみを処理対象とする -paths /foo/ -rule 'type=...,path=/foo/*,...'
/foo/ 以下にある全てのパラメータを再帰的に探索し処理対象とする -paths /foo/ -recursive -rule 'type=...,path=/foo/**/*,...'

このようにどのパターンも -rule フラグのみで表現するようになりました。 これにより、 「/foo/ は直下のパラメータのみ、/bar/ は再帰的に全てのパラメータを処理対象とする」 といった 複雑なルールも表現できるようになりました。

また、ssmwrap v1における -env-prefix および --env-entire-path-rule フラグ内のパラメータとして指定するようになりました。 これにより、以下のように path 毎に異なるプレフィックスを付けることも可能になりました。

# ssmwrap v2
ssmwrap \
    -rule 'type=env,path=/foo/*,prefix=FOO_' \
    -rule 'type=env,path=/bar/*,prefix=BAR_' \
    ...

なお、-rule フラグのエイリアスとして -env -file を用意していますので、 これを使えば多少短く書くこともできます。

-rule を使う場合 -env -file を使う場合
-rule 'type=env,...' -env '...'
-rule 'type=file,...' -file '...'

ライブラリインターフェイスの変更

ssmwrapは、ライブラリとして利用するためのインターフェイスも提供しています。

ssmwrap.Export を実行すると、 SSM Parameterから取得した値を指定したルールに従って環境変数にエクスポートします。 以降の処理でエクスポートされた環境変数を参照することができます。

こちらのインターフェイスについても、CLIインターフェイスにあわせる形で変更しています。

// ssmwrap v1
opts := ssmwrap.ExportOptions{
    Paths: []string{"/foo"},
    Prefix: "TEST_",
}

if err := ssmwrap.Export(opts); err != nil {
    fmt.Fprintf(os.Stderr, "failed to export parameters: %v", err)
    os.Exit(1)
}

// 環境変数を参照する処理...
// ssmwrap v2
ctx := context.Background()

rules := []ssmwrap.ExportRule{
    {
        Path:   "/foo",
        Prefix: "TEST_",
    },
}

if err := ssmwrap.Export(ctx, rules, ssmwrap.ExportOptions{}); err != nil {
    fmt.Fprintf(os.Stderr, "failed to export parameters: %v", err)
    os.Exit(1)
}

// 環境変数を参照する処理...

また、v2化にあたり、importパスも変更しています。ご注意ください。

// ssmwrap v1
import "github.com/handlename/ssmwrap"
// ssmwrap v2
import "github.com/handlename/ssmwrap/v2"

パフォーマンス向上

ssmwrap v1では、例えば以下のようなフラグを渡した場合、AWSへのAPIリクエストが3回発生していました。

# ssmwrap v1
$ ssmwrap \
    -paths /foo/ \ <-- GetParametersByPath
    -prefix FOO_ \
    -recursive \
    -file 'Name=/foo/CERT,Path=/path/to/foo.crt,Mode=600' \ <-- GetParameter
    -file 'Name=/foo/KEY,Path=/path/to/foo.key,Mode=600' \ <-- GetParameter
    -- app run

ssmwrap v2では重複する範囲のSSM Parameterの取得は省略されるため、AWSへのAPIリクエスト回数が減少しています。 先の ssmwrap v1 の例と同等の処理を ssmwrap v2 で行う場合のAPIリクエストは 1回 で済みます (/foo/*/foo/CERT /foo/KEY を含んでいるため)。

# ssmwrap v2
$ ssmwrap \
    -rule 'type=env,path=/foo/*,prefix=FOO_' \ <-- GetParametersByPath
    -rule 'type=file,path=/foo/CERT,to=/path/to/foo.crt,mode=600' \ <-- リクエストは発生しない
    -rule 'type=file,path=/foo/KEY,to=/path/to/foo.key,mode=600' \ <-- リクエストは発生しない
    -- app run

APIクエストが少ない分単純に速いですし、スループット制限 *3 にもかかりにくくなります。

おわりに

ssmwrapは、もともとはEC2上で動かすアプリケーションからSSM Paramsを参照するために作られたツールでした。

現在はアプリケーションの動作にはECSを使うことが主流になっています。 ECSであれば、Task Definitionに secrets パラメータを設定する *4 だけで、コンテナ内から環境変数として SSM Params の値を参照できるので、ssmwrapを使う機会は減ることになりました。

しかし、Lambda functionから手軽にSSM Paramsを参照する場合 *5 や、GitHub ActionsなどのCI/CDサービスでの利用、開発環境セットアップ時の開発用証明書の配置など、まだ活用できる場面はありそうだということで、今回v2化を行うことにしました。

他にもお役に立てるユースケースがあれば幸いです。

*1:執筆時点の最新バージョンは v2.1.0 です。

*2:V2 AWS SDK for Go adds Context to API operations | AWS Developer Tools Blog https://aws.amazon.com/jp/blogs/developer/v2-aws-sdk-for-go-adds-context-to-api-operations/

*3:記事執筆時点のスループット(1秒あたりのトランザクション数)は、 デフォルトでは40、 高スループット時はGetParameter=10,000 GetParameters=1,000 GetParameterByPath=100 となっています。詳細はAWSのドキュメント AWS Systems Manager エンドポイントとクォータ を参照してください。

*4:AWSのドキュメントAmazon ECS 環境変数を使用して Systems Manager パラメータを取得するを参照ください。

*5:Lambda exetension経由で参照することはできますが、 構成が少々煩雑になりますし、ローカル等Lambda以外の環境で動作させる際にはコードの分岐が必要になります