なまえは まだ ない

思いついたことをアウトプットします

CloudFormation でドリフトを検出してしまった Aurora MySQL クラスターを再インポートした話

この記事はフラー Advent Calendar 2020 の4日目の記事です。3日目は @shogo82148 さんで「2020年に書いた GitHub Action + α」でした。


さて、フラーに入って1年半が過ぎ、GoによるWeb API開発だけでなくそれを運用するインフラ領域にもそこそこ真面目に手を出すようになりました。今回はそこでハマった話を書きます。

AWS CloudFormation

弊社、特に私が担当している案件では、インフラを主にAWSで構築・運用しています。 同じような構成のインフラをdevelopment/staging/productionと複数環境に構築することもままあるため、構築は基本的にAWSCloudFormationを利用しています。

Amazon Aurora MySQL

データベースサーバーは基本的にAmazon AuroraのMySQL互換エディションを使ってます。 AWS魔改造してるので、通常のMySQLよりすごいらしいです。何がどのくらいすごいのか、通常のMySQLを運用したことない私にはちょっとよくわかりません。 詳しい方がいらっしゃったら教えてください。

Aurora MySQLのエンジンバージョンとCloudFormation

さて、先日弊社で運用しているAWSアカウントに対し、AWS様から「Aurora MySQLのエンジンバージョンを上げなさい」とのお達しがきました。 しゃーねぇ変えてやるか、ということでCloudFormationにデプロイしてあるスタックテンプレートを確認します。簡略化のため諸々省略してますが、大体↓のようなテンプレートを使ってました。

Resources:
  DBCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      DBClusterParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql
      EngineVersion: 5.7.mysql_aurora.2.08.1
      MasterUsername: root
      MasterUserPassword: "very-very-secret"
  DBInstance1:
    Type: AWS::RDS::DBInstance
    Properties:
      AutoMinorVersionUpgrade: false
      DBClusterIdentifier: !Ref DBCluster
      DBInstanceClass: db.t3.small
      DBParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql

アップデートしろと言ってるのはDBClusterの EngineVersion という箇所です。じゃあこいつを上げて更新すればええんやな!……と思うじゃないですか。そうはいかないのです。CloudFormationのドキュメントを見てみましょう。

Update requires: Replacement

Replacement というのは、ここに差分が生じた場合リソースの更新ではなく置換が行われてしまう、ということです。つまり、CloudFormationでエンジンバージョンを更新すると今動いてるデータベースが殺されて新しいデータベースが立ち上がるのです。困ったことに、置換の際にデータをレプリケーションしてくれるなんて気の利いたことはしてくれません。置換が走った場合、全データロスト(!)となってしまいます。困った困った。

……というわけで、置換を回避しつつエンジンバージョンを上げるにはAWSマネジメントコンソールから直接操作してあげる必要があります。

AWS マネジメントコンソールからエンジンバージョンをアップデートする

というわけでAWSマネジメントコンソールからエンジンバージョンの更新をやってみます。RDSのクラスターを変更し、エンジンバージョンを 5.7.mysql_aurora.2.08.1 から 5.7.mysql_aurora.2.08.3 に更新してみます。

f:id:furusax0621:20201201232941p:plain

この状態で更新をかけ、しばらく待てばアップデート完了です。やったね!!!

……残念ながら、そう単純な話ではありません。CloudFormationのコンソールに移動して、データベースを管理しているスタックのドリフトを検出してみましょう。

f:id:furusax0621:20201201233403p:plain

ドリフトが検出された! エンジンバージョンを手動で更新してしまったので、テンプレートとの差分が出てしまいました。 エンジンバージョンはReplacementなプロパティなので、これを放置したままスタックの他の箇所を更新しようとすると、このドリフトによってデータベースの置換が発生してしまいます(要検証)。というわけで、更新したくてもできない可哀相なリソースの誕生です。困った困った。

ドリフトを検出してしまったリソースを再インポートする

前置きがだいぶ長くなりました、ここからが本題です。 Replacementなプロパティでドリフトを検出してしまった可哀相なリソースを、CloudFormationのインポート機能で救済してみます。

STEP1. ドリフトの差分を埋める

まずはお手元のスタックテンプレートを更新して、発生してる差分を埋めてあげましょう。 今回はエンジンバージョンに差分が出てしまっているのでそこを修正します。

Resources:
  DBCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      DBClusterParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql
      EngineVersion: 5.7.mysql_aurora.2.08.3 # エンジンバージョンを現在の値に揃える
      MasterUsername: root
      MasterUserPassword: "very-very-secret"
  DBInstance1:
    Type: AWS::RDS::DBInstance
    Properties:
      AutoMinorVersionUpgrade: false
      DBClusterIdentifier: !Ref DBCluster
      DBInstanceClass: db.t3.small
      DBParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql

修正したらこれをデプロイします。現在の値と比べて差分が無いので、何もアップデートが発生しないはずです。

STEP2. DeletionPolicy: Retainを設定する

AWSのドキュメントによると、 既存リソースをスタックにインポートするためには対象のテンプレートに DeletionPolicy 属性を付与しておく必要があるそうです。 後の手順の都合もあるため、ここでは DeletionPolicy: Retain を設定します。

Resources:
  DBCluster:
    Type: AWS::RDS::DBCluster
    DeletionPolicy: Retain # これを設定
    Properties:
      DBClusterParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql
      EngineVersion: 5.7.mysql_aurora.2.08.3
      MasterUsername: root
      MasterUserPassword: "very-very-secret"
  DBInstance1:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Retain # これを設定
    Properties:
      AutoMinorVersionUpgrade: false
      DBClusterIdentifier: !Ref DBCluster
      DBInstanceClass: db.t3.small
      DBParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql

忘れがちですが、 DBClusterDBInstance1 の両方に設定しましょう。 修正したテンプレートは忘れずに反映しておきましょう。

STEP3. CloudFormationからリソースを削除する

さて、ここまで来たら一度スタックテンプレートから DBClusterDBInstance1 のリソースを削除します。 後でインポートする際に必要なので、スタックテンプレートではコメントアウトするに留めておくと良いでしょう。

Resources:
#   DBCluster:
#     Type: AWS::RDS::DBCluster
#     DeletionPolicy: Retain
#     Properties:
#       DBClusterParameterGroupName: default.aurora-mysql5.7
#       Engine: aurora-mysql
#       EngineVersion: 5.7.mysql_aurora.2.08.3
#       MasterUsername: root
#       MasterUserPassword: "very-very-secret"
#   DBInstance1:
#     Type: AWS::RDS::DBInstance
#     DeletionPolicy: Retain
#     Properties:
#       AutoMinorVersionUpgrade: false
#       DBClusterIdentifier: !Ref DBCluster
#       DBInstanceClass: db.t3.small
#       DBParameterGroupName: default.aurora-mysql5.7
#       Engine: aurora-mysql

先の手順で DeletionPolicy: Retain を設定してるので、スタックから削除してもリソース自体は生き残ります。

f:id:furusax0621:20201201233642p:plain

STEP4. スタックテンプレートから EngineVersion を削除する

一旦削除したリソースを復活させ、 ドリフトの原因となった EngineVersion をスタックテンプレートから消しておきます。 これでインポート後はエンジンバージョンが上がってもドリフトを検出しなくなります。 また、この時点ではまだスタックテンプレートを反映してはいけません。

Resources:
  DBCluster:
    Type: AWS::RDS::DBCluster
    DeletionPolicy: Retain
    Properties:
      DBClusterParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql
      MasterUsername: root
      MasterUserPassword: "very-very-secret"
  DBInstance1:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Retain
    Properties:
      AutoMinorVersionUpgrade: false
      DBClusterIdentifier: !Ref DBCluster
      DBInstanceClass: db.t3.small
      DBParameterGroupName: default.aurora-mysql5.7
      Engine: aurora-mysql

STEP5. CloudFormationにリソースを再インポートする

いよいよインポートです。 CloudFormationのコンソールでインポート先のスタックを開き、 スタックへのリソースのインポート を選択します。

f:id:furusax0621:20201201233902p:plain

STEP4 で修正したスタックテンプレートをアップロードします。 アップロード済みのS3オブジェクトを指定する方法とテンプレートファイルをその場でアップロードする方法がありますが、今回はその場でアップロードする方法を選択しました。

f:id:furusax0621:20201201233917p:plain

スタックテンプレートから、インポートが可能なリソースが検出されます。 DBClusterDBInstance1 が無事検出されたので、インポートするデータベースの識別子を記述します。

f:id:furusax0621:20201201233934p:plain

確認画面で想定どおりのリソース紐付けができてることを確認したら、 リソースをインポート を選択します。

f:id:furusax0621:20201201233959p:plain

インポート作業の成功を祈りながら見守ります。 IMPORT_COMPLETE となれば成功です。やったね!!!

f:id:furusax0621:20201201234018p:plain

念の為、インポートした後のスタックでドリフトを検出しないか確認しておきましょう。 今回は無事ドリフトを検出することなく作業を終えることができました。

f:id:furusax0621:20201201234140p:plain

まとめ

CloudFormationで不意にドリフトを検出してしまったリソースの再インポート手順について、今回はAurora MySQLのエンジンバージョンを事例にまとめました。

CloudFormationのインポート機能を使ったのは初めてでしたが、とても便利な機能だなと思いました。 最初からCloudFormationを利用してる環境ではあまり登場シーンが無いかもしれませんが、覚えておいて損はない機能でした。

参考