terraform state mvでStateファイル(tfstate)のリソース状態をマージする

エキサイト株式会社の@mthiroshiです。

Terraformを運用していると、ディレクトリ構造を整理したいケースがあります。 コード上での変更は容易ですが、それだけではStateファイル(tfstate)に差分が生まれてしまい、予期しないリソースの削除を招く危険があります。

今回は、TerraformにおけるStateファイルを安全にマージする方法について説明します。

Terraformのディレクトリ構成を変更するケース

例えば、S3のバケット作成の定義を下記のようなディレクトリ構造で管理しているとします。下記では、作成するバケットごとにmain.tfを分けており、それぞれのディレクトリでterraformコマンドを実行します。

s3
├── image_bucket
│   └── main.tf
└── log_bucket
    └── main.tf

この管理方法ではバケットが増えたときに煩雑になるため、上位階層のs3でimage_bucketとlog_bucketをマージしたいと思います。

s3
└── main.tf // image_bucket と log_bucketをマージ

単純にディレクトリ構成を変更するだけでは、Stateファイルとの差分が生まれてしまい、実体のリソースに対して意図しない破棄が起きてしまいます。 そのため、既にリソースが作成された後にディレクトリ構成やリソース定義を変更する場合、実体のリソースに変更が起きないようにStateファイルの修正が必要です。

Stateファイルのマージ

公式のヘルプセンターにStateファイルのマージ方法が記載されています。今回はこの方法に倣って行いました。

support.hashicorp.com

この手順では、Stateファイルのリソース状態を移動する terraform state mv コマンドを使っています。マージ元からマージ先にリソース状態を移動します。

マージ前の状態

先述した、ディレクトリを変更したいケースを元に手順を説明します。まずはマージ前の状態について説明します。

ディレクトリ構造です。

s3
├── image_bucket
│   └── main.tf
└── log_bucket
    └── main.tf

image_bucket/main.tfです。

terraform {
  required_version = "~> 1.3.4"

  backend "s3" {
    bucket                  = "sample-bucket"
    key                     = "s3/image_bucket/terraform.tfstate"
    region                  = "ap-northeast-1"
    shared_credentials_file = "~/.aws/credentials"
    profile                 = "sample-dev"
  }
}

provider "aws" {
  region                   = "ap-northeast-1"
  shared_credentials_files = ["~/.aws/credentials"]
  profile                  = "sample-dev"
}

terraform {
  required_version = "~> 1.3.4"

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

locals {
  bucket_name = "image-bucket"
}

resource "aws_s3_bucket" "image_bucket" {
  bucket = local.bucket_name
}

下記を実行して、Stateファイルを確認します。

$ terraform state list
aws_s3_bucket.image_bucket

aws_s3_bucket.image_bucket のリソースがあります。

同様に、log_bucket/main.tfには下記を変更した内容を設定します。

locals {
  bucket_name = "log-bucket"
}

resource "aws_s3_bucket" "log_bucket" {
  bucket = local.bucket_name
}

log_bucketのStateファイルも確認します。

$ terraform state list
aws_s3_bucket.log_bucket

マージ手順

マージ元のStateファイルの取得

マージ元のimage_bucketとlog_bucketのStateファイルをリモートバックエンドから取得して、ファイルに書き出します。書き出したファイルは、別途バックアップを作ります。

$ terraform state pull > image_bucket.tfstate
$ cp image_bucket.tfstate image_bucket.tfstate.txt

マージ先のStateファイルの作成

マージ先となる s3/ に空のStateファイルを作ります。

s3/main.tfです。

terraform {
  required_version = "~> 1.3.4"

  backend "s3" {
    bucket                  = "sample-bucket"
    key                     = "s3/terraform.tfstate"
    region                  = "ap-northeast-1"
    shared_credentials_file = "/root/.aws/credentials"
    profile                 = "sample-dev"
  }
}

provider "aws" {
  region                   = "ap-northeast-1"
  shared_credentials_files = ["/root/.aws/credentials"]
  profile                  = "sample-dev"
}

terraform {
  required_version = "~> 1.3.4"

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

下記 コマンドを実行し、マージ先のdestination.tfstateを用意します。

$ terraform init
$ terraform apply
$ terraform state pull > destination.tfstate

バックエンドをローカルに切り替える

ローカルのdestination.tfstateに対してマージ作業を行うので、バックエンドをローカルに切り替えます。

s3/の階層に、下記のoverride.tfファイルを作成します。

terraform {
  backend "local" {
  }
}

下記を実行して、バックエンドを変更を反映します。

terraform init --reconfigure

tfstateの移動 (マージ)

image_bucket.tfstate, log_bucket.tfstateに下記のリソースがあります。

image_bucket/image_bucket.tfstate

aws_s3_bucket.image_bucket

log_bucket/log_bucket.tfstate

aws_s3_bucket.log_bucket

これらを terraform state mv コマンドでマージ先に移動します。

$ terraform state mv -state=./image_bucket/image_bucket.tfstate -state-out=destination.tfstate aws_s3_bucket.image_bucket aws_s3_bucket.image_bucket

~~~~省略~~~~

Move "aws_s3_bucket.image_bucket" to "aws_s3_bucket.image_bucket"
Successfully moved 1 object(s).

これでリソースが移動されました。これをlog_bucketのtfstateに対しても行います。

そして、destination.tfstateのリソース状態に合わせて、s3/main.tfに下記を追記します。

locals {
  image_bucket_name = "image-bucket"
  log_bucket_name = "log-bucket"
}

resource "aws_s3_bucket" "image_bucket" {
  bucket = local.image_bucket_name
}

resource "aws_s3_bucket" "log_bucket" {
  bucket = local.log_bucket_name
}

terraform plan コマンドで、Stateファイルとコードに差分がないことを確認します。

$ terraform plan -state=destination.tfstate;

~~~~省略~~~~

aws_s3_bucket.image_bucket: Refreshing state... [id=image-bucket]
aws_s3_bucket.log_bucket: Refreshing state... [id=log-bucket]

No changes. Your infrastructure matches the configuration.

リソースの実体とtfstateに差分がないことが確認できました。

リモートのtfstateに反映

全てのリソースの移動完了後、マージ先のtfstateの"serial"要素の数値をインクリメントします。インラインコメントの部分を修正します。(本来JSONフォーマットにコメントは書けませんが、便宜上のものです。)

{
  "version": 4,
  "terraform_version": "1.3.4",
  "serial": 2, // 1 -> 2に更新する
  "lineage": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_s3_bucket",
      "name": "image_bucket",

~~~~省略~~~~

バックエンドをリモートに変更して、pushすれば完了です。

$ rm override.tf
$ terraform init -recofigure
$ terraform state push destination.tfstate

終わりに

Terraform におけるディレクトリ構成を変更する場合のStateファイルのマージ方法について説明しました。

大まかな流れとしては、元のStateファイルの内容を元に、マージ先の新しいStateファイルにリソース定義を移していく作業を行っています。 バックアップを取ってから行うと安全です。

参考になれば幸いです。

参考

support.hashicorp.com

qiita.com