Terraform Workspacesの基礎と使い方について考えてみた! #AdventCalendar

2019.12.24

こんにちは(U・ω・U)
AWS事業部の深澤です。

さて、こちらは terraform Advent Calendar 2019 24日目の記事になります。

皆さん、 terraformのworkspace はご存知でしょうか。
これは環境を複数用意する際にterraform側でstateを分けて管理ができる機能になります。これを使うことで例えば環境がステージングと本番みたいに分かれていた場合、ディレクトリを分けなくても環境の管理が行えるようになります。ちょっと具体的に見ていきましょう。

workspaceの基礎

terraform workspaceでは以下のコマンドが提供されています。

terraform workspace -h
Usage: terraform workspace

  New, list, show, select and delete Terraform workspaces.

Subcommands:
    delete    Delete a workspace
    list      List Workspaces
    new       Create a new workspace
    select    Select a workspace
    show      Show the name of the current workspace

コマンドはシンプルですね。新しいworkspaceを作り(new)、workspaceの選択、切り替えを行い(select)、今のworkspaceを確認できて(show)、workspaceの一覧が確認できて(list)、workspaceの削除が行える(delete)。特に有効化は必要なく、最初からshowコマンドを実行するとdefaultがセットされている状態になります。

$ terraform workspace show
default

試しに2個、環境を作ってみます。

$ terraform workspace new prod
$ terraform workspace new stg

stg側でリソースを作ってみます。

$ terraform workspace show
stg
$ terraform apply
〜〜〜
Do you want to perform these actions in workspace "stg"?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

workspaceはstgで間違いないよね?と確認され、yesを押すと実際にworkspaceのstgにリソースが作成されます。当然ですが、この後planを実行しても差分は表示されません。

$ terraform workspace show
stg
$ terraform plan
〜〜〜
No changes. Infrastructure is up-to-date.
〜〜〜

ではもう一つの環境、prodに切り替えてplanを実行してみましょう。

$ terraform workspace select prod
$ terraform plan
〜〜〜
Plan: 3 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

差分が表示されました。このようにterraformの機能で環境を分けることができるのがterraform workspaceなんです。ちなみに、workspaceを作成すると、terraform.tfstate.dというディレクトリが作成されます。中身は以下のようになっています。

$ ls terraform.tfstate.d
prod stg
$ ls terraform.tfstate.d/stg
terraform.tfstate

このように、作成されたworkspace毎にディレクトリを切って、stateファイルは分けて管理されることになります。

僕がworkspaceで実際に経験したトラブル

さて、workspaceの基本は皆さんご理解いただけたかと思います。上記ではworkspaceをstg(ステージング)とprod(本番)に分けていますね。これは本当にそれで大丈夫なのか慎重に考えた方が良いです。例えば以下のようなコードがあったとします。

resource aws_vpc vpc {
  cidr_block = "10.0.0.0/16"
  tags = {
    "Name" = format(
      "adventcalendar2019-%s-vpc",
      terraform.workspace
    )
  }
}

resource aws_subnet public_a {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = format(
      "adventcalendar2019-%s-subnet-a",
      terraform.workspace
    )
  }
}

resource aws_subnet public_c {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    "Name" = format(
      "adventcalendar2019-%s-subnet-c",
      terraform.workspace
    )
  }
}

この状態で、subnetの数をステージングだけ増やして欲しいと言われた場合どうしますか。僕はこのような要求に対して以下のようにコードを書き足して対応しました。

resource aws_subnet public_a2 {
  count             = terraform.workspace == "stg" ? 1 : 0
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = format(
      "adventcalendar2019-%s-subnet-a2",
      terraform.workspace
    )
  }
}

resource aws_subnet public_c2 {
  count             = terraform.workspace == "stg" ? 1 : 0
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.0.4.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    "Name" = format(
      "adventcalendar2019-%s-subnet-c2",
      terraform.workspace
    )
  }
}

今も昔もterraformに条件分岐は存在しませんが、countと三項演算子を組み合わせることで、workspaceがstgでないならリソースを作成しない(count = 0)ということが定義できるのです。これが今回はsubnetだけだから良いのですが、意味もなくネットワークが増えていくと言うことは当然なく、その上に乗るリソースの差分もステージング環境と本番環境でバンバン発生していったのです。その度にこのcountを用いて回避し続けていくのは本当に地獄でした…。

この問題は公式でも言及されている

この情報は 公式 でも言及されています。

In particular, organizations commonly want to create a strong separation between multiple deployments of the same infrastructure serving different development stages (e.g. staging vs. production) or different internal teams. In this case, the backend used for each deployment often belongs to that deployment, with different credentials and access controls. Named workspaces are not a suitable isolation mechanism for this scenario.

とのことです。やっぱり本番とステージングみたいな分け方は推奨されていないんですね。

どうやって使おう?

いくつか方法を考えてみました。

テストに使う

考えてみましたと言っておきながら恐縮ですが、 公式 で言及されている使い方があります。

A common use for multiple workspaces is to create a parallel, distinct copy of a set of infrastructure in order to test a set of changes before modifying the main production infrastructure. For example, a developer working on a complex set of infrastructure changes might create a new temporary workspace in order to freely experiment with changes without affecting the default workspace.

terraformを普段から利用されている方ならあるあるだと思うのですが、planで問題なかったけどapplyしたらダメだったということ多いと思います。なので本番の環境と同じ環境をworkspaceで作成しておいて、テストはworkspaceで作成したコピー環境に対し実際にapplyで行い、想定通りにできれば本番にも流すという使い方ですね。

全く同じ環境のものを複数作成する

例えば、使うリソースが環境毎に全く差分がなくて、同じ環境を複数作りたいといった場合にworkspaceを活用できると思います。ただこちらはmoduleや繰り返し処理(count,for_each等)を用いても同じようなことができるので、その後の運用等も考慮してやった方が良さそうです。

差分が発生しなさそうな箇所にのみスコープを絞って活用する

例えばVPCのような他の環境と差分が発生しないリソースに限り、workspaceを活用するという案です。先ほどのVPCとSubnetのコードを元にしますと、以下のようなディレクトリ構成にします。

├── common
│   ├── config.tf
│   ├── output.tf
│   ├── terraform.tfstate.d
│   │   ├── prod
│   │   └── stg
│   │       └── terraform.tfstate
│   └── vpc.tf
└─── stg
    ├── config.tf
    ├── subnet.tf
    └── terraform.tfstate

common配下のvpc.tfには以下のように定義します。

resource aws_vpc vpc {
  cidr_block = "10.0.0.0/16"
  tags = {
    "Name" = format(
      "adventcalendar2019-%s-vpc",
      terraform.workspace
    )
  }
}

そしてこのVPCのIDを参照できるようにoutputを作成します。今回はoutput.tfを用意してみました。

output vpc_id {
  value = aws_vpc.vpc.id
}

このアウトプットされたtfstateを他の箇所で参照します。今回はstgディレクトリ内で次のように定義しました。

data terraform_remote_state stg_common {
  backend = "local"
  config = {
    path = "./common/terraform.tfstate.d/stg/terraform.tfstate"
  }
}

あとはこれをsubnetリソースから参照するだけです。stg/subnetに今回は定義しました。

resource aws_subnet public_a {
  vpc_id            = data.terraform_remote_state.stg_common.outputs.vpc_id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    "Name" = format(
      "adventcalendar2019-%s-subnet-a",
      terraform.workspace
    )
  }
}

ただこれはこれで環境毎のディレクトリ+共通ディレクトリも意識しなければならないので少し管理が煩雑になるのかなとも思います。

最後に

いかがでしたでしょうか。workspaceは一見便利に見えますが落とし穴があり運用の際には結構気を使わなくてはなりません。いくつか活用方法を考えてみましたが、なんだかんだ本家で言われているようなテスト目的で活用するのがベストなのかなぁという気もしています。

以上、深澤(@shun_quartet)でした!