長生村本郷Engineers'Blog

千葉県長生村本郷育ちのエンジニアが書いているブログ

Terraform 運用ベストプラクティス 2019 ~workspace をやめてみた等諸々~

f:id:kenzo0107:20190417103456p:plain

以前 terraform で workspace 毎に tfstate 管理する方法を執筆しましたが、実運用上いくつかの問題がありました。

結論、現在は workspace 運用をやめています。

kenzo0107.hatenablog.com

workspace 運用例

まずは実際の運用例です。

もっとうまいことやってるぞ!という話はあろうかと思いますが、まずはありがちなケースを紹介します。

例) セキュリティグループ作成

以下の要件を実現するセキュリティグループを作成するとします。

要件

  • stg では、社内で Wifi の ip からのみアクセス可
  • prd では、ip 制限なくアクセス可

サンプルコード

  • variables.tf
variable "ips" {
  type = "map"
  default = {
    stg.cidrs    = "12.345.67.89/32,22.345.67.89/32"
    prod.cidrs   = "0.0.0.0/0"
  }
}
  • security_group.tf
resource "aws_security_group" "hoge" {
  name        = "${terraform.workspace}-hoge-sg"
  vpc_id      = "${aws_vpc.vpc_main.id}"
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["${split(",", lookup(var.ips, "${terraform.workspace}.cidrs"))}"]
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
}

実際に terraform を plan/apply する前にまずは terraform workspace を定義する必要があります。

terraform workspace new stg // 既に作成されていたらエラーとなります。
terraform workspace select stg

// terraform workspace = stg とした場合の tfstate をローカルのメモリ上で管理します。
terraform init 

上記のような処理があって、初めて、 variable "ips"stg.cidrs, prd.cidrs が利用できるようになります。

こちらを運用しようとしてみると以下の様な問題にぶつかりました。

実運用との相性が悪い

検証の為、ステージングのみに反映させたい、という時にどう運用したら良いでしょうか。

ステージング用、本番用に設定して、プルリクエストが通って、サンプルのコードを master にマージしていたらどうでしょう?

本番にもデプロイして良さそうに見えます。

いや、むしろ反映されていなければ、混乱します。

その後に master にマージして、本番に反映させたいコードが会った時に、サンプルコードの部分は反映させたくない!と言っても反映されてしまいます。

かといって、以下の様なコードを複数リソースに入れていくのは、余計なステップ数も増え、脳内でリソースが消費されます。レビューするのも辛いです。

count = "${terraform.workspace == "stg" ? 1: 0}"

では、本番用は設定しなければいいじゃないか!と言って設定しないと、本番用はエラーを出す様になり、その他の反映が何もできなくなります。

これはステージングも本番も同じファイルを参照している為に発生しています。

また、以下の様な workspace を利用していると以下の様な問題もありました。

stg, prd 以外に新たに workspace を追加したい場合

以下要望があった場合にどうでしょうか。

  • 負荷試験をする為に本番同様の環境を用意してください」
  • 「外部 API との連携試験をしたいので環境を別途増やして欲しいです!」

例えば、 負荷試験環境を用意しようとすると、 loadtst という workspace を用意するとしたら variables.tf を以下のように修正が必要です。

variable "ips" {
  type = "map"
  default = {
    loadtst.cidrs = "12.345.67.89/32,22.345.67.89/32" // 追加
    stg.cidrs     = "12.345.67.89/32,22.345.67.89/32"
    prod.cidrs    = "0.0.0.0/0"
  }
}

上記例ですと variable "ips" に 1行加えただけで良いですが、実際は あらゆる変数に loadtst.*** = *** というコードを追加していく必要があります。

workspace が増える毎に step 数が増え、ファイルの見通しが悪くなります。

また、以下の様なコードがあると、こちらも脳内リソースを消費し、疲弊します。

lookup(var.ips, "${terraform.workspace}.cidrs")
"${terraform.workspace == "stg" ? hoge: moge}"

workspace 運用をまとめると

workspace の利用はリソースを複数環境で共有する ことで運用する想定の為に、可読性の悪化、実運用との乖離がありました。

  1. 新たに workspace 追加する際に、全ての変数 map に追加しなければならない。
    → コードの見通しが悪くなる。
    → 新規環境の構築難易度が上がる。

  2. ステージングのみに反映という時の実運用が困難
    → ステージングも本番も同じファイルを参照している為、ファイルの中でステージングの場合は?と処理を分ける必要が出てきてしまう。

  3. 今、どの workspace なのかがわかりずらく、 terraform apply する際にかなり躊躇してしまう。
    → 実際 terraform apply 実行前に terraform workspace show で workspace 確認しても、実行中で少し時間が経つと、「あれ?どっちだっけ?」と不安になり、 Terminal を遡って確認することがあったりしました。

ではどうすると良いか?

徹底的に workspace をやめます。

= DRY な設計しよう!

これに尽きます。

実際にどうしたか以下まとめました。

ディレクトリ構成は以下のようにしました。

modules/common ... stg, prd どちらの環境でも共通して同構成で作成するリソースを置きます。

modules/stg,prd ... 個々に異なる構成となるリソースを置きます。*1

.
├── README.md
├──envs/
│   ├── prd
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── provider.tf
│   │   ├── region.tf
│   │   ├── templates
│   │   │   └── user-data.tpl
│   │   └── variable.tf
│   └──stg/
│       ├── backend.tf
│       ├── main.tf
│       ├── provider.tf
│       ├── region.tf
│       ├── templates
│       │   └── user-data.tpl
│       └── variable.tf
│
└──modules
    ├── common
    │   ├── bastion.tf
    │   ├── bucket_logs.tf
    │   ├── bucket_static.tf
    │   ├── certificate.tf
    │   ├── cloudfront.tf
    │   ├── cloudwatch.tf
    │   ├── codebuild.tf
    │   ├── codepipeline.tf
    │   ├── network.tf
    │   ├── output.tf
    │   ├── rds.tf
    │   ├── redis.tf
    │   ├── security_group.tf
    │   └── variable.tf
    ├── prd
    │   ├── admin.tf
    │   ├── admin_autoscaling_policy.tf
    │   ├── api.tf
    │   ├── app.tf
    │   ├── ecr.tf
    │   ├── iam_ecs.tf
    │   ├── output.tf
    │   ├── variable.tf
    │   └── waf.tf
    └── stg
        ├── admin.tf
        ├── api.tf
        ├── app.tf
        ├── ecr.tf
        ├── iam_ecs.tf
        ├── output.tf
        ├── variable.tf
        └── waf.tf

例のセキュリティグループの作成を例にするとどうなるか

以下の様になります。

  • envs/prd/variables.tf
variable "cidrs" {
  default = [
    "0.0.0.0/0",
  ]
}
  • envs/stg/variables.tf
variable "cidrs" {
  default = [
    "12.345.67.89/32",
    "22.345.67.89/32",
  ]
}
  • envs/common/security_group.tf
resource "aws_security_group" "hoge" {
  name        = "${terraform.workspace}-hoge-sg"
  vpc_id      = "${aws_vpc.vpc_main.id}"
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["${var.cidrs"))}"]
}

resource "aws_security_group_rule" "https" {
  security_group_id = "${aws_security_group.hoge.id}"
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
}

もし stg だけに反映させたいセキュリティグループであれば、 envs/stg/security_group.tf に作成したいセキュリティグループを記述します。

これで stg だけ反映という実運用をカバーできます。

また、負荷試験環境 ( loadtst ) という環境を用意したい場合は、以下の様にコピーし、変数を修正すれば良いです。

  • envs/prdenvs/loadtst
  • modules/prdmodules/loadtst

多少構成に変更があろうとも、 loadtst 関連のリソースが prd, stg に影響することはない様に作成できます。

terraform コーディングルール

以下のような workspace の切り替えを利用したコードを利用しないことです。

lookup(var.ips, "${terraform.workspace}.cidrs")
"${terraform.workspace == "stg" ? hoge: moge}"

また、以下も NG とします。 stg だけ異なるのであれば、 modules/stg,prd と分けるべきです。

"${var.env == "stg" ? hoge: moge}"

terraform 実行手順

stg, prd 各環境構築は envs/stg, envs/prd ディレクトリに移動し、 以下実行します。

terraform init
terraform get -update
terraform plan
terraform apply

AWS credentials の扱い

stg, prd で同じ AWS Account を利用する場合、プロジェクトの root に direnv 等、 .envrc を置いて、運用するのが良いと思います。

stg, prd で異なる AWS Account を利用する場合、 envs/(stg,prd) 以下に .envrc をそれぞれ配置し、上記 terraform 実行手順 を実行すれば良いです。

プロジェクト毎の terraform バージョンの違いの対応

tfenv で対応します。

macOS%$ brew install tfenv

以前の執筆記事では terraform を one-off container で実行しバージョン差異を吸収する様にしていましたが、コマンドが長くなり、管理も煩雑になるので、tfenv が望ましいです。

こちらも運用してみての実感です。

その他

これはしといた方がオススメ?レベルですが、 provider で バージョン固定外した方が良かったです。

provider aws {
  version = "1.54.0"
  region  = "ap-northeast-1"
}

固定されていて、最新のリソースが利用できない時があります。*2

その時は、バージョン固定でなく、アップデートしていく方向で修正した方が、最新に追従できます。

総評

実運用をしてみて、 workspace はやめておいた方がいいかなと感じたことをまとめました。

勿論、 workspace の良さを知り尽くしてないからこういう意見になっているとも思いますので、一概に否定する意図はありません。

リポジトリの整理がついたら現段階で公開できるところをしていこうと思います!

以上 Terraform 運用されてる方の知見になりましたら幸いです。

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

*1:ECS + RDS + Redis 構成で CodePipeline からデプロイするサンプル terraform です。

*2:Aurora MySQL が作れない!と思ったら、バージョン固定してた為だったことがありました。