こんにちは。ZOZOテクノロジーズSRE部の西郷です。普段はAWSを用いてマルチサイズプラットフォーム事業(以降MSPと記載します)のシステム構築や運用に携わっています。
このMSPのシステムではRDBにAmazon Aurora PostgreSQLを採用しています。DBを含むネットワークは全てCloudFormationで管理しており、変更は原則テンプレート修正にて行っています。
さて、このAmazon Auroraは定期的なバージョンアップが発生します。この対応についてもテンプレートを更新して行うのですが、組み合わせの悪い部分があり、都度対応を検討してきました。
その問題について、CloudFormationのResource Importを用いることできれいに解決できたため、事例としてご紹介します。
MSPとその生産を支えるインフラ
まずはMSPについて少し触れておきます。MSPはZOZOTOWN上で展開しているサービスです。欲しい商品を選び、身長と体重を選択すると、体型にあったサイズをレコメンドします。対象商品はZOZOTOWNに出店いただいているブランド様と共同で企画・生産を行っています。
MSPの生産を支える取り組みについては、以下のテックブログで詳しく取り上げられていますので、ぜひ御覧ください。
techblog.zozo.com techblog.zozo.com techblog.zozo.com
弊チームで構築・運用しているシステムでは主に発注・生産部分を支える機能を提供し、AWS上はこのような構成になっています。
受発注情報の取得や登録、生産ステータスや納品データの登録といった機能を提供しており、Auroraにはこれらに関する重要なデータが保管されています。
CloudFormationで管理するAuroraのバージョンアップ上の課題
冒頭でも述べた通り、定期的にAWSからバージョンアップがアナウンスされるのですが、その際の対応方法は次の2択です。
- 定められた期限内に運用者が任意のタイミングで行う
- 対応を行わず期限後のメンテナンスウィンドウで行われる自動更新に任せる
しかし、以下のような課題から任意のタイミングで行うのが一般的かと思います。
- DBエンジンのバージョンアップはDBインスタンスの一時的な停止を伴うものである
- 適用されているパッチにより、アプリケーションで予期せぬ不具合に遭遇する可能性がある
弊チームでは、まずCloudFormationからそのままバージョンアップを行うことを検討しました。ですが、その場合DBクラスタとインスタンスが再作成されてしまい、DB内のデータが失われてしまいます。
具体的にはテンプレート上でEngineVersion
というプロパティの値を変更しスタックを更新することになるのですが、公式ドキュメントを確認すると、Update requires: Replacement
と記載されています。
RDSDBCluster: Type: 'AWS::RDS::DBCluster' Properties: # --------- omit Engine: 'aurora-postgresql' EngineVersion: '10.7' #ここを変更する # --------- omit
Update requires
は、AWSリソースに変更を加えた際にどのような更新が行われるのかを示すものです。Replacement
はリソースを再作成して古いリソースと置き換える、いわゆる置換が発生する更新方法です。MSP対応商品の生産や納品に関わる重要なデータが保管されているため、この方法でバージョンアップすることはできません。
従来のバージョンアップ手法の課題と今回実現したかったこと
この悩みに対して弊チームではこれまで以下の方法によるバージョンアップを検討・実施してきました。
アプローチ | メリット | デメリット |
---|---|---|
A.Webコンソールからバージョンアップを行う | 手順がシンプル、作業時間が短い | テンプレートで定義しているDBエンジンバージョンと実際のDBエンジンバージョンが一致しない |
B.スナップショットを利用してスタックで新しいバージョンのDBクラスタ&インスタンスを新規作成する | スタックで認識しているDBエンジンバージョンと実際のDBエンジンバージョンが一致する | DBクラスタのエンドポイントが変わる、データの整合性を取るためにアプリケーションを停止しスナップショットを取る必要があるので作業時間が長くなる |
CloudFormationで全てのAWSリソースを管理している環境においてはテンプレート上の不一致の方が許容しがたい部分だったため、前回はB案で対応を行いました。
とはいえB案の場合はDBクラスタのエンドポイントが変わることになり、できることなら既存のDBクラスタを維持したままバージョンアップできないかと考えていました。
そのため、今回のバージョンアップで実現したかったことをまとめると以下の要件にまとまりました。
- 既存のDBクラスタとインスタンスを維持したままバージョンアップしたい(エンドポイントも変わらない)
- テンプレートで定義しているDBエンジンバージョンと実際のDBエンジンバージョンが一致する状態にしたい
CloudFormationのResource Import
そこで利用したのがResource Importです。
2019年11月にリリースされた機能で、WebコンソールやCLIから作成されたAWSリソースをスタックに取り込むことができます。新規スタックとしてAWSリソースをインポートすることも可能ですが、既存スタックへのインポートも可能です。
さて、このインポートを行う際はテンプレートにDeletionPolicy属性の記述が必要です。これはCloudFormationのリソース属性の1つで、スタックが削除される際にそのスタックで管理されているAWSリソースの扱いを定義するもので、インポートを行う際は保持する(Retain)、という指定が必要になります。
そのため、手動で作成されたAWSリソースをスタックに取り込む、という使い方はもちろんなのですが、以下のようなシーンへの活用も可能です。
- 1つのテンプレートで管理していたが、状況が変わりテンプレートを分割したい
- テンプレートから行うと
Replace
扱いになってしまってできない変更をWebコンソールから行い、テンプレートとの定義差分をなくしたい
いずれも一度AWSリソースをスタックから削除し、既存or新規のスタックに取り込むことで実現できます。
今回はテンプレートから行うとReplace
扱いになってしまってできないDBエンジンのバージョンアップをWebコンソールから行い、その場合発生するテンプレート上の不一致をResource Importで解消できることから、これを使ってバージョンアップを行うに至りました。
実際に行ったバージョンアップ手順
今回行った作業を図にするとこのような流れになります。
また、変更を加えていくテンプレートは以下のようなものです。
AWSTemplateFormatVersion: 2010-09-09 Resources: RDSDBCluster: Type: 'AWS::RDS::DBCluster' DeletionPolicy: 'Delete' Properties: # --------- omit Engine: 'aurora-postgresql' EngineVersion: '10.7' # --------- omit RDSDBInstance: Type: 'AWS::RDS::DBInstance' DeletionPolicy: 'Snapshot' Properties: # --------- omit AllowMajorVersionUpgrade: false AutoMinorVersionUpgrade: false DBClusterIdentifier: !Ref RDSDBClusterApplication DBInstanceClass: 'db.r4.large' Engine: 'aurora-postgresql' # --------- omit SSMParameterDBClusterEndpoint: Type: 'AWS::SSM::Parameter' Properties: Name: '/postgres_host' Type: 'String' Value: !GetAtt RDSDBCluster.Endpoint.Address
以降は実際に本番環境で行った手順についてまとめていきます。なお、ここで記述する作業はあらかじめ接続するサーバを全て停止、バージョンアップ対象のDBインスタンスのスナップショットを取得した上で行っています。
STEP1:バージョンアップ対象のDBクラスタ、インスタンスに対してDeletionPolicy属性をRetainで設定する
まずはDBクラスタとインスタンスをスタックから削除した際にDBクラスタとインスタンスがAWS上に残るようにする必要があります。
テンプレートにて先述のDeletionPolicy属性をRetain
にします。
AWSTemplateFormatVersion: 2010-09-09 Resources: RDSDBCluster: Type: 'AWS::RDS::DBCluster' DeletionPolicy: 'Retain' #[STEP1]Retainで指定する Properties: # --------- omit Engine: 'aurora-postgresql' EngineVersion: '10.7' # --------- omit RDSDBInstance: Type: 'AWS::RDS::DBInstance' DeletionPolicy: 'Retain' #[STEP1]Retainで指定する Properties: # --------- omit AllowMajorVersionUpgrade: false AutoMinorVersionUpgrade: false DBClusterIdentifier: !Ref RDSDBClusterApplication DBInstanceClass: 'db.r4.large' Engine: 'aurora-postgresql' # --------- omit
このテンプレートで変更セットを作成して差分を確認するのですが、DeletionPolicy属性の変更は差分として検出されません。そのため変更セットを実行し、イベントでバージョンアップ対象のDBクラスタとインスタンスがUPDATE_COMPLETE
と記録されることを確認しました。
STEP2:別のリソースから参照している箇所を変更する
このままDBクラスタを削除するとDBクラスタのエンドポイントを参照している箇所は参照先のリソースがなくなるため、スタックの更新に失敗します。
そのため、以下のようにコメントアウトする等何らかの形で参照しないようテンプレートを変更します。
AWSTemplateFormatVersion: 2010-09-09 Resources: # --------- omit #[STEP2]参照しないようにする # SSMParameterDBClusterEndpoint: # Type: 'AWS::SSM::Parameter' # Properties: # Name: '/postgres_host' # Type: 'String' # Value: !GetAtt RDSDBCluster.Endpoint.Address
STEP3:スタックからバージョンアップ対象のDBクラスタ、インスタンスを削除する
この作業でDBクラスタとインスタンスをスタックの管理下から外します。
バージョンアップ対象のDBクラスタとインスタンスの記述をコメントアウトしたテンプレートで変更セットを作成し、反映します。
AWSTemplateFormatVersion: 2010-09-09 Resources: #[STEP3]DBクラスタとインスタンスを削除する # RDSDBCluster: # Type: 'AWS::RDS::DBCluster' # DeletionPolicy: 'Retain' #[STEP1]Retainで指定する # Properties: # # --------- omit # Engine: 'aurora-postgresql' # EngineVersion: '10.7' # # --------- omit # RDSDBInstance: # Type: 'AWS::RDS::DBInstance' # DeletionPolicy: 'Retain' #[STEP1]Retainで指定する # Properties: # # --------- omit # AllowMajorVersionUpgrade: false # AutoMinorVersionUpgrade: false # DBClusterIdentifier: !Ref RDSDBClusterApplication # DBInstanceClass: 'db.r4.large' # Engine: 'aurora-postgresql' # # --------- omit #[STEP2]参照しないようにする # SSMParameterDBClusterEndpoint: # Type: 'AWS::SSM::Parameter' # Properties: # Name: '/postgres_host' # Type: 'String' # Value: !GetAtt RDSDBCluster.Endpoint.Address
注意点としては、変更セットの差分にはDBクラスタとインスタンスがRemove
というアクションで検知されることが挙げられます。
実際に更新を行うとイベント上はDELETE_SKIPPED
と記録され、DBクラスタとインスタンスは削除されずに残ります。
DeletionPolicy属性がRetain
で設定されていない場合、ここでDBクラスタとインスタンスが実際に削除されてしまうため、注意深く行う必要があります。
現在のCloudFormationには、論理ID毎の設定済みDeletionPolicyを確認する方法がありません。反映済みテンプレートを目視確認することは可能ですが、それだけでは不安です。我々は本番環境と同じテンプレートから作られた事前環境を持っているので、そこで入念に動作を確認しました。
STEP4:WebコンソールからDBエンジンのバージョンをアップデートする
スタックの管理外になったところで、対象のDBクラスタを選択し、希望のエンジンバージョンにアップデートします。
当然ながら、変更のスケジューリングは「今すぐ」を選択して変更を行い、DBクラスタのステータスが利用可能
になることを確認しました。
STEP5:バージョンアップしたDBクラスタとインスタンスをResource Importでスタックに取り込む
バージョンアップしたDBクラスタとインスタンスを再度スタック管理下に置くため、テンプレートを以下のように変更します。DBのエンジンバージョンはこのタイミングでアップデートしたものに変更しておきます。
AWSTemplateFormatVersion: 2010-09-09 Resources: #[STEP4]DBクラスタとインスタンスのコメントアウトを戻す RDSDBCluster: Type: 'AWS::RDS::DBCluster' DeletionPolicy: 'Retain' #[STEP1]Retainで指定する Properties: # --------- omit Engine: 'aurora-postgresql' EngineVersion: '10.13' #[STEP4]アップグレードしたバージョンを指定する # --------- omit RDSDBInstance: Type: 'AWS::RDS::DBInstance' DeletionPolicy: 'Retain' #[STEP1]Retainで指定する Properties: # --------- omit AllowMajorVersionUpgrade: false AutoMinorVersionUpgrade: false DBClusterIdentifier: !Ref RDSDBClusterApplication DBInstanceClass: 'db.r4.large' Engine: 'aurora-postgresql' # --------- omit #[STEP2]参照しないようにする # SSMParameterDBClusterEndpoint: # Type: 'AWS::SSM::Parameter' # Properties: # Name: '/postgres_host' # Type: 'String' # Value: !GetAtt RDSDBCluster.Endpoint.Address
今回は既存のスタックにインポートしたかったため、該当スタック > スタックアクション > スタックへのリソースのインポートで操作を行いました。以下のようにDBクラスタとインスタンスの識別子を指定することでインポートが可能です。
この際、テンプレートにインポート対象のDBクラスタやインスタンス以外の変更があると以下のようなエラーになります。
Update, create or delete operations cannot be executed during import operations.
そのため、STEP2で行った変更を元に戻す作業は次のSTEPで対応しました。
STEP6:参照している箇所を戻す
テンプレートを以下のように修正の上、変更セットを作成し、反映を行って参照する状態に戻します。
AWSTemplateFormatVersion: 2010-09-09 Resources: # --------- omit #[STEP6]STEP2でコメントアウトしていたのを戻す SSMParameterDBClusterEndpoint: Type: 'AWS::SSM::Parameter' Properties: Name: '/postgres_host' Type: 'String' Value: !GetAtt RDSDBCluster.Endpoint.Address
スタックの更新がUPDATE_COMPLETE
になることを確認した上でAuroraに接続するサーバを起動し、動作確認を行いました。
Resource Importの良さと利用する際の注意点
Resource Importを利用することでDBクラスタのリソースを維持したままテンプレートと実際のDBエンジンバージョンの不一致を解消できました。テンプレートからそのまま行うと置換が必要になってしまう変更に対して、置換を回避できるアプローチがあることは、CloudFormationでAWSリソースを管理する環境において非常に有用だと感じました。
一方、今回のようにResource Importを利用する際の注意点だと感じたことは以下の点です。
- インポートと同時に新規作成や更新、削除といった変更を行うことはできない
- 変更を加えたい場合はインポート後にスタックを更新する
- 削除対象を参照している箇所がある場合は事前にテンプレートを編集し依存関係を解消しておく必要がある
- Import対象のリソースによっては、依存が多く修正範囲が広がる場合がある
- 全てのAWSリソースをImportできるわけではないため意図通りにImportできるか事前によく確認する必要がある
- DeletionPolicy属性の設定・スタックから削除・取り込みを実際に試してみるのが望ましい
- Importできるリソースは公式ドキュメントに記載されている
Resource Importを利用する際の注意点というわけではないのですが、DeletionPolicy属性の変更は変更セット作成時に差分として検知されません。こちらも頭の片隅においておくと良いかと思います。
まとめ
CloudFormationでAWSリソースを管理していると、置換が発生する変更を行いたい、テンプレートを分割したいといったシーンは比較的よくある悩みかと思います。このような"やりたいけどできなかった"ことを解決できたことは非常に有益であり、今後もより運用しやすい環境になっていくのではないか、という期待が持てました。
ZOZOテクノロジーズでは、ZOZOMATやWEAR、MSPといった事業をテクノロジーで支えるさまざまな職種を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com