AWS CloudFormationのカスタムリソースでRDSやElasticsearchをアップデートする仕組みを作る

こんにちは、Yakumoチームの@ueokandeです。 本日はYakumoチームで取り組んだ、Relational Database Service (RDS)クラスタとElasticsearchクラスタをアップデートする仕組みを紹介します。

YakumoプロジェクトはUS市場向けにkintone.comをAWSから提供することをゴールにしたプロジェクトです。 プロジェクトではこれまで国内のデータセンターから提供していたkintone.comをAWSに移行します。 Yakumoプロジェクトの開発環境とデプロイパイプラインについては前回の記事をどうぞ!

blog.cybozu.io

背景

Yakumoで利用するAWSリソースは全てCloudFormationで管理しています。 CloudFormationは開発環境、ステージング環境、そして本番環境の構築や設定変更に利用しています。 これまではRDSやElasticsearchもCloudFormationで管理してました。 しかしCloudFormationで管理していると、クラスタのバージョンアップができないという課題がありました。

CloudFormationではRDSやElasticsearchのバージョンを指定できますが、すでに作成されたクラスタに対して異なるバージョンを適用すると、CloudFormationが既存のクラスタを一度削除します。 そのためRDSやElasticsearchのバージョンアップをしたくなったとき、今の仕組みだとお客様のデータをリストアする手順が必要になります。 そこでデータが失われること無く、RDSやElasticsearchをアップデートする仕組み作りに取り組みました。

理想のRDS・Elasticsearchクラスタの管理方法

まず仕組みを作る前にYakumoチーム内での要件を整理しました。 チームで実現したいことは以下の2つです。

  • バージョンアップや設定変更で既存のデータは消えない。
  • 既存のCloudFormationの定義と同様、利用してるリソースが宣言的(Declarative)に記述されてる。

1つ目の要件はもちろん外せませんが、開発チームとしては2つ目の要件も重要です。 近年Kubernetesを筆頭に、インフラを宣言的に記述できることが重要となってきています。 人が作りたいものをYAMLなどで記述して、システムがそれに基づいてリソースを作成したり、既にあるリソースを更新します。

Yakumoでも手順の自動化ではなく、インフラを宣言的に作成できるように、CloudFormationやKubernetesを導入してきました。 RDSやElasticsearchも同様に宣言的に管理して、所望のバージョンを指定するとそのバージョンにアップデートしたいというのがチームの要件でした。

AWS APIにはクラスタのアップデートAPIがあるので、クラスタを削除せずにアップデートする方法はいくつか考えられます。

  • クラスタ作成のみをCloudFormationで行い、アップデートや設定変更は手動オペレーションで実行
  • RDSやElasticsearchなどデータを持つリソースはCloudFormationで管理しない
  • CloudFormationのカスタムリソースでデプロイする

1番目、2番目は「宣言的に記述できる」という要件から外れるので採用しませんでした。 Yakumoでは3番目のカスタムリソースでデプロイする仕組みを作りました。

CloudFormationカスタムリソースによる管理

CloudFormationはカスタムリソースという独自のリソースを定義できます。 カスタムリソースはCloudFormation上で作成・更新・削除が開始すると、Simple Notification Service (SNS)に通知を送ったりLambda関数を実行できます。 そのためカスタムリソースを利用することで、標準リソースではできないユーザ独自のデプロイ処理やプロパティを定義できます。 YakumoではRDSやElasticsearchクラスタをカスタムリソースで定義して、Lambda関数内でクラスタを操作をするようにしました。

RDSクラスタのクラスタ管理の構成です。

カスタムリソースをデプロイする仕組み

CloudFormation上でカスタムリソースを作成・更新・削除すると、Lambda関数が呼ばれます。 Lambda関数ではAWS APIを呼び出してRDSクラスタを操作します。 更新時には現在のクラスタの状態と新しいプロパティ値を比較して、差分があるプロパティを既存のクラスタに適用します。

たとえば以下のようにカスタムリソースを更新してCloudFormationに適用すると、既存クラスタを削除すること無くRDSのバージョンアップが実行されます。

KintoneRDSCluster:
  Type: Custom::RdsCluster
  Properties:
    # 呼び出すLambdaのARNを指定する
    ServiceToken:
      Fn::ImportValue: !Sub "RdsDeployLambdaArn"

    # Lambdaにわたすパラメータ
    DBClusterParameterGroupName: !Ref RDSParameterGroup
-   EngineVersion: "5.7.mysql_aurora.2.04.3"
+   EngineVersion: "5.7.mysql_aurora.2.04.4"
    ...

一見すると単純に見えますが、実際作ってみるといくつか注意点があったので順を追って説明します。

変更できないプロパティがある

一部のプロパティはAPIでは変更できません。 たとえばRDSはマスターユーザーのユーザー名を後から変更できません(CloudFormationの標準リソースではクラスタを再作成します)。 チームではこういったプロパティの変更はできないという制約条件として割り切りました。 Lambda関数でこれらのプロパティの変更を検知するとエラーを返すようにして、CloudFormationの適用を失敗させます。

こういったプロパティはよほどのことがない限り変更することはないので現状困りません。 仮にもし将来これらのプロパティの変更が必要になったら、Lambda関数を拡張するだろうということで、今は対応してません。

クラスタが利用できるまで待つ

CloudFormationで呼び出されたLambda関数は、クラスタを作成や設定変更が完了してクラスタが利用できるまで待つ必要があります。 もし待たずにすぐに処理を返すと、CloudFormationの適用が完了したけど実はまだ利用できないといったことが起こります。 また前回の適用が終わってないのに、新たに設定を変更しようとして失敗するといったケースも考えられます。

クラスタ作成や設定変更を待つには、GoのAWS SDKにあるrequest.Waiterを使いました。 このパッケージはAWS APIを一定期間実行して、所望の状態になるまで待つことができます。 ここでクラスタの作成・適用を待ってLambda関数の実行時間が長くなると、タイムアウトについても考慮する必要があります。

Lambdaのタイムアウトを考慮する

Lambda関数のタイムアウト上限値は15分ですが、この時間内にクラスタ作成や設定変更が完了しないことがあります。 Lambda関数が結果を返さなければ、CloudFormationはLambda関数をリトライします。 そのためLambda関数では多重実行を考慮して設計する必要があります。

例えば作成するリソースIDにランダムな文字列を付与したとします。 もしリソース作成時にLambda関数がタイムアウトすれば、1度目のLambda関数によって作成されたリソースを追跡できません。 そのためCloudFormationがリソースを削除できず残り続けます。

YakumoではRDSクラスタやElasticsearchの名前を、スタックに対して一意なIDにしました。 具体的にはスタックIDとリソースの論理IDのハッシュ値をサフィックスに付与します。 リトライ時にRDSクラスタやElasticsearchクラスタを特定できるので、Lambda関数で作成時にリトライされたか否かを特定できるようになります。 こういったベストプラクティスはAWS公式ブログでも紹介されてます。

まとめ

この記事ではYakumoプロジェクトでのRDSとElasticsearchクラスタの管理方法を紹介しました。 Yakumoプロジェクトは単なるAWS移行だけではなく、今まで国内のインフラでできなかったことや反省点を活かした、技術的チャレンジも多くあります。 その1つがこの宣言的にRDS、Elasticsearchクラスタを更新できる仕組みです。

短期的に見ると手動オペレーションでバージョンアップが手っ取り早いですが、長期的な観点で見ると宣言的なインターフェイスが昨今のインフラの理想でもあります。 Yakumoではまだ紹介しきれない知見や体験がありますが、それはまた別の記事で紹介したいと思います。