terraform moved/import/remove blockを利用して脱terraformコマンドを目指す!

お久しぶりです。私ごとながら一軒家を購入しまして、荷物整理やら家具購入やら息子の保育園探しやらで怒涛の7,8月を過ごしておりました。なんとか今月も記事を書くことができてホッとしております。

今回はterraform バージョン1.7までに登場したmoved/import/remove blockを利用してterraformのstateを操作してみたいとおもいます。

moved/import/remove blockとは

moved/import/remove blockはterraformのstate操作をtfファイルに記述して行うことができます。

詳しい使い方は後ほど解説しますが、たとえばmovedブロックを利用する場合は以下のように記述します。そうすることでterraform state mvコマンドと同じくあるresourceのstateを別のresourceに移動することができます。

moved {
  from = aws_s3_bucket.from
  to = aws_s3_bucket.to
}

利用する理由

そもそもterraform stateコマンドがあるのになぜこのようなblockを利用する必要があるのでしょうか?

わざわざファイルに記載してplan, applyとする必要があるので一見コマンドで実行するより煩雑そうです。しかし、これらのblockを利用することには以下のようなメリットがあります。

  • stateに変更を加える前に差分を確認することができる
  • stateの変更をコードに残しておくことができる
  • リソースの作成や変更などと同時にstateの操作が可能

もちろんコード量が増える、実行までが面倒などデメリットもありますので状況に応じてどの戦略を取っていくかを判断していく必要がありますね。

準備

今回は検証用としてディレクトリを2つ(ex. terraformA terraformB)用意しました。それぞれの中身は同じで問題ありません。backend.tfやprovider.tfにはファイル名どおりbackendの情報やaws providerの情報を記載していますがこの辺りはお好みでご用意くださいませ。

.
├── README.md
├── terraformA
│   ├── backend.tf
│   ├── locals.tf
│   ├── provider.tf
│   └── s3.tf
└── terraformB
    ├── backend.tf
    ├── locals.tf
    ├── provider.tf
    └── s3.tf


まずはterraformAで単一のS3バケットを作成いましょう。s3.tfの中身はこんな感じです。

resource "aws_s3_bucket" "from" {
  bucket = "tf-blocks-${local.account_id}"
}

AWS ACCOUNT IDはlocals.tfというファイルを作成してdata経由で取得しています。

data "aws_caller_identity" "current" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
}


準備が完了しましたらplan, applyでリソースを作成します。

これでterraformAのみterraform stateが存在する状態を作ることができました!このstateをterraformBに各blockを利用して移行してみましょう。

import block

まずはimport blockから使ってみましょう。

terraformBではstateを管理していないのでterraform planを実行するとresourceの内容が全く同じにも関わらず新しくS3バケットを作成しようとしてしまいます。

❯ terraform plan
...

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.from will be created
  + resource "aws_s3_bucket" "from" {
 ...

まずはimport blockを利用してすでに作成されたs3バケットをterraformBのterraform stateに込んでみましょう。

import.tfを作成し、import blockを定義してきます。

import {
  id = "tf-blocks-${local.account_id}" # リソースの識別子
  to = aws_s3_bucket.from              # import 先
}

terraform planしてみると先ほど作成たs3バケットがimportされることがわかります、そのままapplyしましょう。

Terraform will perform the following actions:

  # aws_s3_bucket.from will be imported
    resource "aws_s3_bucket" "from" {
...
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

もう一度terraform planしてみるとNo changes.になっていることが確認できます!

removed block

これでterraformBにもstateが存在する状態となりました。同じリソースを複数のstateファイルで管理するのは良くないのでterraformAからstateを削除しましょう。

ただし、ここで直接resourceを削除してしまうとs3バケットが削除されてしまうのでstateを削除しつつリソースは残すようにしたいところですね。そんな時に有用なのがremove blockです。

terraformA側のs3.tfの内容はコメントアウトしておきます。resourceあるいはファイルごと削除しても問題ありません。

# resource "aws_s3_bucket" "from" {
#   bucket = "tf-blocks-${local.account_id}"
# }

続いてremoved.tfを作成しましょう。

removed {
  from = aws_s3_bucket.from
  lifecycle {
    destroy = false
  }
}

terraform planの結果が以下のようになっていれば、applyしましょう。

Terraform will perform the following actions:

 # aws_s3_bucket.from will no longer be managed by Terraform, but will not be destroyed
 # (destroy = false is set in the configuration)
 . resource "aws_s3_bucket" "from" {
        # (13 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

これでterraformAからstateが削除されましたが、リソースは残っています。完全にterraformBに管理が移りましたね。

moved block

ところでterraformBのs3.tfを見てみましょう。

resource "aws_s3_bucket" "from" {
  bucket = "tf-blocks-${local.account_id}"
}

リソース名が"from"となっています。terraformBは移行先なのでここは"from"ではなく"to"としたいところです。しかし、そのままresource名を変更してしまうとS3バケットが再作成されてしまいます。

moved blockを利用してS3バケットが再作成されないようにresource名を変更してみましょう。

まずはimport blockをコメントアウトするかimport.tfごと削除します。これからresource名を変更するため、import blockのtoで定義するresourceが存在しなくなってしまうためです。

# import {
#   id = "tf-blocks-${local.account_id}" # リソースの識別子
#   to = aws_s3_bucket.from              # import 先
# }

moved blockが定義されていればstateやS3バケットが削除されることはありませんのでご安心を。

次にresource名を変更します。

resource "aws_s3_bucket" "to" { # 変更
  bucket = "tf-blocks-${local.account_id}"
}

最後にmoved.tfを作成しましょう。

moved {
  from = aws_s3_bucket.from
  to   = aws_s3_bucket.to
}

これでplanすると以下のような結果になると思います。applyしましょう。

Terraform will perform the following actions:

  # aws_s3_bucket.from has moved to aws_s3_bucket.to
    resource "aws_s3_bucket" "to" {
...
        tags                        = {}
        # (12 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

これでresource名の変更も完了です。

以上で最初に作成したS3バケットに全く影響を与えることなくstateの操作が完了しました!

まとめ

今回はmoved/import/remove blockを利用してstateを操作してみまし。

terraform stateコマンドを利用するか、これらのblockを利用するか、はたまたtf-migrateを利用するかは組織の方針やサービス規模などによって異なると思います。

本記事がterraform戦略策定の一助となれば幸いです。

ここまで読んでいただきありがとうございました!

Terraform

Posted by CY