Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

TerraformでのRDS定義にMapを使うようになった理由

最近、AWS環境の構築・管理にはTerraformを利用しています。
その中で、RDSについては運用を考慮してMapを使って定義しているのでご紹介します。

Terraform使った時のDBリストアで危険操作

Terraformを使い始めた頃、他の人が構築した開発環境で以前のバックアップからAuroraクラスタをリストアしてほしいと依頼された時です。

私は、今あるaws_rds_clusterリソースにsnapshot_identifierを追加して対応しました。
snapshot_identifierが変更になるので、対象のaws_rds_clusterはreplaceとなります。
開発環境は利用されていないタイミングだったため、そのままreplaceで進めました。

ここで問題になるのは、DBバックアップはAuroraの自動バックアップのみであった事です。
replaceなので、TerraformはAuroraクラスタを削除した後にスナップショットを指定して同じな名前のクラスタを再作成しますが、Auroraの自動バックアップはクラスタと一緒に削除されてしまいます。
この時はリストアに成功しましたが、リストア元のスナップショットは消えてなくなりました。 リストアに失敗していたら消失しているところです。

この後、バックアップにAWS Backupを利用するようにしたと同時にコード化された環境でのDBリストアについて見直しを行いました。

リストアする時はどんな手順?

DBリストアする時、前述のように削除して同じ名前で再作成することが許されるとは限りません。

  • 切り替え前にデータ確認をしたい
  • 切り替え前に最新データをマージしたい
  • 切り替えに失敗したらすぐに元に戻したい
  • 切り替え後に古いDBの分析をしたい

上記のような理由から、問題のある旧DBとリストアした新DBを並行稼働する必要のある事が多いのではないでしょうか。
これには、

  1. 新しくリストアする
  2. アプリケーションの接続先を切り替える
  3. 古いDBを削除する

という3ステップを同時ではなく任意のタイミングで実行する必要があります。 それぞれの間でデータの確認や整備を行うためです。

ここでさらに、アプリケーションが参照していない方のDBはインスタンスタイプを落としたりしたいです。
新しいDBと古いDBは違う構成とし、アプリケーションの接続先を切り替える前には新しいDBを確実に本番用構成にする必要があるのです。

IaCでの対応

インフラをコード化していると現在のDBもコードとして管理しており、新しくサービスに投入するDBもコードで構成管理をする必要があります。

もっとも簡単に思いつく方法は、既存のコードをコピーして新しいDBを作成する方法です。
すると、以下のような問題が出てきます。

  • 緊急時、コピー後に変更が必要なところを洗い出すのが大変
  • 旧DBと新DBの変更点を見にくい
    • リソース名等の変更が必要な点とインスタンスタイプ等の意図的に変えたところが分かりにくい
    • 切り替え前に旧DBを新DBに合わせる時、必要な変更点を避けてコードを部分的にコピーする必要がある
  • リソースの変更による影響範囲が見えにくい
    • CloudWatch Alarmやアプリケーション接続先を格納したSecret等、切り替え時の影響範囲の洗い出しと修正が必要
  • 環境間でテンプレートファイルの共有ができない
    • たとえば検証環境と本番環境等で同じ構成としている時、リストアする環境だけ構成変更となり環境間のテンプレート差異が大きくなる

たとえば、一時的に以下のような変更点のある2つのリソースが作成することになります。

7,8c7,8
< resource "aws_db_instance" "db" {
<   identifier                      = "${var.env}-db"
---
> resource "aws_db_instance" "db2" {
>   identifier                      = "${var.env}-db2"
13c13
<   instance_class                  = var.db_instance_class
---
>   instance_class                  = "db.t3.small"
15c15
<   multi_az                        = var.db_multi_az
---
>   multi_az                        = false
24c24
<   #snapshot_identifier             = ""
---
>   snapshot_identifier             = "xxxxxxxxxxxxxxxxx"

DB接続先切り替えには、2つのDBの差分を比較して上記からリソース名とidentifiersnapshot_identifier以外の設定を合わせる必要があります。
また、DB接続先を切り替えた後はこの環境だけdb2という名前のリソースをメンテナンスしていくことになります。 DB自体の設定のみならず、このリソース情報を参照している箇所はすべてdb2を参照することが必要です。
同じテンプレートから生成した他の環境との差異が少なからず出てしまいます。

Terraformのfor_eachが好き

最終的に、Terraformのfor_eachを使って 最初から複数DBを稼働させる前提にテンプレートを作成する ようにしました。

まず、以下のように変数を定義しておきます。

variable "db_map" {
  description = "DB設定"
  type        = map
}
variable "db_target" {
  description = "DB設定"
  type        = string
}

これに対し、たとえば前述のような2つのDBを定義する時は変数を以下のように設定します。

db_target = "db"
db_map    = {
  db = {
      instance_class = "db.m5.large"
  }
  db2 = {
      instance_class      = "db.t3.small"
      multi_az            = false
      snapshot_identifier = "xxxxxxxxxxxxxxxxx"
  }
}

db_mapで作成するインスタンスを定義し、db_targetでアプリケーションの参照先インスタンス(本番稼働中インスタンス)を指定します。
コードの定義を丸ごとコピーするよりもすっきりしているのではないかと思います。
これにより、

  1. 新しくリストアする
  2. アプリケーションの接続先を切り替える
  3. 古いDBを削除する

はすべて変数の書き換えだけで対応できるようになり、環境間でのテンプレート差異も発生しなくなります。(差異は変数の値のみです)
また、2つの内容を合わせる時も数行の内容をすべてコピーして書き換えるだけです。

実際のリソース定義はfor_eachを利用し、変数で定義された数だけ作成します。

resource "aws_db_instance" "db" {
  for_each = var.db_map

  identifier                      = "${var.env}-${each.key}"
  db_name                         = "db"

  engine                          = "mysql"
  engine_version                  = lookup(each.value,"engine_version","8.0.20")
  instance_class                  = each.value["instance_class"]

  multi_az                        = lookup(each.value,"multi_az",true)
  ……<<中略>>……
  snapshot_identifier             = lookup(each.value,"snapshot_identifier",null)
}

lookup関数を使えばデフォルト値を設定することが可能です。
これにより、通常の本番設定とは変えたい部分だけを変数で定義できます。

接続先も変数化されているため、アプリケーションの接続先を設定にはaws_db_instance.db[var.db_target].endpointでエンドポイントを取得できます。
これにより、変数の設定1つでアプリケーションの接続設定やCloudWatch AlertのDimension等を同時に変更することが可能です。

最後に

IaCにより同じ構成の環境を横展開しやすくなりました。
同時に、IaCでの運用ルールに縛られて環境間で違いを出しにくくなってしまったり、コードが複雑化することもあると思います。
そのような時の1つの強力なツールとして、Terraformのfor_eachlookup関数を紹介させて頂きました。