こんにちは! ZOZOテクノロジーズ開発部の塩崎です。 この記事ではCloudFormationにDBのマスタパスワードなどの秘密情報を渡す3つの方法を説明いたします。
前提
我々のチームではAWSインフラリソースのプロビジョニングにCloudFormationを使用しています。 CloudFormationのテンプレートファイルはGitHubでバージョン管理されており、スタックに対するチェンジセットの作成をCircleCIから行っています。 このあたりの詳細は以下の記事に書かれているため、詳細はそちらをごらんください。
課題
このような方法でCloudFormationテンプレートを管理していましたが、それに伴う課題が生まれました。 DBのマスタパスワードなどの情報をどのようにして渡すかということです。
テンプレート内で使用するためのパラメーターは以下のような形式でテンプレートの中に埋め込み・参照できます。
Parameters: VPCCidrBlock: # パラメーターの定義 Type: String Default: '10.0.0.0/16' Resources: EC2VPC: # VPCを作成 Type: 'AWS::EC2::VPC' Properties: CidrBlock: !Ref VPCCidrBlock # こんな感じで参照。他の箇所からも!Refで参照できる。
しかし、この方法をそのまま使うと秘密情報をテンプレートにそのまま埋め込む必要が出てしまいます。 ちょうど以下のテンプレートのような形になります。
Parameters: RDSMasterUserPassword Type: String Default: 'Very_$ecret_Data' Resources: RDSDBCluster: Type: 'AWS::RDS::DBCluster' Properties: MasterUserPassword: !Ref RDSMasterUserPassword
これではGitHubにアクセスできる人から秘密情報が丸見えです。 可能ならばこれらの情報はGitHubにコミットしたくありません。 さらにCircleCIなどの外部SaaSなどに秘密情報(もしくはどこかから秘密情報を取り出すことができる権限情報)を渡すことも避けたいです。
一方でCloudFormation側の事情を考えると、CloudFormationは何らかの方法で平文の秘密情報を知っておく必要があります。 CloudFormationが内部的にAWSのAPIを呼び出してリソースを作成するときにはこの情報が必要なためです。
これらの要件をまとめると、以下の図で示すようなCloudFormationに秘密情報を渡す「何らかの仕組み」が必要です。
解決策
上の図の「何らかの方法」に対応する解決策を3つ紹介いたします。
UsePreviousValue
最初に紹介するのは、CloudFormationにパラメーターを渡す時にUsePreviousValueをTrueにする方法です。 この方法で以前テンプレートに渡した値を引き継げます。 ですので、そのパラメーターを2回目以降使う時はテンプレートの中に値を埋め込む必要がなくなります。
Parameters: RDSMasterUserPassword Type: String Default: '' # ここは空でOK Resources: RDSDBCluster: Type: 'AWS::RDS::DBCluster' Properties: MasterUserPassword: !Ref RDSMasterUserPassword
チェンジセットを作成する際には以下のようなJSONを作成し、awsコマンドを叩くことで以前の値を再利用することが出来ます。
[ { "ParameterKey": "RDSMasterUserPassword", "UsePreviousValue": true } ]
aws cloudformation create-change-set --stack-name=<stack name> --parameters=parameters.json
参考: https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-change-set.html
カンの良い方ならば既にお気づきかもしれませんが、この方法には最初にパラメーターをセットするときにはどうするのかという問題があります。 最初にパラメーターをセットするためにはCloudFormationのテンプレートをAWSマネジメントコンソールもしくは手元のターミナルから反映する必要があります。 せっかくCircleCIを使ったCI/CDを構築しているのに、この部分だけが手動反映なのは残念な気持ちになります。 また、parameters.jsonを作成する必要があるのも少々面倒です。
Systems ManagerのSecureStringを使用する
次に紹介する方法はSystems ManagerのSecureStringを使用する方法です。 Systems ManagerはEC2インスタンスやオンプレのインスタンスを管理するためのものです。 その中に文字列を暗号化して保存するためのストレージがあるので、それを活用します。 Systems Managerに保存した文字列はDynamic Referenceという方法でCloudFormationから参照できます。
Resources: RDSDBCluster: Type: 'AWS::RDS::DBCluster' Properties: MasterUserPassword: '{{resolve:ssm-secure:rds-master-user-password:1}}'
最終行がDynamic Referenceでパラメーターを参照している部分です。
Dynamic Referenceの書式は以下に示すように:
で区切られた4つのセクションに分かれています。
{{resolve:ssm-secure:parameter-name:version}}
前半の2つはSystems ManagerのSecureStringを使うことを指定しています。 後半の2つで保存されたパラメーターのキーとバージョンを指定しています。 最新のバージョンを指定するということはできず、必ず数字でバージョンを指定する必要があります。
パラメーターをセットするためには以下のようにawsコマンドを叩くか、もしくはAWSマネジメントコンソールで行います。
aws ssm put-parameter --name rds-master-user-password --value 'Very-$ecret-Data' --type SecureString
Systems Managerがこの値を保存するときにはKey Management System(KMS)を用いた暗号化がなされます。
暗号化に使う鍵はデフォルトキーだけでなく、 --key-id
パラメーターを使ってユーザーキーを指定することも可能です。
この方法を使うことでCloudFormationにパラメーターを渡すことが出来ますが、注意点もあります。 適用できるリソースの種類に制限があるということです。
現時点ではIAMユーザーのパスワードやRDSのマスタパスワードなどの11種類のリソースに対してのみこの方法を適用できます。
そのため、任意の箇所へ秘密情報を埋め込むということは出来ません。
たとえば、ECSで動いているアプリケーションに対して環境変数を通して秘密情報を渡すことを考えると以下のようなテンプレートになるかと思います。 AWS::ECS::TaskDefinitionリソースはSecureStringに対応していないため、SSM SecureStringを使った方法は適用できません。
Resources: ECSTaskDefinitionApplication: Type: 'AWS::ECS::TaskDefinition' Properties: ContainerDefinitions: Environment: - Name: 'DB_PASSWORD' Value: 'Very-$ecret-Data' # ここには{{resolve:ssm-secure:rds-master-user-password:1}}と書けない
Secrets Manager
最後に紹介する方法はSecrets Managerを使って秘密情報を渡す方法です。 この方法はSystem ManagerのSecureStringを使用する方法に似ていますが、少々異なる面もあります。
Secrets Managerを使った方法もDynamic Referenceを使用してパラメーターの参照を行うため、書式がかなり似ています。
Resources: RDSDBCluster: Type: 'AWS::RDS::DBCluster' Properties: MasterUserPassword: '{{resolve:secretsmanager:rds:SecretString:password}}'
こちらの方法のDynamic Referenceの書式は以下に示すように:
で区切られた5つのセクションに分かれています。
secret-id
と json-key
の2つを指定して秘密情報の取り出しを行います。
Secrets Managerのそれは連想配列型であるため、これら2つの情報を使って秘密情報を指定します。
{{resolve:secretsmanager:secret-id:SecretString:json-key}}
秘密情報をセットするためには以下のコマンドを使用します。 この例ではRDSのパスワードだけを設定していますが、RDSのユーザーやホスト名などの関連する情報を設定することも出来ます。 また、Secrets ManagerでもKMSのユーザーキーを用いた暗号化を行うことが出来ます。
aws secretsmanager put-secret-value --secret-id rds --secret-string '{"password":"Very-$seret-Data"}'
Systems Managetとは異なり、Secrets Managerを使ったDynamic Referenceはどのリソースに対しても使用することが出来ます。 このような利便性がある一方で、うっかりと秘密情報を公開してしまう危険性も同時に持っているため注意が必要です。
Resources: ECSTaskDefinitionApplication: Type: 'AWS::ECS::TaskDefinition' Properties: ContainerDefinitions: Environment: - Name: 'DB_PASSWORD' Value: '{{resolve:secretsmanager:rds:SecretString:password}}'
3つの手法の比較
上で紹介した3つの方法の比較です。
UsePreviousValue | SSM Secure String | Secrets Manager | |
---|---|---|---|
CloudFormationのチェンジセット作成を自動で行えるか | X | O | O |
パラメーターが暗号化されて保存されるか | X | O | O |
KMSのユーザーキーを利用できるか | X | O | O |
任意のCloudFormationリソースに対して使用できるか | O | X | O |
他のAWSサービスとのインテグレート | X | O | O |
この表を見てみると、Secrets Managerを使うのが現状のベストだと思います。
他のサービスとのインテグレートについて
今回紹介した方法はCloudFormationとSSM、CloudFormationとSecrets Managerという組み合わせでの使い勝手を見るという観点でした。 一方でSSMやSecrets ManagerはCloudFormation以外のサービスと組み合わせて使うことも出来ます。 その観点から見ると、現時点ではSSMのほうが他のサービスとのインテグレートへの対応が早いかと思います。
例えばElastic Container Service(ECS)とのインテグレートを考えます。 ECSでタスクを起動する時に秘密情報を環境変数にセットしてから起動したいとします。
2019年2月現在ではこのようなことが出来るのはSSM SecureStringのみです。
Secrets Managerの方が後発のサービスであることを考えると致し方ない気もしますが、SecretsManagerのこれからに期待したいです。
2/5追記 いつの間にかSecrets ManagerからECSに秘密情報が渡せるようになっていました。 気づいたらどんどん便利になっていくaws!! Secrets Managerの方が後発のサービスなので対応時期に少々の差(2018年11月と2019年1月)が出てしまっていますが、将来的にはこの差が縮まっていくことを期待しています。
よいまとめありがとうございます!
— ポジティブな Tori (@toricls) 2019年2月5日
実は1月末の時点で ECS から Secrets Manager の値取れるようになってるので、ブログ記事にも反映して欲しいです〜
参考: https://aws.amazon.com/jp/about-aws/whats-new/2018/11/aws-launches-secrets-support-for-amazon-elastic-container-servic/ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html#secrets-create-secret
まとめ
CloudFormationに秘密情報を渡す3つの方法をお伝えしました。 現時点ではSecrets Managerを使うのがベストだと思います。
我々Marketing AutomationチームではAWSのインフラ構成をCloudFormationで管理することによって、Infrastructure as Codeを実現しています。
最近ではGCPのインフラも増えてきたことからTerraformの使用も視野に入れて日々の開発運用を行っております。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。