Terraformで管理しているRDS MySQLをなるべくダウンタイムなしにAurora MySQLへ移行した話

## はじめに

先日たまたま目にした [Railsを主戦場としている自分が今後学ぶべき技術について(随筆)](https://blog.unasuke.com/2020/i-have-to-learn-those-things-in-the-future/) を読んで、自分もRailsを主戦場として8年ぐらい仕事をしていて状況が似ている上に、今後必要になるであろう技術の認識も近かったので首がもげるほど共感しつつ、同時に焦燥感を募らせておりました。

記事ではTypeScriptなどのフロント方面にスキルを広げられていましたが、私はというと（諸事情により自分がインフラもみることになったので） インフラ方面にも手を広げることにしました[^1]。

今回は**Terraformで管理されているRDS MySQLをAurora MySQLへ移行する**というRailsが主戦場なアプリケーションエンジニアとしてはレアな実績を解除したので、その話を書きたいと思います。

これは[Hameeアドベントカレンダー](https://qiita.com/advent-calendar/2020/hamee)19日目の記事です。

## 背景

私は現在、とあるIoT製品の開発プロジェクトに携わっているのですが、新製品発表を控えているためユーザー数の拡大に備えてDBをRDS MySQLからAurora MySQLへ移行しようという計画が出てきました。。。嘘です。私がAuroraにしたいって言いました。そのためのコスト試算もして稟議も通しました。Hameeは目的に合致していればエンジニアの提案にも寛大ないい会社です。

すでにIoT製品と専用アプリがリリースされており、（いまは）ユーザー数は少ないながらも稼働しているサービスのDBを差し替えることになるため、ダウンタイムは最小限に移行作業を行う必要がありました。

かんたんにまとめると以下のような状況です

* 既存DBはRDS MySQLで稼働中
* ユーザー影響を最小限にするためダウンタイムはなるべくなしでAurora MySQLに移行したい

## 移行方法の検討

移行方法については公式のドキュメント（[Amazon Aurora MySQL DB クラスターへのデータの移行](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Migrating.html)）を参考に、AWSのソリューションアーキテクト（SA）さんにも相談して[Aurora リードレプリカを使用した、MySQL DB インスタンスから Amazon Aurora MySQL DB クラスターへのデータの移行](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Migrating.RDSMySQL.Replica.html)を選択しました。

リードレプリカを利用した移行でもいくつか方法があるのですが、今回は以下の2つ方法を検討しました

1. 内部DNSを利用してDBの接続先を抽象化し、DNSの向き先をAuroraへ変更する
2. 環境変数で定義しているDBのホスト情報をデプロイして変更する

#### 1. 内部DNSを利用してDBの接続先を抽象化し、DNSの向き先をAuroraへ変更する

アプリケーションのDB接続先ホストに内部DNSで発行したドメインを指定し、DNSのCNAMEを変更することでAuroraに向ける方法です。文章で書くと分かりづらいので簡単に書くとこんな感じ。

```
現状:
App -> DB(rds.database.host)

移行前:（RDS MySQL）
App -> dns.database.host(DNS) -> DB(rds.database.host)

移行後:（Aurora MySQL）
App -> dns.database.host(DNS) -> DB(aurora.database.host)
```

* メリット
    * DNSのTTLを短く（5秒とか）指定することで高速に切り替えが可能（アプリケーションのデプロイなども不要）
    * 変更対象がDNSのCNANEだけでよい（アプリケーションが複数ある場合、複数箇所の変更が必要）
* デメリット
    * 内部DNSの名前解決のためにRoute53のコストがかかる
    * DNSの浸透までの時間が本当に指定時間で済むのか不明

DNSを利用したスマートな移行方法でSAさんに相談した結果でもこちらをおすすめされました。コストに関しては移行が完了した後に直接参照方式に変更してしまえば削減できるため、それほど大きな問題にはならなそうですし、やはり高速に移行できるというのが最大の魅力ですね。

#### 2. 環境変数で定義しているDBのホスト情報をデプロイして変更する

アプリケーションのDBの接続先情報をRDS -> Auroraに変更する方式。先程と同じように表現するとこんな感じ。

```
現状:（RDS MySQL）
App -> DB(rds.database.host)

移行後:（Aurora MySQL）
App -> DB(aurora.database.host)
```

* メリット
    * （DNS方式と比べると）Route53のコストがかからない
* デメリット
    * 切り替えまでにかかる時間がアプリケーションのデプロイ時間に左右される
    * DBの接続先情報の管理の仕方によっては修正が複数箇所になる可能性がある

### 移行方法の決定

上記を踏まえて、まずは**1. 内部DNSを利用してDBの接続先を抽象化し、DNSの向き先をAuroraへ変更する**方式を採用することにしました。ただ、不確定要素もあるため開発環境でこの方式を試して問題があるようならホスト情報の更新方式に変えようと考えていました。

## Auroraへの移行手順

ざっくりとした手順は以下の通りです。

1. Aurora MySQLのリードレプリカインスタンスを作成する
2. Route53でDB接続用の内部DNSのドメインの作成
3. 既存のアプリケーションの接続先を2のドメインに変更
4. RDS MySQLをリードオンリーに変更
5. Aurora MySQLのレプリケーションが追いつくのを待つ
6. Aurora MySQLをマスターに昇格
7. Route53で内部DNSのドメインをAuroraに変更

順番にTerraformのサンプルコードといっしょに見ていきます。

#### 1. Aurora MySQLのリードレプリカインスタンスを作成する

既存のDBとしてRDS MySQLがTerraform上でこんな感じで定義されているとして

```terraform
resource "aws_db_parameter_group" "mysql" {
  # （中略）
}

resource "aws_db_instance" "mysql" {
  # (中略）
   parameter_group_name = aws_db_parameter_group.mysql.id
}
```

Auroraのインスタンスを作成に必要なリソースを定義します。ここで一部抜粋して `aws_rds_cluster`と `aws_rds_cluster_instance` が記載します。[^2]

```terraform
resource "aws_rds_cluster" "aurora-mysql-cluster" {
  # (中略）
  # RDS MySQLのレプリケーションの定義
  replication_source_identifier = aws_db_instance.mysql.arn
}

resource "aws_rds_cluster_instance" "aurora-mysql" {
  # (中略）
  cluster_identifier = aws_rds_cluster.aurora-mysql-cluster.id"
}
```

これらの定義を適用すればAuroraのレプリケーションが作成されます

#### 2. Route53でDB接続用の内部DNSのドメインの作成

Route53を使ってDBの接続用ドメインを作成します。

```terraform
resource "aws_route53_record" "database" {
  # （中略）
  type    = "CNAME"
  ttl     = "5"
  records = [aws_db_instance.mysql.hostname]
}
```

この `ttl = "5"` がポイントです。ここでは5秒としていますが移行の要件に合わせて調整してください。

#### 3. 既存のアプリケーションの接続先を2のドメインに変更

DBの接続先情報をアプリケーションで管理する方法は様々ですが、ここではFargateのECSタスク定義内の環境変数で管理しているものとします。

```terraform
resource "aws_ecs_task_definition" "application" {
  # 中略
  container_definitions = <<-EOF
[
  {
    "environment":
      DATABASE_HOST = "${aws_route53_record.database.fqdn}"
  }
]
EOF
}
```

この定義を反映しておきます。ここまでで事前準備が完了しました。いよいよ移行作業に入ります。

#### 4. RDS MySQLを読み取り専用に変更

まずはRDS MySQLを読み取り専用に変更します。これはオンラインで適用が可能です。
Terraformでいうと `aws_db_parameter_group` に `read_only` パラメータを `1` に設定することで実現します。

```terraform
resource "aws_db_parameter_group" "mysql" {
  # （中略）
  parameter {
    name  = "read_only"
    value = "1"
  }
}
```

おいおいダウンタイムなしって言ってたのに書き込み停止してるやんけ！というツッコミもあるかとは思いますが、書き込みの停止を0にするのは難しいと判断し読み込みのみ可能ということで実際の移行も行いました。

#### 5. Aurora MySQLのレプリケーションが追いつくのを待つ

さて、これで書き込みがなくなったのでAuroraのレプリケーションがマスター（いまはメインって言うんでしたっけ）に追いつくのを待ちます。方法はSequelProなどのDBクライアントでAuroraに接続し `SHOW SLAVE STATUS` で `Seconds behind master` が `0` になっていればOKです。

#### 6. Aurora MySQLをマスターに昇格

続いてAuroraをマスター（メイン）に昇格します。
これでAuroraに書き込みができるようになります。

```terraform
resource "aws_rds_cluster" "aurora-mysql-cluster" {
  # (中略）
  # RDS MySQLのレプリケーションの定義をはずす
  # replication_source_identifier = aws_db_instance.mysql.arn
}
```

AWSのコンソール上でもマスタへの昇格ができますので、コンソールでの昇格後にTerraform側で上記設定を外すことを忘れなければどちらの方法でも問題はありません。

公式ドキュメント: [Aurora リードレプリカの昇格](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Migrating.RDSMySQL.Replica.html#AuroraMySQL.Migrating.RDSMySQL.Replica.Promote)

#### 7. Route53で内部DNSのドメインをAuroraに変更

最後にDNSの向き先をAuroraに向けてあげれば完了です。

```terraform
resource "aws_route53_record" "database" {
  # （中略）
  type            = "CNAME"
  ttl             = "5"
  records         = [aws_rds_cluster.aurora-mysql-cluster.endpoint]
}
```

文章にすると長いですが、実際の作業時間としては反映の待ち時間も含めて15分程度でした。

### 発生した問題点と解決策

これは完全には解決できていない問題なのですが、Route53を利用したDNS方式の切り替えに5分ほどかかりました。TTLは5秒に設定しているため、アプリケーション側でコネクションプーリングをしていると期待しているタイミングで切り替えが行われないかもしれません。結局自分たちはどうしたかというと、かなり強引ですがアプリケーションのデプロイをすることで強制的に再接続をさせて回避しました。

また、Rails x Auroraだと先述のRailsのコネクションプーリングとAuroraのフェイルオーバーの相性の問題で[書き込みができなくなるケースがある](https://qiita.com/alfa/items/d4c38d87cdf0462f3615)ことが発覚し、この記事の通り接続エラーが発生した場合は再接続してくれる `mysql-aurora` gemを導入しました。コンソール上で手動フェイルオーバーを実行して検証したところ、特に問題は発生しなかったため一応の解決と考えています。

## まとめ

今回Auroraへの移行を経験してみて思ったのは、Terraformがあってよかった・・・とAWSのSAさん神・・・でした。この2つがなかったら私では移行はできなかったと思います。社内にインフラエンジニアがいるものの、なぜか役回りとしてインフラの面倒も見ることが多いのですが、エンジニアとしては貴重な経験ができて結果オーライでしょうか。焦燥感--

この記事がだれかのお役に立てば幸いです。

[^1]: といってもTerraformで管理されているAWS上のリソースを必要に応じて調整しているレベルではあるので、いわゆる「インフラエンジニア」をがっつりやっているわけではありません。
[^2]: 実際には他にもsubnetやparameter_groupの定義も必要です

## 参考記事

* [Amazon Aurora MySQL DB クラスターへのデータの移行](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Migrating.html)
* [Aurora リードレプリカを使用した、MySQL DB インスタンスから Amazon Aurora MySQL DB クラスターへのデータの移行](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Migrating.RDSMySQL.Replica.html)
* [AuroraのフェイルオーバーとRailsのコネクションプールを共存させる方法](https://qiita.com/alfa/items/d4c38d87cdf0462f3615)