こんにちは。ROBOT PAYMENTでSREをしています @trunkatree です。 今回は、TerraformでFeature Toggleを実装する機会があったので、それについてご紹介したいと思います。
きっかけ
今年はじめ頃、請求管理ロボでは本番環境とは別にデモ環境を用意し提供することになりました。このデモ環境は、お試しやシステム連携の開発等で利用していただく環境です。そのため基本的には本番環境と同じ機能を提供し、必要に応じて先行リリースを行うような運用をしています。
アプリケーションに加えTerraformでの開発も必要な機能を先行リリースしたいという話が出てきました。環境ごとに別のソースコードをデプロイしたい場合、環境ごとにブランチを分ける運用(例えば本番環境はmainブランチでデモ環境はdemoブランチなど)にすることも考えられますが、開発期間中に随時mainブランチを取り込んだり差分を直したりするのは手間に感じます。mainブランチ一本で運用できれば、それがシンプルでよさそうです。
Feature Toggleという開発手法については以前から聞いたことがあり、いつか取り入れてみたいと気になっていたので、今回の場面にぴったりではないかとアイデアが浮かびました。
Feature Toggleとは
Feature Toggleとは、簡単に言うと何か機能(Feature)について「公開する(ON) / 公開しない(OFF)」を切り替えるスイッチ(Toggle)のようなものをコード中に実装しておき、そのスイッチによってリリース状態を切り替える開発手法のことです。Feature Flagと呼ばれることもあるようです。スイッチをOFFにしておけばユーザーには公開されていない状態になるので、実装中のコードも随時コードベースに入れていきながら開発を進めることができます。チーム開発においては、開発者間でのコードの調整作業をより早い段階でできるようになるメリットがあります。また、リリース後に問題が見つかった際にはよりわかりやすくロールバックできるというメリットもあります。詳しくは他のページに譲ります。
実装
まず前提として、請求管理ロボのTerraformコードはWorkspace機能を使って環境を切り替える構成となっており、全環境が単一のコードによって作成されています。そうでない例としては、環境ごとにディレクトリを切ってそれぞれで実装する構成などがあると思います。特定環境に機能を実装する場合、環境ごとにコードが分かれているのであれば必要な環境にだけ実装すればよいということになると思います。しかし単一のコードで複数環境を作成している場合、何か細工を入れない限りは全環境に同じものができ上がることになるので、コードの強制力によって環境差異が生まれにくいのが良い点である一方、柔軟性にかけるという側面もあります。なのでFeature Toggleという手法が特に必要とされている状況にあると言えます。
実装にはTerraformのcount機能を使います。ちょっと泥臭いなとも感じたのですが、Terraformの公式ブログでも紹介されていました*。実は、請求管理ロボではこのcount機能によって"本番環境にだけ作成する"という実装がたびたびあり(←手間がかかるところ)、count自体は見慣れていました。そんな背景もありつつ、今回改めてFeature Toggleという考え方を踏まえた上でcount機能を使うことになりました。
* https://www.hashicorp.com/blog/terraform-feature-toggles-blue-green-deployments-canary-test
さて、サンプルコードになります。定時で起動するバッチ "sample_batch" を作成します。請求管理ロボのバッチ基盤にはAWS Batchを用いており、サンプルコードでは
- aws_cloudwatch_event_rule
- aws_cloudwatch_event_target
- aws_batch_job_definition
を定義しています。EventBridge(旧CloudWatch Events)でスケジュール設定をし、Batchのジョブ定義を用意した上でスケジュールのターゲットに設定します。
resource "aws_cloudwatch_event_rule" "sample_batch" { # feature toggle count = terraform.workspace == "prod" ? 0 : 1 name = "sample_batch-${terraform.workspace}" description = "schedule sample_batch" schedule_expression = "cron(0 * * * ? *)" is_enabled = true } resource "aws_cloudwatch_event_target" "sample_batch" { # feature toggle count = terraform.workspace == "prod" ? 0 : 1 target_id = "sample_batch-${terraform.workspace}" rule = aws_cloudwatch_event_rule.sample_batch[count.index].name arn = aws_batch_job_queue.sample_queue.arn role_arn = aws_iam_role.sample_cloudwatch_events.arn batch_target { job_definition = aws_batch_job_definition.sample_batch[count.index].name job_name = "sample_batch" } } resource "aws_batch_job_definition" "sample_batch" { # feature toggle count = terraform.workspace == "prod" ? 0 : 1 name = "sample_batch-${terraform.workspace}" type = "container" retry_strategy { attempts = 3 } lifecycle { create_before_destroy = true } container_properties = <<CONTAINER_PROPERTIES { # ここは省略 } CONTAINER_PROPERTIES }
各resourceにcountを書いています。1
ならばそのresourceは1つ作成され、 0
ならば作成されません。前述の通りWorkspace機能によって環境を切り替えているので、 terraform.workspace
で条件分岐させています。
count機能を使ったresourceには、tfstate上のresource名に番号 [count.index]
が付与されるので、 aws_cloudwatch_event_rule.sample_batch[count.index].name
や aws_batch_job_definition.sample_batch[count.index].name
のような記述をする必要があります。ここはちょっと面倒なところです。
通例な実装にcountを追加しただけですが、対象リソースが増えると管理が大変になってきます。Feature Toggleを実装しているのだとわかりやすいよう、変数を用いて名前付けしたほうがベターに思えてきます。次のようにリファクタリングしました。
variable "feature_toggle" { type = map(string) default = { "sample_batch.prod" = false "sample_batch.demo" = true } } resource "aws_cloudwatch_event_rule" "sample_batch" { count = var.feature_toggle["sample_batch.${terraform.workspace}"] ? 1 : 0 name = "sample_batch-${terraform.workspace}" description = "schedule sample_batch" schedule_expression = "cron(0 * * * ? *)" is_enabled = true } resource "aws_cloudwatch_event_target" "sample_batch" { count = var.feature_toggle["sample_batch.${terraform.workspace}"] ? 1 : 0 target_id = "sample_batch-${terraform.workspace}" rule = aws_cloudwatch_event_rule.sample_batch[count.index].name arn = aws_batch_job_queue.sample_queue.arn role_arn = aws_iam_role.sample_cloudwatch_events.arn batch_target { job_definition = aws_batch_job_definition.sample_batch[count.index].name job_name = "sample_batch" } } resource "aws_batch_job_definition" "sample_batch" { count = var.feature_toggle["sample_batch.${terraform.workspace}"] ? 1 : 0 name = "sample_batch-${terraform.workspace}" type = "container" retry_strategy { attempts = 3 } lifecycle { create_before_destroy = true } container_properties = <<CONTAINER_PROPERTIES { # ここは省略 } CONTAINER_PROPERTIES }
variable "feature_toggle"
を追加し、countを var.feature_toggle["sample_batch.${terraform.workspace}"] ? 1 : 0
に変更しました。多少スイッチっぽくなったのではないでしょうか。コメントも削除しました。
type = map(string)
を使っているのは、既存のコードに合わせたという理由です。
おわりに
TerraformでFeature Toggleを実装する例でした。実装そのものは難しいことはないと思いますが、Feature Toggleという開発手法をプロジェクトの中で実践してみることができたというのが一番の収穫だったと思っています。本番環境を守りつつ効率よく開発を進めるために、やり方を工夫することで対応するという姿勢を大事にしていきたいです。今回はインフラリソースだけが対象でしたが、これを景気づけに今後アプリケーション開発においても検討していくことができたらと思います。
We are hiring!!
ROBOT PAYMENTでは一緒に働く仲間を募集しています!!!
speakerdeck.com
www.robotpayment.co.jp
🎉twitter採用担当アカウント開設!🎉どんどん情報発信していきます!!