TL;DR
Workspace機能を用いて複数環境を管理する代わりに、シンボリックリンクを駆使して共通設定を使い回します。
どうしてWorkspaceを使わないの? …という理由については、本記事の下の方に書いています。
サンプルリポジトリ
サンプルリポジトリを https://github.com/progrhyme/sample-terraform-symlink に作りました。
※動作確認はしていないので、何かありましたらプルリクエストなど下さい。
構成概要
ディレクトリ構成
.
├── dev/
│ ├── .envrc
│ ├── dev.auto.tfvars # dev環境の共通変数定義
│ ├── app/
│ │ ├── app-main.tf -> ../../shared/app-main.tf
│ │ ├── app-variables.tf -> ../../shared/app-variables.tf
│ │ ├── backend.tf
│ │ ├── common-variables.tf -> ../../shared/common-variables.tf
│ │ ├── dev.auto.tfvars -> ../dev.auto.tfvars
│ │ ├── global.auto.tfvars -> ../../shared/global.auto.tfvars
│ │ ├── main.tf
│ │ ├── provider.tf -> ../../shared/provider.tf
│ │ ├── terraform.tfvars
│ │ └── variables.tf
│ └── infra/
│ ├── backend.tf
│ ├── common-variables.tf -> ../../shared/common-variables.tf
│ ├── dev.auto.tfvars -> ../dev.auto.tfvars
│ ├── global.auto.tfvars -> ../../shared/global.auto.tfvars
│ ├── infra-main.tf -> ../../shared/infra-main.tf
│ ├── infra-outputs.tf -> ../../shared/infra-outputs.tf
│ ├── infra-variables.tf -> ../../shared/infra-variables.tf
│ ├── main.tf
│ ├── outputs.tf
│ ├── provider.tf -> ../../shared/provider.tf
│ ├── terraform.tfvars
│ └── variables.tf
├── modules/
│ ├── compute/
│ │ ├── main.tf
│ │ └── variables.tf
│ ├── db/
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── network/
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── prod/
│ ├── .envrc
│ ├── prod.auto.tfvars # prod環境の共通変数定義
│ ├── app/
│ │ ├── :
│ │ └── variables.tf
│ └── infra/
│ ├── :
│ └── variables.tf
└── shared/
├── app-main.tf # appの共通レシピ
├── app-variables.tf # appの共通変数設定
├── common-variables.tf # globalと環境ごとの共通変数設定
├── global.auto.tfvars # 全環境・セグメントで共通の変数定義
├── infra-main.tf # infraの共通レシピ
├── infra-outputs.tf # infraの共通Output
├── infra-variables.tf # infraの共通変数設定
└── provider.tf
terraform実行例
cd dev/infra
terraform init # 初回のみ
terraform apply
このように dev/infra
や prod/app
といったディレクトリがterraformを実行する場所になるわけですが、以降はこのterraform実行単位を「セグメント」と称することにします。
ポイント
- トップ階層に
dev/
,prod/
といった環境単位のディレクトリを置いています。 -
dev/.envrc
,prod/.envrc
は、direnvを使ってAWS_PROFILE
を切り替えるためのものです。 - 環境ごとに共通の変数定義は
dev/dev.auto.tfvars
,prod/prod.auto.tfvars
にまとめています。 - そのほか環境をまたいで共有するレシピは
shared/
配下に置きます。
メリット
- 設定をDRYにできる。
- どの変数を修正したらどの環境/セグメントに影響するかがわかりやすい。
- direnvによって、環境ごとにクラウドの認証情報が異なる場合に、ディレクトリ移動だけで切り替えが可能。
課題
割とsymlinkが多く、複雑な構成になってしまったので、かえってわかりにくいという意見もあるかもしれません。
利用しているテクニック
Variable Filesの自動読込み
terraformを実行すると、カレントディレクトリの terraform.tfvars
ファイルと *.auto.tfvars
というsuffixのファイルが自動的に読み込まれます。
See https://www.terraform.io/docs/configuration/variables.html#variable-files
例えば、 dev/infra
セグメントでは、 global.auto.tfvars
, dev.auto.tfvars
, terraform.tfvars
の3つのVariables Filesをロードしています。
ちなみに、このファイル群が読み込まれる順序を把握していれば、最初に読み込まれるファイルでデフォルト値を定義し、後に読み込まれるファイルで上書きするという手法も使えますが、最終的にどの変数定義が使われるかがわかりにくくなるので、今はそういうことはしない方針にしています。
Remote Stateの参照
infra
で構築したネットワークの情報を app
から参照するようにしています。
以下に、サンプルリポジトリ中で関係するコードの一部を示します:
data "terraform_remote_state" "infra" {
backend = "s3"
config {
bucket = "<backet name>"
key = "dev-infra.tfstate"
region = "<region>"
}
}
module "compute" {
source = "../../modules/compute"
compute_bastion = "${var.compute_bastion}"
ec2_ssh_key_name = "${var.ec2_ssh_key_name}"
network = "${data.terraform_remote_state.infra.network}" # ここで参照
}
参考:
VariableやOutputでmapを活用
上記の network
変数もそうですが、適宜mapを使うことで、variableやoutputを何度も記述する手間を減らしています。
例:
output "outputs" {
value = "${map(
"vpc_main_id", "${aws_vpc.main.id}",
"security_group_main_default", "${aws_vpc.main.default_security_group_id}",
"subnet_main_public1", "${aws_subnet.main_public.0.id}",
"subnet_main_public2", "${aws_subnet.main_public.1.id}",
"subnet_main_private1", "${aws_subnet.main_private.0.id}",
"subnet_main_private2", "${aws_subnet.main_private.1.id}",
)}"
}
variable "vpc_main" { type = "map" }
variable "subnets_main" { type = "map" }
variable "db_main" { type = "map" }
vpc_main = {
cidr = "10.0.0.0/16"
}
subnets_main = {
public = ["10.0.0.0/24", "10.0.1.0/24"]
private = ["10.0.2.0/24", "10.0.3.0/24"]
}
db_main = {
family = "mysql5.7"
engine = "mysql"
engine_version = "5.7.19"
instance_class = "db.t2.micro"
storage_type = "gp2"
allocated_storage = 20
name = "<db-name>"
username = "<db-username>"
password = "<db-password>"
}
参考:
- https://www.terraform.io/docs/configuration/variables.html#maps
- https://www.terraform.io/docs/configuration/interpolation.html#map-key-value-
Workspace機能について
Workspace機能を使うために
以上のような構成を前提とすると、環境間で構築するコンポーネントに差分がなく、WorkspaceごとにVariable Filesを上手く切り替える仕組みを整えれば、Workspace機能を使っても良さそうです。
「当該Workspaceでだけロードされるファイル」という仕組みがWorkspace機能の方で実装されたら、コンポーネント差異も吸収できるので、もっと使いやすくなりそうですね。
試してみてつらかったところ
Terraform Best Practices in 2017 - Qiitaのエントリを参考に一度、試してみたのですが、以下がつらみでした:
-
lookup
地獄。書くのがつらかった。 - Terraformのmapは要素に異なる型のものを入れられないといった制約があり、すべてをmapに押し込めるのが難しいと感じた。
まとめ
Terraformでsymlinkを使って設定を共通化する構成例を紹介しました。
たぶん、似たようなことをやっているプロジェクトは多いと思いますが、意外と記事など探してもあまり見つからなかったので、書いてみました。
どなたかの参考になれば幸いです。