AWS
terraform
frontend
その他
www.slideshare.net
github.com ActiveMailerにSESを追加するためのもの、というイメージが強かったが、いつのまにか色々な機能が増えていた。
後述する、lambdaでdockerイメージを使える機能と組み合わせた、active jobのlambdaバックエンドはうまく管理する方法を考えられれば検討に値しそう。
今月はre:Inventが開催中であることもあって、AWS関連の諸々が非常に多い。
aws.amazon.com dev.classmethod.jp lambdaがdockerイメージをサポートした。 これまでlambdaのデフォルト実行環境にない共有ライブラリを使用したりする際にはlambda layerなる独自のレイヤを重ねなければならなかったことを考えると簡単になったと言える。
この記事は第二のドワンゴ Advent Calendar 2020 12月7日の記事です。
AWSのSystem Managerにはパラメータストアという機能があります。
これはサーバサイドアプリケーションの動作に必要な各種の設定情報を、必要であれば暗号化とともに格納しておける機能です。
一方で、サーバサイドアプリケーションをサービスインフラ上で動作させる方法のガイドラインの有名なものにThe Twelve-Factor Appがあり、その中で環境ごとに異なる設定情報は環境変数から注入せよ、という方針が示されています。
これは特にサーバサイドアプリケーションをdockerコンテナに包んで動作させる際に重要です。
この2つを合わせて考えると、パラメータストアに格納されている値を環境変数へ展開した後でサーバサイドアプリケーションを起動したくなってきますよね。
実はAWSのコンテナオーケストレーションサービスであるところのECSには、その機能があります。
じゃあそれでいいんじゃないか、と思いきや、これが絶妙に手間がかかる。
{ "containerDefinitions": [{ "secrets": [{ "name": "environment_variable_name", "valueFrom": "arn:aws:ssm:region:aws_account_id:parameter/parameter_name" }] }] }
Systems Manager Parameter Store を使用して機密データを指定する - Amazon ECS
というように、ECSのコンテナ定義の中に展開後の環境変数名と展開元となるパラメータストアのパラメータのarnを両方書かなければならないんですよね。
これがどのように困るかというと、
となるわけです。
新たな設定値を作るのなんてそれほど多いわけではないけれどたまにある、くらいの頻度なので余計にうっかりしてしまいやすく。
次に思いつくのが、パラメータストアから環境変数へ展開するだけの単機能の実行ファイルをgoあたりのワンバイナリで書き、使う方法でしょう。
これは本当にお手軽な方法で、お手軽がゆえに様々な人が思いついてそれぞれが作っています。 知ってるだけで6個もある。
github.com github.com github.com github.com github.com github.com
そしてお手軽すぎてそれぞれが作ってるがゆえにどれもデファクトにはなっておらず、残念ながらメンテされているのか怪しいものばかり。
そこでこの記事の本題。
AWS CLIとjqを使って展開してしまいましょう。AWS CLIはAWS謹製なので当然メンテされていますし、jqも汎用ツールです。
具体的にはこのようにします。
これが何をやっているかというと、まずaws ssm get-parameters-by-pathサブコマンドの出力は下記のような形式になっています。
$ cat response { "Parameters":[{ "Name": "/service_name/dev/HOGE", "Value": "hoge\\nvalue" },{ "Name": "/service_name/dev/FUGA", "Value": "fuga value" }] }
これのParametersキーの内容を各オブジェクトに分解し…
$ cat response | jq -r -c '.Parameters[]' {"Name":"/service_name/dev/HOGE","Value":"hoge\\nvalue"} {"Name":"/service_name/dev/FUGA","Value":"fuga value"}
jqのstring interpolation記法を使ってbashのexport形式に加工しています。
$ cat response | jq -r -c '.Parameters[] | "export \(.Name|split("/")[-1])=\"\(.Value)\""' export HOGE="hoge\nvalue" export FUGA="fuga value"
もう少し詳しく説明しますが、まず\(~)
がjqのstring interpolation記法です。
.Name|split("/")[-1]
の部分はNameキーの内容を"/"でsplitし、その配列中の末尾要素を取り出しています。これが展開後の環境変数名となります。
Valueキーの内容はそのままでよいので単に.Value
です。
.Value
の前後の\"
は単にエスケープしたダブルクオーテーションであって、これはjqのstring interpolationがダブルクオーテーションでなければ動作しなかったためにエスケープが必要でした。
というわけで、このbashスクリプトをこれをサーバサイドアプリケーションの起動直前にevalすれば、パラメータストアから環境変数へ秘密情報を展開した状態で起動できます。
そうすると、コンテナ定義に書くのは下記だけで良くなります。
{ "containerDefinitions": [ { "environment": [ { "name": "AWS_PARAMETER_STORE_REGION", "value": "ap-northeast-1" }, { "name": "AWS_PARAMETER_STORE_PREFIX", "value": "/service_name/dev/" } ], } }
パラメータの追加時も単にパラメータストアに作成するだけで、コンテナ定義を編集する必要はありません。
ついでに、/service_name/
以下を編集する権限をそのサービスの開発チームに付与すれば、設定値を変更する権限を安全に移譲できます。
実際、僕の仕事の担当サービスではマイクロサービスとまでは行かないものの複数のサブシステムが存在するため、それぞれの開発者にサブシステムの名前空間だけを編集する権限を移譲しています。
これを動作させるためにはECSコンテナにタスクロールとして与えるIAMロールにおいて下記アクションの許可が必要となります。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:GetParametersByPath", "kms:Decrypt" ], "Resource": [ "arn:aws:ssm:<region>:<aws_account_id>:parameter/<parameter_name>", "arn:aws:kms:<region>:<aws_account_id>:key/<key_id>" ] }, { "Effect": "Allow", "Action": [ "ssm:DescribeParameters" ], "Resource": ["*"] } ] }
なお、この方法には明確な弱点があって、jqとAWS CLI、そしてAWS CLIの依存であるpython3をインストールしなければならない関係で、コンテナイメージのサイズが肥大します。具体的には60MBほど。
つまり、
を天秤にかけた結果、最後のコンテナイメージ肥大を許容してAWS CLIとjqを使う方法に落ち着いたわけです。
これで設定値の追加時にコンテナ定義は編集しなくてよくなったわけですが、パラメータストアへのパラメータ作成もterraformで簡単に.してしまいましょう。
具体的には下記のようにします。
resource "aws_ssm_parameter" "parameter" { for_each = var.app_environment_variables type = "String" name = "/${var.service_name}/${var.env}/${each.key}" value = each.value } variable "env" {} variable "service_name" {} variable "app_environment_variables" {}
var.service_name
にはサービス名を、var.env
には環境名を渡しましょう。今回の場合はservice_name
とdev
ですね。
そして、var.app_environment_variables
に下記のようなmapを渡してあげると、既に示した例で出てくるような、パラメータがパラメータストア上に作成されます。便利。
module "parametor_store" { env = "dev" service_name = "service_name" app_environment_variables = { HOGE = "hoge\nvalue" FUGA = "fuga value" } }
一つ注意しなければならないのが、aws_ssm_parameter.parameterのtypeをStringにしているため、パラメータストア上に平文で保存される点です。
これをSecureStringにするとkmsの暗号鍵で暗号化して保存できますが、terraformで書くと結局平文の値がtfファイルやtfstateに書かれることになってしまうため、SecureStringにする意味が薄くなってしまいます。
そのため仕事では、SecureString型にする必要があるようなデータベースへの接続パスワードなどについてはあえてterraformで管理せず、Webコンソール上から直接手で作成しています。
何か良い方法があったら知りたい。
というわけで、AWS SSMのパラメータストアから環境変数へパス指定で秘密情報をAWS CLIとjqを使って一括展開する方法でした。
本当はECSがパス指定からの一括展開をネイティブでやってくれるようになればこのようなことはやらなくて良いのですが!
なにとぞ、どうにかなりませんか、AWSさん!!
タイトルの通り。
実は以前にも踏んだことがあるのだけれど、しばらく期間が開いてまた踏んでしまったので書き残しておくことにする。
ActiveModel::Validatorを継承するValidatorなんて日常的に書くわけではないのでたまに踏んでしまうのは仕方ないという話もあるが…
さて、ActiveRecordにはバリデーションを表現する方法が複数ある。
ある日、仕事でActiveModel::Validatorを継承したValidatorを作っていた。
そのValidatorではとある深遠な理由でバリデーション対象のレコ―ドから辿るのが少々面倒なレコードを複数回参照していたので、思わずValidator内で
def hoge_record @hoge_record ||= record.fuga.piyo.hoge end
というようにインスタンス変数にメモ化してしまった。
すると、request specで一つのテストケースのみを指定した場合はテストがpassするが、2つ以上のテストケースをまとめて実行するとfailするという事象に見舞われる。
これはActiveModel::Validationsが実行すべきValidatorをModelに登録する仕組みから来るもの。
具体的に言うと、Validatorがインスタンス化されるのはvalidate_withによってValidatorがModelに対応付けられるタイミングであって、そのインスタンスが何度も使いまわされるから。
そのため||=
を使ってインスタンス変数にメモ化してしまうと、初回のvalidateでメモ化したオブジェクトが入りっぱなしになってしまうため意図した動作にならない。
validate_withによってValidatorがModelに対応付けられるタイミングでValidatorがインスタンス化される、当該のコードはここ。
def validates_with(*args, &block) options = args.extract_options! options[:class] = self args.each do |klass| validator = klass.new(options, &block) if validator.respond_to?(:attributes) && !validator.attributes.empty? validator.attributes.each do |attribute| _validators[attribute.to_sym] << validator end else _validators[nil] << validator end validate(validator, options) end end
バリデーション対象のレコードから辿れるのであれば、バリデーション対象のレコード側でメモ化してしまうのも手だが、バリデーション対象のレコードのパブリックメソッドにてメモ化してしまうのがドメインモデル間の関係性として本当に適切かなのかは疑問が残るので少し悩ましい。