Septeni Engineer's Blog

セプテーニ・オリジナルのエンジニアが綴る技術ブログ

AWS System Manager Parameter Store と typesafe config をいい感じに組み合わせたい

こんにちは。AWS好きの河内です。今年も早いものでもう3月ですね。

サービスで利用するアクセスキーやパスワードなどの機密情報管理、どうやるのがスマートなのか思いを巡らせています。 PaaS 環境なら機密情報を管理するための仕組みが用意されていることが多そうですが、今回は EC2 の上で直接動いているサービスが対象です。AWS では System Manager Parameter Store が提供されています。

AWS Systems Manager パラメータストア は、設定データ管理と機密管理のための安全な階層型ストレージを提供します。

私の身の回りでは Scala 、特に Play で作られているアプリケーションが多いので parameter store と typesafe config と組み合わせる方法を模索しました。

案1. 環境変数で読み込む

設定の注入といえば環境変数ですね。 The twelve-factor app でも推奨されています。

12factor.net

すでに parameter store の内容を環境変数に設定するプログラムを書いている人も居ます。

a3no.hatenablog.com

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 環境変数を使う方法です。

docs.oracle.com

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 からパラメータを取得し、システムプロパティに設定するだけです。

github.com

使ってみましょう。

お試しコードは 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.conf1my.conf2 を設定から読み込み表示しています。 まず sbt run でそのまま動かしてみます。

f:id:tkawachi:20180301142910p:plain

予期したとおり None, None になっています。

つぎに parameter store に次の設定をします。

  • /service1/common/my.conf1A に設定
  • /service1/common/my.conf2B に設定
  • /service1/prod/my.conf2C に設定
  • /service1/prod/my.conf3D に設定

f:id:tkawachi:20180301144200p:plain

なんとなく /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 以下の設定が読み込まれます。

ではブラウザでアクセスしてみましょう。

f:id:tkawachi:20180301143200p:plain

予期したとおり my.conf1/service1/common/my.conf1 から読み込まれて A に、 my.conf2/service1/prod/my.conf2 から読み込まれて C になりました。

PSBridge を使うとコードや設定ファイルを書き換えることなく parameter store の値を使って typesafe config の設定を手軽に上書きできます。 typesafe config の設定を上書きすることを念頭に考えましたが、実のところシステムプロパティを設定しているだけなので、システムプロパティを設定する他のシナリオでも利用可能です。 もっと良い方法や、この方法に問題があれば是非 @kawachi まで教えて下さい。

*1:まだプロダクションで使用したことはありません。