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以外の環境で動作させる際にはコードの分岐が必要になります

AWSコスト異常検知を導入したら、『人にお願いする』トイルが発生したのでSlackBotを作って解消した

SREチームの池田(@mashiike)です。SRE連載の5月号になります。

AWSのコストについては、多くの方がすごく気にしていると思います。 カヤックでもAWSのコストの変動に関しては敏感に気にしています。

そんな方々の心のお供になる機能が、 AWSコスト異常検知(AWS Cost Anomaly Detection) です。 今回は、このコスト異常検知にまつわるトイル削減の取り組みを紹介します。

背景

AWSコスト異常検知は、AWS マネジメントコンソールの中では『Billing and Cost Management』配下にある機能になります。 この機能を使うことでAWSで発生したコストに関して、通常とは異なるコストの発生を検知することができます。

コスト異常検知自体については、CureApp テックブログ様のZennの記事がわかりやすくまとまっているので、そちらを参照いただければ幸いです。

zenn.dev

aws.amazon.com

さて、ここからはカヤック特有かもしれない背景があります。 カヤックには、ゲーム事業を担当する事業部やクライアントワークを担当する事業部など、多種多様にあります。 請求や権限分離の都合上、事業や案件ごとにAWSアカウントを開設しているため、その数は 100近く に及びます。 そして、それらのAWSアカウントはAWS Organizationsでまとめて管理されており、一つのPayerアカウントに紐づけられています。  

そんな状況で、100近くあるアカウントそれぞれにコスト異常検知を設定・管理するのは大変です。 そのため、カヤックではPayerアカウントに対してコスト異常検知を設定し、Organization全体でまとめてコスト異常検知を行っています。

以下のスクリーンショットは、導入当時に発生したとあるアカウントでのRedshiftのRI切れのコスト異常を検知した際のものです。  

このようにPayerアカウント1つにコスト異常検知を設定するだけで、それに紐づく連結アカウントのコスト異常を素早く検知することができました。

ここまでは良いのですが、ここから次の2つのことをする必要があります。

  1. PayerアカウントのCostExplorerを閲覧し、コストの変化状況を確認する。
  2. Payerアカウントで、検出されたコスト以上に対して『正常な異常』『誤検知』『問題ではありません』のフィードバックをする。

そして、このときに発生する問題が、『限られた人しかアクセスができない Payerアカウント』で上記の2つを行う必要が有るという点です。 コスト異常が発生するたびに『Payerアカウントにアクセス権を持つ人』に上記の2つを依頼するというトイルが生まれてしまったのです。

人に頼めばトイル、SlackBotに頼めばトイルじゃない!

上記のトイルを削減するために生まれたSlackBotが aws-cost-anomaly-slack-reactor になります。

github.com

このSlackBotがやることは単純です。

  1. SNS Topic経由でコスト異常が通知されたら、BotのLambdaが起動され、『正常な異常』『誤検知』『問題ではありません』の3つのアクションボタンがついた通知をSlackに投稿する。
  2. その通知メッセージの返信に、関連するコストの変化状況のグラフを投稿する。
  3. アクションボタンが押されたら、Lambda Function URL経由でLambdaが起動され、対応するコスト異常にフィードバックを与える。  

『Payerアカウントにアクセス権を持つ人』に頼んでいた内容をSlackBotが肩代わりしてくれます。 もちろんLambda Function URLはパブリックにアクセス可能なよう設定できるので、『Payerアカウントにアクセス権を持つ人』でない人のアクションも受け付けられるというわけです。  

動作の様子は次のスクリーンショットのようになっています。

(これは、開発用アカウントでBedrockのTaitan Image Generator G1 モデルを検証目的で初めて使ってみたときのものです。今まで使ったこと無いものを初めて使ったのでもちろんコスト異常です)

このSlackBotを導入したことで、コスト異常が発生するたびに起きていた『Payerアカウントにアクセス権を持つ人』に依頼するというトイルを削減することができました。

まとめ

AWSコスト異常検知は、AWSのコストに関して通常とは異なるコストの発生を検知することができる機能です。 カヤックでは、Payerアカウントに対してコスト異常検知を設定し、Organization全体でのコスト異常検知を行っています。  

しかし、Payerアカウントという性質上、限られた人しかアクセスできず、コスト異常が発生するたびに『Payerアカウントにアクセス権を持つ人』に操作を依頼するというトイルが生まれてしまいました。 そこで、SlackBotを導入することで、人ではなくSlackBotに依頼するという形で、コスト異常が発生するたびに起きていたトイルを削減することができました。  

このように日々の運用でも、ちょっとした依頼を人にするということを繰り返していないでしょうか?それ、多分トイルです。

カヤックでは、日々発生するちょっとしたトイルを削減していくエンジニアも募集しています。