Help us understand the problem. What is going on with this article?

TerraformでWorkspaceを使わずに複数環境をDRYに設定する

More than 1 year has passed since last update.

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/infraprod/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 から参照するようにしています。
以下に、サンプルリポジトリ中で関係するコードの一部を示します:

dev/app/backend.tf
data "terraform_remote_state" "infra" {
  backend = "s3"

  config {
    bucket = "<backet name>"
    key    = "dev-infra.tfstate"
    region = "<region>"
  }
}
shared/app-main.tf
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を何度も記述する手間を減らしています。

例:

modules/network/outputs.tf
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}",
  )}"
}
shared/infra-variables.tf
variable "vpc_main"     { type = "map" }
variable "subnets_main" { type = "map" }
variable "db_main"      { type = "map" }
dev/infra/terraform.tfvars
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>"
}

参考:

Workspace機能について

Workspace機能を使うために

以上のような構成を前提とすると、環境間で構築するコンポーネントに差分がなく、WorkspaceごとにVariable Filesを上手く切り替える仕組みを整えれば、Workspace機能を使っても良さそうです。

「当該Workspaceでだけロードされるファイル」という仕組みがWorkspace機能の方で実装されたら、コンポーネント差異も吸収できるので、もっと使いやすくなりそうですね。

試してみてつらかったところ

Terraform Best Practices in 2017 - Qiitaのエントリを参考に一度、試してみたのですが、以下がつらみでした:

  • lookup地獄。書くのがつらかった。
  • Terraformのmapは要素に異なる型のものを入れられないといった制約があり、すべてをmapに押し込めるのが難しいと感じた。

まとめ

Terraformでsymlinkを使って設定を共通化する構成例を紹介しました。

たぶん、似たようなことをやっているプロジェクトは多いと思いますが、意外と記事など探してもあまり見つからなかったので、書いてみました。

どなたかの参考になれば幸いです。

progrhyme
Software Engineer. Was @key-amb
https://progrhy.me/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away