Terraformベストプラクティス解釈

最近、自分のチームでTerraformを使ってAWSオーケストレーションをやろうという話になった。

サーバー台数も10~20台程度、今まではAWSコンソールをポチポチしてインスタンスを用意していたが
サービス規模の拡大に伴って、手動オペレーションによるミスを防いだり、インフラ説明コストなどを省くこと、インフラ整理の意味合いで導入している最中。

Terraformに触るのは初めてだったので、ドキュメントを読みつつ基本的な機能について勉強しつつ
どのようなファイル構成で管理をするのがいいのか、経験者の同僚にアドバイスをもらいにいったところ
Hashcorpが公開しているベストプラクティスのリポジトリを教えてくれた。

github.com

ところがこの構成、けっこう複雑で初心者がいきなり読み始めるのは辛かったので
復習も兼ねてベストプラクティスを読み解いていく。

理解するために必要な要素

  • AWSの基本的な知識
  • 基本的なTerraformの操作
  • tfvarsファイルを使った変数管理
  • module機能

おおむねこの4つを理解すれば、その知識を使って読み解くことができるはず。
最初の2つは今回は解説を省きます。

tfvarsファイルを使った変数管理

Terraformではtfファイル内に変数を記述することができますが、変数に値を入力するには

  • tfファイル内に記述
  • 実行時にオプションで指定
  • 環境変数に埋めておく
  • tfvarsファイルに記述

の4種類の方法があります。

Input Variables - Terraform by HashiCorp

ベストプラクティスではtfvarsの手法が採用されており
以下のようなファイルが存在します。

best-practices/terraform.tfvars at master · hashicorp/best-practices

ここで定義した変数はtfファイル内で参照することができます。
ベストプラクティスでは環境ごとに利用する変数を全て1つのtfvarsファイルで管理しています。

module機能

Terraformにおけるmoduleは各resourceを抽象化するためのものです。

例えばベストプラクティスの中では

AWSのコンピューティングリソース(要するにEC2)群を管理する compute module
HAProxyが動くであろうインスタンスのresourceを抽象化した haproxy moduleが存在します。

ベストプラクティスでは、上に示したように

  • 各resource自体の抽象化 (haproxy module)
  • AWSのリソース群を示す抽象化 (compute module)

の2段階の抽象化が行われています。

さらにmoduleには、自分自身の変数をmodule外から出力するための outputという構文も用意されており
これを使うことで、例えばTerraformで作成したVPCのidをmoduleの外からも参照することが可能です。
https://github.com/hashicorp/best-practices/blob/master/terraform/modules/aws/network/vpc/vpc.tf#L17

ベストプラクティスでは、このmodule機能を使って
各resourceを抽象化、そして作成したresourceのidなど動的な情報をoutputすることで
module間の値の受け渡しを実現しています。

具体的にはEC2インスタンスを立ち上げる際に
先にネットワークを構築し、VPC, subnetなどのidをoutputしてEC2の設定に利用するなどの使い方をしています。

ベストプラクティスの構成

ここまでで、必要な知識は揃ったので
改めてベストプラクティスの構成を見ていきます。

module/aws
├── compute
│   ├── compute.tf
│   └── haproxy
│          ├── haproxy.sh.tpl
│          └── haproxy.tf
└── network
    ├── bastion
    │   └── bastion.tf
    └── vpc
     └── vpc.tf

providers/aws
├── README.md
├── global
│   ├── global.tf
│   └── terraform.tfvars
├── us_east_1_prod
│   ├── terraform.tfvars
│   └── us_east_1_prod.tf
└── us_east_1_staging
    ├── terraform.tfvars
    └── us_east_1_staging.tf

まず、1番上の階層には moduleとprovidersの2つのディレクトリが存在します。

前者はそのままmoduleを、後者は環境ごとのtfvarsやリソース群のmoduleを管理するtfファイルを含みます。

どんなリソースを作成しているか追うためにproduction配下のtfファイルを見ていきます。

production.tfでは、利用するリソース群のmoduleを定義しています。 以下はEC2リソース群を管理するcompute moduleです。

# production.tf
module "compute" {
  source = "../../../modules/aws/compute"

  name               = "${var.name}"
  region             = "${var.region}"
  vpc_id             = "${module.network.vpc_id}"
  vpc_cidr           = "${var.vpc_cidr}"
  key_name           = "${aws_key_pair.site_key.key_name}"
  azs                = "${var.azs}"
  private_subnet_ids = "${module.network.private_subnet_ids}"
  public_subnet_ids  = "${module.network.public_subnet_ids}"
  ~以下略~
}

compute moduleで読み込んでいるcompute.tf内には
各resourceを抽象化したmoduleが定義されています。
このmoduleで渡した変数が最終的にresourceとして使用されます。

# compute.tf
module "haproxy" {
  source = "./haproxy"

  name               = "${var.name}-haproxy"
  vpc_id             = "${var.vpc_id}"
  vpc_cidr           = "${var.vpc_cidr}"
  key_name           = "${var.key_name}"
  subnet_ids         = "${var.public_subnet_ids}"
  atlas_username     = "${var.atlas_username}"
  atlas_environment  = "${var.atlas_environment}"
  atlas_token        = "${var.atlas_token}"
  amis               = "${var.haproxy_amis}"
  nodes              = "${var.haproxy_node_count}"
  instance_type      = "${var.haproxy_instance_type}"
  sub_domain         = "${var.sub_domain}"
  route_zone_id      = "${var.route_zone_id}"
}

順番に見ていったように

ファイル 記述内容
production.tf どういったリソース群を利用しているのか
compute.tf どういうresourceを利用しようとしているのか
haproxy.tf どういう設定でリソースを利用しようとしているのか

が、それぞれわかるようになっており
大きく3階層の構造で ファイル構成が成り立っています。

ここにEC2のインスタンスを追加したければ最下層のmoduleを
新しいリソース群を追加したければ、中段以降のmoduleを追加していくような形です。

所感

ひと通り触ってみましたが

  • 階層の深さ
  • 変数定義の非DRYな感じ

は最後まで辛かったです。

階層に関しては、初見時にコードを追いかけていくのが大変だったので
中段のリソース群をまとめているcompute.tfなどのファイルを減らせないかと考えたのですが
そうするとproduction.tfなどの大元のファイルに各moduleを定義することになり、行数が肥大化してしまい
その環境にどういった種類のリソースが存在するのか、見通しが悪くなってしまうので、今の構成を受け入れるしかないという結論に落ち着きました。

2つ目の変数定義なのですが、各tfファイルに何度も variable hoge { } と書かされるのが本当にしんどいです。

今回は tfvars -> environment.tf -> resource_group.tf -> resource.tf といった具合に計3階層に渡って変数の値を受け渡す必要があったため
同じ変数の定義を3回書いています。

めちゃくちゃ面倒です。
この辺はTerraform側の対応を待つしかないようです。

しかし、それら不便な点を差し引いても Terraformは非常に便利な点が多いため、これからも積極的に利用していこうと思います。