SREチーム(新卒)の市川恭佑です。
カヤックのサービスでは、信頼性の担保を目的として、ステージング環境を作成する方針を取っています。 ステージング環境では、検証の精度を高めるために、量・質ともに本番環境に類似したデータベースが求められる局面が頻出します。 そこで今回は、Tonamel という自社サービスにおける、検証用データベースの立ち上げを自動化する取り組みについて紹介します。
サービスの置かれていた状況と解決方針
Tonamel の実行基盤は Amazon Web Services (AWS) 上にあり、本番環境とステージング環境は別のアカウントとして、同一の AWS Organizations 組織内に構築されています。 もともと、ステージング環境では、本番環境のデータは利用せず、手作業でダミーデータを作成していました。 それゆえに、データベースに格納されているデータ量は本番環境と比べて明らかに少なく、データベースマイグレーションなどの主要な変更に対して、メンテナンス時間の見積もりやリスクの洗い出しに難がありました。 この問題を解決するべく、以下の二つの対策を検討しました。
概要 | 懸念点 |
---|---|
ダミーデータ生成スクリプトの作成 |
|
本番環境にあるデータの複製 |
|
どちらでもデータ量を本番環境に近づけることは可能ですが、ダミーデータの場合、データの質も近づけることは困難です。 特に、RDBMSにおける関係テーブルやインデックスについては、格納されるデータの質がアプリケーションのパフォーマンスに重大な影響を与える1ので、ここに本番環境との大きな差異があると検証の精度が損なわれます。 これに対し、Tonamel ではステージング環境へのアクセス権が、本番環境を操作可能な開発者に限られている等の複数の理由から、センシティブな情報に対する懸念点を解消できたため、本番環境にあるデータの複製を選択しました。
AWS Step Functions によるワークフロー構築
Step Functions の採用に至った経緯
Tonamel の永続化されたデータベースは Amazon Aurora (RDS) および Amazon DynamoDB を併用しています。 データストアが複数の独立したリソースにまたがっている以上、強い整合性を保証するデータベース複製の実現は困難です。 また、複製されたデータベースの利用者は本番環境ではなく、あくまでステージング環境です。 データに不整合がある際にアプリケーションが一貫した挙動を保てば、ここでは問題にはなりません。
また、責務についても、ビジネスロジックを実装したアプリケーション本体とは異なり、分離されたものであるべきです。 このような性質から、実装コストも考慮して、厳密ではなくとも比較的容易に要求を満たすために、ワークフローエンジンに頼る方針を取りました。
ワークフロー実行エンジンには Step Functions を選択しました。 ちょうど2021年の9月に AWS SDK 統合がアナウンスされ、社内でもデプロイ用ツール Stefunny の開発が進んでいたのが主な理由です。 Step Functions の開発体験には(特に型関係で)まだまだ課題がありますが、これから利用が活発になる見通しです。
ワークフローの全体像
ワークフローの全体像は以下のとおりです。 本番環境アカウントにある Aurora と DynamoDB を、ともに AWS Backup の機能でバックアップ作成し、Backup ボールト(Vault)に格納します。これをステージング環境アカウントの Backup ボールトにコピーして、リストアを実行します。
以下の画像はイメージですが、バックアップの(クロスアカウント)コピーは本番環境側のワークフローが行なっていることに注意してください。
Aurora のクロスアカウント共有可能なバックアップ手段は、これ以外にも効果的なものが用意されています。しかし DynamoDB については現在この方法が最もシンプルかつ現実的なため、AWS Backup を選択し、Aurora 側もこれに統一しました。
なお、AWS Backup については利用にあたっていくつか注意点があるため、後述します。
Amazon EventBridge を用いたワークフローのクロスアカウント統合
本番環境とステージング環境のアカウントにある、二つの Step Functions ワークフローは EventBridge で接続しました。先にイメージを示します。
まず、本番環境側では、 Step Functions ワークフローが終了次第、デフォルトのイベントバスに AWS によって発行されるイベントが流れます。これに対し、「バックアップ用の Step Functions ワークフローの成功通知であった場合はステージングのイベントバスに通知する」というルールを登録することでステージング環境に通知します。
次に、ステージング環境では、本番環境から受け取ったイベントがデフォルトのイベントバスに流れてくるので、「バックアップ用の Step Functions ワークフローの成功通知であった場合はリストア用のワークフローを発火する」というルールを登録すれば十分です。
なお、イベントバスについては、AWSが発行するイベントを用いるため、AWS によってデフォルトで用意されているものを選択しました2。 カスタムイベントバスによる統合も可能かもしれませんが、必要性がないと判断したため未検証です。
EventBridge の権限設定
EventBridge ターゲットには AWS Identity and Access Management (IAM) のロールを設定できます。 同時に、EventBridge イベントバスにはリソースベース・ポリシーも設定できます。
クロスアカウントの検証作業は大きな手間と時間を要するので、ここではシンプルな設定方法を紹介します。 ただし、他のオプションを含む詳細については公式ドキュメント群をご参照ください。
本番環境:
EventBridge ターゲットのロールについて、ステージング環境のデフォルトのイベントバスに対する events:PutEvents
を許可します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Action": "events:PutEvents", "Resource": "arn:aws:events:ap-northeast-1:${stg_account_id}:event-bus/default" } ] }
ステージング環境:
まず、EventBridge イベントバスについて、本番環境のアカウントからのアクセスを許可3します。
{ "Version": "2012-10-17", "Statement": [{ "Sid": "AllowOtherAccounts", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::${prod_account_id}:root" }, "Action": "events:PutEvents", "Resource": "arn:aws:events:ap-northeast-1:${stg_account_id}:event-bus/default" }] }
次に、EventBridge ターゲットのロールについて、復元用の Step Functions ワークフローの実行を許可します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Action": "states:StartExecution", "Resource": "arn:aws:states:ap-northeast-1:${stg_account_id}:stateMachine:${workflow_name}" } ] }
以上で EventBridge を用いた Step Functions ワークフローのクロスアカウント統合に必要な権限の設定は完了です。
AWS Backup によるクロスアカウント・バックアップの注意点
AWS の各種サービスには、それぞれ個別にバックアップやリカバリの機能が提供されています。 AWS Backup は基本的にそれらを抽象化したもので、難解にとらえる必要はありません。
しかし、一般に個別のバックアップ機能を直接使うことが多いせいか、Web 上に見つかる知見が心もとないのも事実です。 概要を知りたい場合には公式ドキュメントか、Classmethodさんのブログが役立ちます。
ここでは、クロスアカウントのユースケースに限って、著者が今回の取り組みで気になったポイントのみを紹介します。
ボールト関連で要求される暗号化キーへのアクセス
AWS Backup では、ボールトに AWS Key Management Service (KMS) の鍵を暗号化キーとして登録します(作成時に指定しない場合、AWSが用意したデフォルトの鍵が利用されます)。
まず、ドキュメントにあるとおり、コピー元のボールトとコピー先のボールトは、互いに同じ鍵で暗号化されている必要があります。つまり、クロスアカウントのコピーをするには、暗号化キーもクロスアカウントに共有する必要があります。今回の事例では、本番側に共有専用の KMS 鍵を作成し、ステージング側からのアクセスを受け入れるように設定しました。
そして、この暗号化キーへのアクセス権は、バックアップジョブの実行ロールだけでなく、ボールトの作成者にも要求されます。 KMS のリソースベース・ポリシーは厳しめに設定するのが良いので、クロスアカウントだからといって焦らずにボールトの作成者を特定し、必要な対象にのみ権限を与えると良いです。
マネジメントコンソール上に現在発生している問題
執筆時点(2022年5月)において AWS Backup のマネジメントコンソールでは、少なくとも二つの軽微な問題が発生しています。 カヤックでは、基本的に Infrastructure as Code (IaC) を推進していますが、インフラ開発自体の検証ではコンソールを使うことも多々あります。 これらの問題についてはサポートケースを作成し、すでに AWS に報告済みなので、あくまで修正されるまでの情報共有としての記述です。
鍵の指定:
一つめは、ボールトの作成画面で別のアカウントにある KMS 鍵を指定できないことです。
自由記述の許されないセレクトボックスになっていて、候補には現在のアカウントの鍵しか出ません。
代わりに、AWS CLI などを経由して CreateBackupVault
API を叩くことで KMS 鍵の Amazon Resource Name (ARN) を直接指定できます。
アクセス権の案内:
二つめは、コピージョブを作成する画面に出てくるアクセス権限の案内です。 公式ドキュメントも最新のコンソールと少しずれていますが、直感的な操作で別アカウントのボールトの指定までは到達できます。すると、ボールトの ARN を入力した時点で、青い長方形に囲まれたアクセス権に関する案内が表示されます。
ここで、Allow ボタンを押下すると、画面の中央に次のようなリソースベース・ポリシーが表示されます。 AWS リソースが変更されるような操作は今のところ発生しないようです。
この例は、コピー元アカウント(XXXXXXXXXXXXXXX)にある src
というボールト内のリカバリーポイントをコピーするにあたって、コピー先アカウント(123456789012)にある dest
というボールトを送信先に指定したものです。
案内では src
のボールトの(リソースベース)ポリシーにコピー先アカウント(123456789012)からのアクセスを許可するように、とのメッセージとともにポリシー例が提示されますが、実際に必要なものは逆です。dest
のボールトのポリシーに、コピー元アカウント(XXXXXXXXXXXXX)からのアクセスを許可する必要があります。セマンティクスも、提示されているもの(つまり "allow dest
to copy sth into src
")は明らかにおかしいです。
結論として、ボールトのリソースベース・ポリシーは、以下のようなものをコピー先にのみ付与すれば十分です。コピー元には必要ありません(今回の事例ではXXXXXXXXXXXXXは本番のアカウントID、付与するボールトはステージング側)。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Allow XXXXXXXXXXXXX to copy into this vault", "Effect": "Allow", "Action": "backup:CopyIntoBackupVault", "Resource": "*", "Principal": { "AWS": "arn:aws:iam::XXXXXXXXXXXXX:root" } } ] }
なお、コピージョブの実行ロールには別途アクセス権を付与する必要があります。
また、コピージョブ自体を StartCopyJob
で発火する場合には、発火時のユーザーに iam:PassRole
権限が必要です。
アプリケーションの接続先データベースの切り替え
検証用データベースを立ち上げ終わったら、そのデータベースをアプリケーションが利用できるように接続先を切り替えます。 このステップは、初期化方法やパラメータの保持方法など、アプリケーションを取り巻くさまざまな要因に影響されて好ましい対処が変わります。 そのため、概要に軽く触れたのち、実装中の気づきを紹介する程度にとどめます。
今回の事例では、このステップを、二つの連動した Step Functions ワークフローとは切り離して、AWS Lambda 関数として実装しました。 代表的な理由は以下のとおりです。
- データベースのバックアップやリストアと異なり、長い時間を要するものではない
- ステージング環境でもアプリケーションの状態や挙動を変えるタイミングはコントロールできた方がいい4
- Aurora のパスワードを変更する必要が5あるが、Step Functions の Input/Output に秘匿情報を晒したくない
- Step Functions の開発体験に疲れが溜まり、静的解析が恋しくなった
Lambda 関数は Go で書かれ、下記のステップを同期的に処理します。
- Aurora のパスワード更新および完了待機
- RDS Proxy のターゲット付け替え
- パラメータの更新
- アプリケーションの更新
Aurora のパスワード変更に関する注意点
Aurora でマスターユーザーパスワードを即時反映で変更するには ModifyDBCluster
API コール時に ApplyImmediately
を指定すればよいですが、その完了を機械的に待つには注意が必要です。というのも、パスワード変更中は DescribeDBClusters
API レスポンスの DBCluster
オブジェクトにある Status
が "available"
から "resetting-master-credentials"
になりますが、この変遷にも(観測した範囲で)数秒から数十秒程度の遅延がありました。
「 ApplyImmediately
である以上、変更値はともかく、 Status
くらいはアトミックに変遷するだろう」という甘い考えで Modify 後に間髪入れずに Describe を叩くコードを書くと、パスワードの変更処理が始まってすらいない状態にも拘わらず、「パスワード変更が完了して Status
が "available"
に戻った」と勘違いして、プログラムが次に進んでしまいます。スマートとは言い難いですが、固定の待ち時間を挟む対処をしました。
RDS Proxy のターゲットおよびグループに関する注意点
先述の Aurora の Status
が "resetting-master-credentials"
である最中は RDS Proxy のターゲットグループに(ターゲットとして)登録できません。
これが、そもそも Lambda の処理を非同期にもせず Aurora のパスワード変更を待機している理由です6。
また、現時点では、RDS Proxy のターゲット・ターゲットグループには以下のような注意すべき制約があります。
- ターゲットグループは削除も作成もできずデフォルトを使うしかない
- ターゲットグループに複数のターゲットを同時に登録できない
これにより、接続先の切り替えの際には、古いターゲットを先に解除して新しいターゲットを登録するという順番を守る必要があります。 RDS Proxy は強力で価値のあるサービスですが、比較的新しいものであるがゆえ、細かな挙動が発展途上である印象を受けます。 今回とはまったく別のユースケースでも、しっかりと挙動を確認しておくと良いでしょう。
まとめ
今回は、Tonamel の事例を通して、Webサービスにおける検証用データベースの立ち上げを自動化するシンプルな方法について、その全体像を紹介しました。
このような取り組みは、手動オペレーションによって掛かる明確なコストを削減するだけでなく、信頼性向上のためのプロセスを導入しやすくして、事故によって発生しうる暗黙的なコストを削減する効果もあります。また、SRE的な取り組みというと、敷居の高さを感じて手を引いてしまうことも多いかもしれません。しかし、革新的なプラットフォームとして喧伝できるほどの堅牢性や汎用性を持ったソフトウェアを開発せずとも、クラウド事業者等によって提供されている既存技術の愚直な組み合わせで達成可能なものは沢山あります。
カヤックの SRE チームでは、地に足のついた取り組みを継続し、社内外における情報共有を通して、より効率的にサービスの信頼性を高めていきます。
-
弊社にはISUCON(Webサイトのパフォーマンスチューニング大会)の優勝者も複数人在籍しています。先日開かれた社内ISUCONに関する記事はこちら。↩
-
イベントバスのポリシーは
Sid
に空文字を受け付けません。そういうこともあるようです。↩ -
バックアップ・転送・リストアのプロセスは比較的長時間かかるので、終了するタイミングの見積もり・コントロールが困難です。↩
-
Auroraはバックアップ時の認証情報でリストアされます。今回はステージング環境で本番環境と同じユーザー名を使っていますが、パスワードは取得できない設定のため更新が必要です。↩
-
新しい RDS Proxy を作成して、データベース用のドメイン(CNAMEレコード)を更新する等、他の方法も検討・挑戦しましたが、今回は最終的な着地点がここでした。↩