CloudFormation Resource ImportによるRDSバージョンアップ時の定義差分を解消する一手法

こんにちは。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

カテゴリー