最近、自分のチームでTerraformを使ってAWSのオーケストレーションをやろうという話になった。
サーバー台数も10~20台程度、今まではAWSコンソールをポチポチしてインスタンスを用意していたが
サービス規模の拡大に伴って、手動オペレーションによるミスを防いだり、インフラ説明コストなどを省くこと、インフラ整理の意味合いで導入している最中。
Terraformに触るのは初めてだったので、ドキュメントを読みつつ基本的な機能について勉強しつつ
どのようなファイル構成で管理をするのがいいのか、経験者の同僚にアドバイスをもらいにいったところ
Hashcorpが公開しているベストプラクティスのリポジトリを教えてくれた。
ところがこの構成、けっこう複雑で初心者がいきなり読み始めるのは辛かったので
復習も兼ねてベストプラクティスを読み解いていく。
理解するために必要な要素
- 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は非常に便利な点が多いため、これからも積極的に利用していこうと思います。