ROBOT PAYMENT TECH-BLOG

株式会社ROBOT PAYMENTのテックブログです

TerraformコードをリファクタリングしながらDBバージョンアップを進めた話

こんにちは。ROBOT PAYMENTでSREをしている @trunkatree です。 請求業務自動化サービスである 請求管理ロボ はAWS上にシステム構築しており、DB(データベース)にはAmazon Auroraを利用しています。インフラ構成管理にはTerraformを用いており、DBもそこに含めて管理しています。今回はDBバージョンアップ対応(= Auroraのアップグレード対応)の際に実施した、各環境のDBを順次バージョンアップしていくためのTerraformコードの工夫についてご紹介したいと思います。

はじめに

請求管理ロボには本番環境・デモ環境・検証環境など複数の環境があり、それぞれにDBが存在しています。DBのバージョンアップ対応を進めるにあたって、次のような流れを計画しました。

  1. 【検証環境1】まぁとにかくバージョンアップしてみる
  2. 【検証環境2】バージョンアップ作業手順の検証を兼ねてバージョンアップする
  3. 【検証環境3】バージョンアップにかかる時間の検証を兼ねてバージョンアップする
  4. 【デモ環境】本番環境システムメンテナンスのリハーサルを兼ねてバージョンアップする
  5. 【本番環境】システムメンテナンスにてバージョンアップする

このように検証を兼ねながら順次バージョンアップしていきます。本番環境ではシステムメンテナンスを実施するためにスケジュールの調整が必要ということもあり、今回の場合は最初のバージョンアップから約1ヶ月ほどの期間を要しました。

ところで、請求管理ロボのTerraformコードは、環境ごとにworking directoryを分けずにWorkspace機能を用いて全環境分の構成管理をしています。すなわち、基本的には全環境の構成が同一であることが前提であり、環境差異が生じないようにコードに強制力を持たせられることがメリットです。環境によって異なる設定値は変数によって振り分けています。裏返しですが、デメリットとしては柔軟性に欠ける点が挙げられます。 各環境の状態はメインブランチを正としており、CI/CDもその前提で作り込んでいるため、なるべくメインブランチとその時点の構成に差分が出ないように開発を進めたいところです。

今回のように、バージョンアップ前の環境・バージョンアップ済み環境が混在する期間があると、デメリットである柔軟性の低さが全面に出てしまいます。本ブログでは、Terraformの count メタ引数や moved ブロックを用いてデメリットをカバーしつつ、DBバージョンアップを進めていった様子をご紹介します。

前置き

実施時のTerraformのバージョンは 1.3.9 です。 DBまわりのTerraformコードについてですが、DBインスタンスやパラメータグループなどの実際のリソースはローカルModule ./modules/database にまとめて定義しており、working directory ./database ではModuleを呼び出しているだけ、というつくりになっています。以下、紹介のためのサンプルコードです。

  • ディレクトリ構成
$ tree -d | grep -e database -e modules
├── database
├── modules
│   ├── database
$
  • ./database/main.tf
module "database" {
  source = "../modules/database"
}
  • ./modules/database/main.tf
resource "aws_rds_cluster" "main" {
  // 省略
}

resource "aws_rds_cluster_instance" "main" {
  // 省略
}

resource "aws_rds_cluster_parameter_group" "main" {
  // 省略
}

resource "aws_db_parameter_group" "main" {
  // 省略
}

また今回のDBバージョンアップの操作は、 terraform apply コマンドではなく手作業で(AWSマネジメントコンソールからの操作で)行いました。これは、DBの状態を確認しつつ進めたい・再起動が必要、などの側面を考慮した結果です。

工程の概要

最初に作業の流れについて簡単に説明しますと、現状のTerraform Moduleのコピーを作成し、そちらをバージョンアップ後のコードに更新し、環境ごとに順次その新しいModuleに移行していきます。 どの環境でどのModuleを使うか?の指定には count メタ引数を用い、移行時には terraform state mv コマンドを実施し、なるべく手作業を避けるための工夫として moved ブロックを活用します。

注釈として、この方針は要するにModuleのバージョン管理をしていくようなものですが、今回の対象はローカルModuleなのでModule自体のバージョン管理はできません。ローカルModuleのバージョンはソースコードのバージョン管理と一体となっています。

In particular, modules sourced from local file paths do not support version; since they're loaded from the same source repository, they always share the same version as their caller.

developer.hashicorp.com

ちなみに、Moduleで count が使えるようになったのはバージョン 0.13moved ブロックが追加されたのは 1.1 です。継続的にバージョンを上げていくことの大切さを感じます。請求管理ロボでは Renovate を利用して継続的なバージョンアップを支える仕組みを導入しています。その経緯については少しだけこちらに書いています ↓

tech.robotpayment.co.jp

以下のような流れで実施します。

  1. 【事前作業】
    • 1-1. module.database に対してcountを導入し module.database[0] にする
    • 1-2. module.database-v2[0] を新規作成する
    • 1-3. module.database-v2[0] をDBバージョンアップ後のコードに更新する
  2. 【DBバージョンアップ作業(各workspaceにて)】
    • 2-1. module.database-v2[0] を用いるコードに更新する
    • 2-2. module.database[0] から module.database-v2[0] に移行する
    • 2-3. パラメータグループなどを更新するためにterraform applyを実行する
  3. 【事後作業】
    • 3-1. 移行が完了し不要となった module.database[0] などを削除する
    • 3-2. module.database-v2[0] を module.database に名称変更する

工程の詳細

サンプルコードを交えつつ、詳細を紹介していきます。

1. 【事前作業】

1-1. module.database に対してcountを導入し module.database[0] にする

module "database" {
+  count = contains(["prod", "demo", "test1", "test2", "test3"], terraform.workspace) ? 1 : 0
+ 
  source = "../modules/database"
}

+ moved {
+   from = module.database
+   to   = module.database[0]
+ }

workspace(環境)によってModuleを作成する/しないを振り分けていくためにcountを導入します。リスト ["prod", "demo", "test1", "test2", "test3"] の中に "terraform.workspace" がある場合は module.database を作成し( count = 1 )、そうでない場合は作成しない( count = 0 )ようにします。また、countを導入するとリソースアドレスにインデックスが付与され( module.database[0] )、既存の( module.database )とは別物のリソースと認識されてしまうため、このままでは terraform plan で差分が生じてしまいます。そこでmovedブロックを使用し、差分が出ないようにします。

1-2. module.database-v2[0] を新規作成する

$ tree -d | grep -e database -e modules
├── database
├── modules
│   ├── database
│   ├── database-v2
$

./modules/database に横並びする形で ./modules/databae-v2 を作成します。DBバージョンアップ後のコードはこちらに記述していきます。

1-3. module.database-v2[0] をDBバージョンアップ後のコードに更新する

./modules/databae-v2 のコードをバージョンアップ後の内容に更新します。DB本体のコードは、最初にバージョンアップした検証環境で terraform import し、その内容を参考に作成しました。また、バージョンアップに伴って必要なパラメータ設定の更新なども行います。

2. 【DBバージョンアップ作業(各workspaceにて)】

2-1. module.database-v2[0] を用いるようにcount設定を更新する

module "database" {
-  count = contains(["prod", "demo", "test1", "test2", "test3"], terraform.workspace) ? 1 : 0
+  count = contains(["prod", "demo", "test2", "test3"], terraform.workspace) ? 1 : 0

  source = "../modules/database"
}

module "database-v2" {
-  count = contains([], terraform.workspace) ? 1 : 0
+  count = contains(["test1"], terraform.workspace) ? 1 : 0

  source = "../modules/database-v2"
}

DBバージョンアップ作業を手作業で実施するとともに、コードについても module.database[0] から module.database-v2[0] に移行するよう更新します。( ↑ "test1"を移行する場合)

2-2. module.database[0] から module.database-v2[0] に移行する

コード上の記述は module.database-v2[0] に移行しましたが、tfstate 上はまだ変わっていません。ここで terraform state mv コマンドを実行し、移行作業を完了させます。補足ですが、各環境を順次バージョンアップしていくため、ここではmovedブロックは使いません。movedブロックだと全workspaceに効いてしまいます。

$ terraform state mv module.database[0] module.database-v2[0]
Move "module.database[0]" to "module.database-v2[0]"
Successfully moved 1 object(s).
$

2-3. パラメータグループなどを更新するためにterraform applyを実行する

DB自体は手作業でバージョンアップしましたが、パラメータなどその他の設定は terraform apply コマンドで反映させます。あわせてDBの再起動なども行います。

これにてDBバージョンアップ作業は完了で、module.database-v2[0] によって構成管理している状態になりました。

3. 【事後作業】

全環境のDBバージョンアップが完了した後、今回の工程のために追加したコードを削除し、元の状態に戻しておきます。

3-1. 移行が完了し不要となった module.database[0] などを削除する

- module "database" {
-   count = contains([], terraform.workspace) ? 1 : 0
- 
-   source = "../modules/database"
- }

module "database-v2" {
  count = contains(["prod", "demo", "test1", "test2", "test3"], terraform.workspace) ? 1 : 0

  source = "../modules/database-v2"
}

- moved {
-   from = module.database
-   to   = module.database[0]
- }

すべてのworkspaceで module.database-v2[0] への移行が完了したので、不要となった module.database[0] とcount導入時に追加したmovedブロックを削除できます。../modules/database のソースコードもディレクトリごと削除します。

3-2. module.database-v2[0] を module.database に名称変更する

- module "database-v2" {
-   count = contains(["prod", "demo", "test1", "test2", "test3"], terraform.workspace) ? 1 : 0
- 
-   source  = "../modules/database-v2"
- }
+ module "database" {
+   source  = "../modules/database"
+ }

+ moved {
+   from = module.database-v2[0]
+   to   = module.database
+ }

moduleやdirectoryを名称変更して元のコードに近づけます。countも不要となったので削除します。movedブロックを追加しておくことによって、terraform plan で差分を出すことなく名称をリファクタリングすることができます。このままmovedブロックを残して作業完了としても問題ないですが、せっかくなので terraform state mv コマンドを実行してキレイにしておきます。

$ terraform state mv module.database-v2[0] module.database
Move "module.database-v2[0]" to "module.database"
Successfully moved 1 object(s).
$

これでmovedブロックを削除することができます。

- moved {
-   from = module.database-v2[0]
-   to   = module.database
- }

以上で全工程完了です。バージョンアップ期間中を配慮したコードに調整しつつ、最終的にはもともとのコードを素朴にバージョンアップしたような状態になりました。

補足ですが、公式ドキュメントを確認するとmovedブロックは削除せずに残すことが推奨されています。今回の場合はまわりの開発状況を把握できている状況だったため、問題ないと判断し削除しました。

We strongly recommend that you retain all historical moved blocks from earlier versions of your modules to preserve the upgrade path for users of any previous version.

If you do decide to remove moved blocks, proceed with caution. It can be safe to remove moved blocks when you are maintaining private modules within an organization and you are certain that all users have successfully run terraform apply with your new module version.

developer.hashicorp.com

終わりに

各環境のDBを順次バージョンアップしていく中での、Terraformコードの工夫についてご紹介しました。 moved ブロックは簡単かつ安全にリファクタリングするために導入された機能ですが、terraform plan で差分を出さずにコードをリファクタリングすることができるのはとても便利だと感じました。コードを変更するタイミングと実際の構成/設定やtfstateを更新するタイミングを分割することができ、それによってレビューが容易になったり、作業工程を明確にしてハンドリングしやすくすることができます。これはスムーズな開発や安全なシステム運用をする上で大きなメリットになると思います。 これからもよりよい開発運用フローを追求していきたいと思います。



We are hiring!!

ROBOT PAYMENTでは一緒に働く仲間を募集しています!!!

speakerdeck.com
www.robotpayment.co.jp
🎉twitter採用担当アカウント開設!🎉どんどん情報発信していきます!!