CloudFormation の管理が壊れてしまったのを復旧させた話

この記事は [Opt Technologies Advent Calendar 2019](https://qiita.com/advent-calendar/2019/opt-technologies) 8 日目の記事です。
7 日目の記事は @peko-858 さんの [【ScalikeJDBC 入門】SQLInterpolation を QueryDSL に書き換えてみた](https://qiita.com/peko-858/items/76f5b3fa114953a9da1e)です
9 日目の記事は @technicakidz さんの `AWSのChalice ハンズオンにいったよ` です

# CloudFormation の管理が壊れてしまったのを復旧させた話

誤って CloudFormation(以下 CFn)管理から外れてしまった Aurora の DBCluster を最近リリースされた [リソースのインポート](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html) 機能を利用して CFn 管理に復旧させました
そのときの話を書いてみようと思います

＊ステージング環境なので本番環境でやらかした話ではない

# 何が起こったか

CFn 適用時に DBCluster の削除に失敗し、「CFn 管理上は存在しないが、DB の実態は残ったまま」という状態になってしまっていました
また、それに際し関係する SecurityGroup なども削除をしようとして失敗し、同様に CFn 管理下から外れ実態は残ったままになってしまいました

# 結果

CloudFormation のリソースのインポートに [対応しているリソース](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html) についてはインポートをして CFn 管理に戻し、対応していない SecurityGroup などは作り直して復旧させました

# 環境

## 利用技術

- CloudFormation(aws-cdk @0.20.0)
- Aurora Serverless MySQL

aws-cdk が異常に古いのはチーム（主に私）の怠慢です（死）

## Stack の構造

- DatabaseStack: DBCluster を定義している Stack。DBCluster を定義する関係で、DB 用の SecurityGroup もここに定義
- ApplicationStack: アプリサーバの定義。アプリサーバから DB に繋ぐためにアプリサーバ用 SecurityGroup から DB 用 SecurityGroup への IngressRule 付与もこちらで実施

# 削除失敗までの経緯

（自分は長期休暇で不在だったので事前の状況については若干曖昧）

- ステージング環境でデータのマイグレーションを伴うアプリの機能をトライアンドエラーするために、DB を立て直そうという判断を下した
  - 他の方法、例えば dump 使うなど、を試したりはしていたが様々な事情でやめたとのことでした
- どうせステージング環境だし、と DBCluster を削除して Snapshot から作り直す、という方法を試すことにした

具体的には CFn テンプレートから DBCluster を一旦削除して、もう一度 DBCluster 定義を SnapshotIdentifier を指定して立て直すという手順によって実現しようとしたようです
ですが、CFn テンプレートから DBCluster の削除をして Stack の更新をするも、リソースの削除には失敗しつつ CFn でロールバックが実施されずにステータス上 `UPDATE_COMPLETE` になってしまっていました
これによって、「CFn 管理上は存在しないが、DB の実態は残ったまま」という状態になってしまいました

---

\*補足
CFn で定義された DBCluster の DeletionPolicy はデフォルトで `Snapshot` のはずなのに消えないのはなんでだったのか不明です
その時の CFn 適用時のイベントログを見ても `DBCluster StagingDatabase was not found during DescribeDBClusters` というメッセージが出ていたが、なぜ `DescribeDBClusters` で DBCluster を見つけられないのかがわからずこれ以上は原因を追うことが今となってはできない状態でした
また、そのとき適用した CloudFormation テンプレートがコミットログに残っていないので追うこともできない状態でした

---

またこのとき、その他のリソース（SecurityGroup、SubnetGroup、ClusterParameterGroup）も同様に実態は残ったままになりました
なお、ApplicationStack 上で定義されているアプリサーバから DB への IngressRule の削除も同じタイミングで実施して、こちらは削除に成功していました

# これによって発生した問題

- CFn テンプレートと実態が乖離し、本番環境への CFn 適用が出来なくなってしまっていた
- SecurityGroup の IngressRule が消えてしまっていたので、アプリケーションサーバから DB に接続ができず API サーバやバッチが軒並み死んでいた

# 復旧までの経緯

まず `CFn テンプレートと実態が乖離し、本番環境への CFn 適用が出来なくなってしまっていた` 問題の対応をしました

[リソースのインポート](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html) という、既存の AWS リソースを CFn 管理下に取り込むという機能を利用しました
他の選択肢はとくに検討しなかったです（状況としては `DBCluster という CFn 管理されていない既存リソースをStackに取り込みたい` という形になり、この機能を利用すれば解決できそうだという見込みだったので）

今回はリソースのインポートの準備として、以下の 2 点を実施しました

- SecurityGroup、SubnetGroup、ClusterParameterGroup を CFn にて同名で作成し直し
  - これらのリソースは先述の通り CFn 管理下にない状態になってしまったため
  - また、リソースのインポート時はインポートしたい Stack の CFn テンプレートを使って実施するので、先にこれらのリソースがないと DBCluster を定義できないため
- DeletionPolicy を定義
  - リソースのインポート機能は明示的な定義が必要で、もともとは定義していなかったため

aws-cdk を利用しているので、 `cdk synth` コマンドを利用して CFn テンプレートファイルの生成し、AWS コンソールよりインポートを実施して無事 DBCluster とその周辺リソースが CFn 管理下に戻せました
リソースのインポート自体はドキュメントに従って実施するだけで特に詰まるところはなかったですね

これによって、[削除失敗によって発生した問題](#削除失敗によって発生した問題) のうち、 `CFn テンプレートと実態が乖離し、本番環境への CFn 適用が出来なくなってしまっていた` 問題が解決です

---

次に `SecurityGroup の IngressRule が消えてしまっていた` の対応です

こちらは ApplicationStack を適用し直して SecurityGroup から IngressRule を付与するだけ・・・のつもりでした

が、aws-cdk が生成する CFn テンプレート上で、ApplicationStack の `AWS::EC2::SecurityGroupIngress` の Propeties の `GroupId` で指定した `Fn::ImportValue` で得られる値が古い論理 ID を参照しているという問題にあってしまい、DB 用の Stack で定義している `SecurityGroup` を正しく参照できず IngressRule を作成できないという事態に遭遇しました
結論から言うと一旦別名で SecurityGroup を作成し直して、もとの名前に残すことで解決出来ました
DBCluster などとは違って、こちらは作り直しが簡単に可能なリソースだったので、問題の究明さえできてしまえば作り直すだけでよかったので良かったですね

どういうことか詳しく説明します

先述の通り、DatabaseStack では DB 用の SecurityGroup と DBCluster の定義を、ApplicationStack ではアプリサーバ用のリソース定義及びアプリ用 SecurityGroup から DB 用 SecurityGroup への IngressRule 付与を実施していました
コードとしては以下のようになります（抜粋）

```ts:DatabaseStack.ts
// 前略
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DbSecurityGroup', { vpc: vpcRef });
this.databaseRef = {
  securityGroupId: new cdk.Output(this, 'SecurityGroupId', {
    value: dbSecurityGroup.securityGroupId,
  })
    .makeImportValue()
    .toString(),
};
// 後略
```

```ts:ApplicationStack.ts
const dbSecurityGroup = ec2.SecurityGroupRef.import(this, 'DbSecurityGroupRef', props.databaseRef);
const appSecurityGroup = new ec2.SecurityGroup(this, 'AppSecurityGroup', { vpc: vpcRef });
dbSecurityGroup.addIngressRule(appSecurityGroup, new ec2.TcpPort(3306));
```

以上のように、DatabaseStack で作成した SecurityGroup を Output 経由で参照できるようにし、ApplicationStack で `import` して参照を取って `addIngressRule` で IngressRule を付与しています
このときソースコードの以下の部分で ApplicationStack から DB 用 SecurityGroup への参照を取っています

```ts
ec2.SecurityGroupRef.import(this, 'DbSecurityGroupRef', props.databaseRef);
```

実際に出力する CFn テンプレートを確認すると、import の部分は以下のようになっておりました

```yaml:ApplicationStack.yml
Resources:
  DbSecurityGroupReffromApplicationStackAppSecurityGroup[**aws-cdkの生成するランダム文字列**]:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      # 略
      GroupId:
        Fn::ImportValue: DatabaseStack:SecurityGroupId
      # 略
```

どうもこの `Fn::ImportValue: DatabaseStack:SecurityGroupId` で参照している SecurityGroupId が _一度 CFn 管理から外れた古い SecurityGroup_ を参照しているような挙動をしていました（細かいところまで調査は出来ていないので、確信を持っては言えないです、すみません。。）
これは先の手順で DBCluster を CFn 管理下に復旧させた後に ApplicationStack を適用してもその状態が継続したままだったので、仕方なく DatabaseStack 側の SecurityGroup を一度別名で作成、これを ApplicationStack から参照させて動く状態にし、元の名前に戻すというやや強引な手順で参照を正常な状態に戻しました

# おわりに

だいぶややこしい話になってしまいましたが、以上のような流れで CFn 管理下から外れてしまったリソースを復旧できました

得られる教訓としては、

- DB のバックアップからの復旧手順に相当するものはちゃんと確立させておく（手順を作成し、実際に復旧できることまでちゃんと試す）
  - 2017 年にあった GitLab の大障害を見てるんだからやっとけよという話もありつつ・・・
- インフラをコード管理していても、コミットとしてちゃんと履歴を残さないと結局原因を追いにくくなってしまうので、実環境へ反映させた構成管理コードは必ずコミットして記録として残す
- ライブラリのバージョンはちゃんと上げる
  - 今回は aws-cdk のバージョンが古いことに起因して問題（あるいは問題解決の障壁）に繋がらなかったが、正直かなりヒヤッとした

あたりですかね
ともかく、無事 CFn 管理に巻き戻せて本当に良かったです
リソースのインポート機能を作ってくれて本当にありがとう AWS・・・