RevComm Tech Blog

コミュニケーションを再発明し 人が人を想う社会を創る

チーム開発でも安全にTerraformのリファクタリングをしたい

はじめに

MiiTel Analytics Platformチームの小門です。

RevCommではサービス基盤にAWSとして利用していますが、IaCには主にTerraformを用いています。
基本的にTerraformコードはGitHubで管理され、プルリクエストを介してCI/CDを自動実行してリソースの構築、構成変更を行います。

最近、Terraformコードを管理するリポジトリを新設したり既存のコードをリファクタリングする機会があったためナレッジを共有します。

この記事では、Terraform v1.5以降に導入された機能であるimport/removed/movedブロックを活用する方法を紹介します。

動機

IaCの活用度合いはサービスや会社、チームの状況に大きく左右されると思います。 必ずしもプロジェクトの初期からIaCが整備されるとは限らないし、サービスや組織の変化に応じてコード管理の都合も変わることでしょう(弊社が正にそうです)。

Terraformは機能が豊富なため上記のような事情に柔軟にアプローチできます。 例えば既存のリソースをIaC管理に取り込む場合はterraform import、逆に特定のリソースをIaC管理から除外する場合はterraform state rmなどのコマンドがあります。

しかしIaCという特性上、手動コマンド実行によるtfstateを操作するのはチーム開発において不都合があります。 例えばチームの誰かによって同じタイミングでCI/CDが起動されると、お互いの変更が競合したりどちらかの変更が後勝ちするような可能性が考えられます。

このような場合でもTerraformの機能を有効活用することでチーム開発に支障をきたさずリファクタリングすることができます。

サンプルコード

AWSリソースを管理するための最小のサンプルとして、ECSクラスターを1つ管理するケースを考えてみます。

初めのディレクトリ構成:

.
└── provider.tf

provider.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

検証コードのバージョン

  • Terraform v.1.9.8
  • AWS provider: v5.78.0

importブロック

importブロックはTerraform v1.5以降で利用可能です。

terraform cliのterraform import [ADDR] [ID]と等価です。

import {
  to = [ADDR]
  id = "[ID]"
}

IaC管理されていないAWSリソースであるECSクラスターrevcomm-2024-adventcalendarを新たにIaC管理に含めます。
※terraform cliだとterraform import aws_ecs_cluster.foo revcomm-2024-adventcalendar

# main.tf
resource "aws_ecs_cluster" "foo" {
  name = "revcomm-2024-adventcalendar"
}

# import_ecs_cluster.tf
import {
  to = aws_ecs_cluster.foo
  id = "revcomm-2024-adventcalendar"
}
.
├── import_ecs_cluster.tf
├── main.tf
└── provider.tf

※好みですが、importブロックはいずれ削除可能なためmain.tfではなく別ファイルにすることで後で丸ごと消せるようにしています。

この状態でterraform planを実行すると以下のようになります。

$ terraform plan
...
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

1 to importかつ0 to add, 0 to changeのため新たにリソースは作成されず、また変更もされないことが分かります。

importブロックはコマンドの手動実行ではなくコード変更によってIaCの管理対象を操作することができます。

※以降のコードはterraform applyを実行 && import_ecs_cluster.tfを削除した状態とします。

removedブロック

removedブロックはTerraform v1.7以降で利用可能です。

その名の通りリソースをIaC管理から外す(リソース実体は削除しない)ためのもので、importブロックとは逆の用途です。
terraform cliのterraform state rmと等価です。
terraform state rm [ADDR]

removed {
  from = [ADDR]

  lifecycle {
    destroy = false
  }
}

lifecycleブロックでdestroy = falseとしてリソースを削除しないようにできます。

# main.tf
removed {
  from = aws_ecs_cluster.foo

  lifecycle {
    destroy = false
  }
}

# resource "aws_ecs_cluster" "foo" {
#   name = "revcomm-2024-adventcalendar"
# }

また(IaC管理から)削除するリソースのTerraformコードも合わせて削除する必要があります。 removedブロックも後から削除可能なため、私のチームでは初めコメントアウトに留め、その後removedブロック自体の削除時にresourceブロックも削除する形で運用しています。

planの実行結果は以下のようになります。

$ terraform plan
 # aws_ecs_cluster.foo will no longer be managed by Terraform, but will not be destroyed
 # (destroy = false is set in the configuration)
...
Plan: 0 to add, 0 to change, 0 to destroy.

0 to add, 0 to change, 0 to destroyのため、実際のリソースに変更は起きません。

movedブロック

removedブロックはTerraform v1.7以降で利用可能です。

Terraformコード上の管理名(ADDR)をリネームする場合に使用します。
terraform cliのterraform state mvと等価です。
terraform state rm [SOURCE] [DESTINATION]

moved {
  from = [SOURCE]
  to   = [DESTINATION]
}

上記までの例で、ECSクラスターrevcomm-2024-adventcalendarのTerraformコード上の管理名を(敢えて)fooとしていました。
しかし、この命名では役割が分かりづらいためいずれ問題が起きることでしょう。

これをfooからapiにリネームするリファクタリングを安全に行うことができます。

+ moved {
+   from = aws_ecs_cluster.foo
+   to   = aws_ecs_cluster.api
+ }
- resource "aws_ecs_cluster" "foo" {
+ resource "aws_ecs_cluster" "api" {
  name = "revcomm-2024-adventcalendar"
}

planの実行結果は以下のようになります。

$ terraform plan
  # aws_ecs_cluster.foo has moved to aws_ecs_cluster.api
  ...
Plan: 0 to add, 0 to change, 0 to destroy.

aws_ecs_cluster.fooaws_ecs_cluster.apiに変更しつつ、実際のリソースに影響がないため安全にリファクタリングを実行できます。

まとめ

Terraformのリファクタリングをチーム開発の中でも安全に実施する方法と簡単なケーススタディを紹介しました。

特にimportブロックとremovedブロックを併用することでリポジトリを跨いだリファクタリング、IaCコードの分割などが行えるのがとても気に入っています。