この記事はフラー Advent Calendar 2020 の4日目の記事です。3日目は @shogo82148 さんで「2020年に書いた GitHub Action + α」でした。
さて、フラーに入って1年半が過ぎ、GoによるWeb API開発だけでなくそれを運用するインフラ領域にもそこそこ真面目に手を出すようになりました。今回はそこでハマった話を書きます。
AWS CloudFormation
弊社、特に私が担当している案件では、インフラを主にAWSで構築・運用しています。 同じような構成のインフラをdevelopment/staging/productionと複数環境に構築することもままあるため、構築は基本的にAWSのCloudFormationを利用しています。
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
に更新してみます。
この状態で更新をかけ、しばらく待てばアップデート完了です。やったね!!!
……残念ながら、そう単純な話ではありません。CloudFormationのコンソールに移動して、データベースを管理しているスタックのドリフトを検出してみましょう。
ドリフトが検出された! エンジンバージョンを手動で更新してしまったので、テンプレートとの差分が出てしまいました。 エンジンバージョンは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
忘れがちですが、 DBCluster
と DBInstance1
の両方に設定しましょう。
修正したテンプレートは忘れずに反映しておきましょう。
STEP3. CloudFormationからリソースを削除する
さて、ここまで来たら一度スタックテンプレートから DBCluster
と DBInstance1
のリソースを削除します。
後でインポートする際に必要なので、スタックテンプレートではコメントアウトするに留めておくと良いでしょう。
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
を設定してるので、スタックから削除してもリソース自体は生き残ります。
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のコンソールでインポート先のスタックを開き、 スタックへのリソースのインポート
を選択します。
STEP4 で修正したスタックテンプレートをアップロードします。 アップロード済みのS3オブジェクトを指定する方法とテンプレートファイルをその場でアップロードする方法がありますが、今回はその場でアップロードする方法を選択しました。
スタックテンプレートから、インポートが可能なリソースが検出されます。
DBCluster
と DBInstance1
が無事検出されたので、インポートするデータベースの識別子を記述します。
確認画面で想定どおりのリソース紐付けができてることを確認したら、 リソースをインポート
を選択します。
インポート作業の成功を祈りながら見守ります。 IMPORT_COMPLETE
となれば成功です。やったね!!!
念の為、インポートした後のスタックでドリフトを検出しないか確認しておきましょう。 今回は無事ドリフトを検出することなく作業を終えることができました。
まとめ
CloudFormationで不意にドリフトを検出してしまったリソースの再インポート手順について、今回はAurora MySQLのエンジンバージョンを事例にまとめました。
CloudFormationのインポート機能を使ったのは初めてでしたが、とても便利な機能だなと思いました。 最初からCloudFormationを利用してる環境ではあまり登場シーンが無いかもしれませんが、覚えておいて損はない機能でした。