こんにちは。AWS好きの河内です。今年も早いものでもう3月ですね。
サービスで利用するアクセスキーやパスワードなどの機密情報管理、どうやるのがスマートなのか思いを巡らせています。 PaaS 環境なら機密情報を管理するための仕組みが用意されていることが多そうですが、今回は EC2 の上で直接動いているサービスが対象です。AWS では System Manager Parameter Store が提供されています。
AWS Systems Manager パラメータストア は、設定データ管理と機密管理のための安全な階層型ストレージを提供します。
私の身の回りでは Scala 、特に Play で作られているアプリケーションが多いので parameter store と typesafe config と組み合わせる方法を模索しました。
案1. 環境変数で読み込む
設定の注入といえば環境変数ですね。 The twelve-factor app でも推奨されています。
すでに parameter store の内容を環境変数に設定するプログラムを書いている人も居ます。
typesafe config の HOCON 文法では環境変数を参照することができます。
application.conf
で次のように書いておけば、DATABASE_URL
環境変数が設定されているときのみ db.default.url
が上書きされます。
db.default.url=jdbc://default/url db.default.url=${?DATABASE_URL}
typesafe config との組み合わせで考えると、上書きしたい項目ごとに予め my.config=${?ENV_VAR}
のような行を application.conf
に追加しておく必要がある点がイマイチです。
環境変数を使うアイデア自体は捨てがたいものがありますが、他の方法を模索することにしました。
案2. 引数でシステムプロパティを設定する
typesafe config ではシステムプロパティで既存の設定を上書きできます。
-Ddb.default.url=jdbc://production/url
のような引数を与えれば db.default.url
設定を上書きできます。
Parameter store を参照して引数を生成するプログラムを用意しておけば、楽に引数を指定できそうです。
しかし、ps
コマンドなどでプロセスをリストした際に引数は出力されます。
今回は機密情報も扱いたいので、同ホストにログインできる誰からでも参照できるのは望ましくありません。
案3. JAVA_TOOL_OPTIONS でシステムプロパティを設定する
引数に現れないでシステムプロパティを設定する方法は無いのでしょうか?
ひとつ見つけました。 JAVA_TOOL_OPTIONS
環境変数を使う方法です。
JAVA_TOOL_OPTIONS
環境変数に -Ddb.default.url=jdbc://production/url
のように設定しておけば、JVM 起動時に引数として解釈されます。しかもプロセスリストには出てきません。
コレでいいか…と思ったのですがこの方法にも問題があります。
ひとつは
Picked up JAVA_TOOL_OPTIONS: -Ddb.default.url=jdbc://production/url
のような出力が標準エラーに出力されること。機密情報を想定しているので出力してほしくありません。
もうひとつは、getuid() != geteuid あるいは getgid != getegid のときには読み込まれない ようです。
思わぬところで落とし穴にはまることが出てきそうです。
案4. Java agent でシステムプロパティを設定する
引数に現れない方法でシステムプロパティを設定する方法、 typesafe config が最初に動作する前にシステムプロパティを設定できれば良いのだが…何か方法はないだろうか。と考えてたときに Java agent のことを思い出しました。
java.lang.instrument (Java Platform SE 8 )
Java agent はバイトコードを変換するために使われる機構です。
Provides services that allow Java programming language agents to instrument programs running on the JVM. The mechanism for instrumentation is modification of the byte-codes of methods.
今回はバイトコードを変換したいわけではありませんが、 Java agent のエントリポイントとして使われる premain()
関数は main()
関数実行前に呼ばれるため、システムプロパティを設定するには良いタイミングです。
実際に書いてみました*1。単に AWS SDK を使って parameter store からパラメータを取得し、システムプロパティに設定するだけです。
使ってみましょう。
お試しコードは Play Scala Starter Example の app/controllers/HomeController.scala
を次のように書き換えました。
package controllers import javax.inject._ import play.api.mvc._ import play.api.Configuration @Singleton class HomeController @Inject()(cc: ControllerComponents, config: Configuration) extends AbstractController(cc) { def index = Action { Ok(views.html.index( s"my.conf1: ${config.getOptional[String]("my.conf1")}, " + s"my.conf2: ${config.getOptional[String]("my.conf2")}" )) } }
my.conf1
と my.conf2
を設定から読み込み表示しています。
まず sbt run
でそのまま動かしてみます。
予期したとおり None, None になっています。
つぎに parameter store に次の設定をします。
/service1/common/my.conf1
をA
に設定/service1/common/my.conf2
をB
に設定/service1/prod/my.conf2
をC
に設定/service1/prod/my.conf3
をD
に設定
なんとなく /service1/prod/my.conf2
は SecureString にしてみました。
機密度が高いものは SecureString にすると良いでしょう。
Parameter store は /
区切りで階層化できます。
service1
を運用していて、共通設定が /service1/common
以下に、本番環境用設定が /service1/prod
以下にあるという想定です。
psbridge.jar をダウンロードします。 次のコマンドで実行します。
$ aws-vault exec myprofile -- sbt -J-javaagent:psbridge.jar=/service1/common,/service1/prod run
Parameter store へアクセスするために AWS の credential が必要になります。 私は aws-vault で開発用アクセスキーを管理しているので、aws-vault 経由で実行しています。 本番環境では instance profile に role を紐付けて運用していることが多いと思うので、明示的な credential 指定は不要です。
sbt に対して java の引数を指定するには -J
プレフィックスを付ける必要があるので -J-javaagent:
を指定しています。 /service1/common,/service1/prod
と指定することで、まず /service1/common
以下の設定が読み込まれ、次に /service1/prod
以下の設定が読み込まれます。
ではブラウザでアクセスしてみましょう。
予期したとおり my.conf1
は /service1/common/my.conf1
から読み込まれて A
に、 my.conf2
は /service1/prod/my.conf2
から読み込まれて C
になりました。
PSBridge を使うとコードや設定ファイルを書き換えることなく parameter store の値を使って typesafe config の設定を手軽に上書きできます。 typesafe config の設定を上書きすることを念頭に考えましたが、実のところシステムプロパティを設定しているだけなので、システムプロパティを設定する他のシナリオでも利用可能です。 もっと良い方法や、この方法に問題があれば是非 @kawachi まで教えて下さい。
*1:まだプロダクションで使用したことはありません。