CLOVER🍀

That was when it all began.

Terraformのモジュールを書いてみる

これは、なにをしたくて書いたもの?

Terraformのモジュールというものを、1度自分で書いてみようかな、と。

練習ですね。

Terraform Module

Terraformのモジュールは、複数のリソースをまとめて抽象化したものです。

Creating Modules - Terraform by HashiCorp

モジュールは、主に以下の3つで構成されます。

  • モジュール自身のリソース定義
  • モジュールを使用するための変数(Variables)
  • モジュールの結果の出力定義(Outputs)

Module structure

モジュールは他のモジュールを呼び出すこともできますが、シンプルに構造を保つことが推奨されているようです。

また、モジュールが単なるリソースのラッパーとなることは、避けた方がよいとされています。モジュールは、リソースよりも
抽象度を高めたものを提供すべきなようです。

When to write a module

リソースのラッパーのモジュールを作るくらいなら、直接リソースを使った方が良いみたいです。

モジュールは、2種類存在します。

  • ルートモジュール
  • ネストされたモジュール

ネストされたモジュールは、modulesディレクトリに配置されたモジュールのことを指します。

ルートモジュールは、ルートディレクトリからモジュールとして構成されたものですね。たとえば、以下のようなAWS上で
Consulクラスタを構成するモジュールです。

GitHub - hashicorp/terraform-aws-consul: A Terraform Module for how to run Consul on AWS using Terraform and Packer

こういう思考だと、特に意識せずにTerraformの構成ファイルを書いた場合は、無名モジュール(?)を作っているような
感じになるんでしょうかね。

で、Configuration Languageを見ると、これは「ルートモジュール」と呼ばれるものになるようです。

Modules - Configuration Language - Terraform by HashiCorp

なるほど。

モジュールには、以下を含めるべきだとされています。

  • README.md
  • ライセンス
  • main.tf、variables.tf、outputs.tf

Standard Module Structure

必要に応じて、ネストされたモジュールも含まれることになります。

また、VariablesとOutputsには説明(description)を記載するべきであり、exampleも用意すべきだとか。

作成したモジュールは、Terraform RegistryまたはPrivate Registryで公開可能です。

Publishing Modules - Terraform by HashiCorp

また、モジュールを使うにあたっては、Registry以外のものからも参照することができます。

Module Sources - Terraform by HashiCorp

モジュールの合成に関するトピックは、こちらを参照。

Module Composition - Terraform by HashiCorp

複数のモジュールの依存関係についての考え方、マルチクラウドに対する考え方などが記載されています。

その他、モジュールを作るにあたって参考にするものは、コミュニティのものなどを見るとよいのでしょう。

terraform-community-modules · GitHub

説明はこれくらいにして、モジュールを使ってみましょう。

環境

今回の環境は、こちら。

$ terraform version
Terraform v0.12.24

また、お題としてはMySQL Providerを使ってモジュールを作ることにします。

Provider: MySQL - Terraform by HashiCorp

対象となるMySQLはバージョン8.0.19、IPアドレス172.17.0.2で動作しており、rootで外部からログイン可能なものとします。

モジュールを作成する

では、さっそくモジュールを作成していきましょう。

以下に従って、

Standard Module Structure

まずはモジュール用のディレクトリを作成して、main.tf、variables.tf、outputs.tfを作成。

$ mkdir -p modules/my_mysql
$ touch modules/my_mysql/{main.tf,variables.tf,outputs.tf}

これ、実質はネストしたモジュールですね。

exampleも一応用意してみますが、今回はとりあえず飛ばします。

$ mkdir -p examples/my_mysql
$ touch examples/my_mysql/main.tf

まずは、main.tfの中身を書いていきます。

お題として、

  • MySQLデータベースを作成する
  • 作成したデータベースを使う、アプリケーション用のユーザーと管理用のユーザーを作成し、権限を与える

ということをやっていきたいと思います。

modules/my_mysql/main.tf

terraform {
  required_version = ">= 0.12.24"

  required_providers {
    mysql = ">= 1.9.0"
  }
}

provider "mysql" {
  endpoint = var.mysql_endpoint
  username = var.mysql_admin_username
  password = var.mysql_admin_password
}

resource "mysql_database" "database" {
  name = var.database_name

  default_character_set = "utf8mb4"
  default_collation = "utf8mb4_ja_0900_as_cs_ks"
}

resource "mysql_user" "admin_user" {
  user = var.database_admin_user_username
  plaintext_password = var.database_admin_user_password
  host = var.database_admin_user_host
}

resource "mysql_grant" "admin_user" {
  user = mysql_user.admin_user.user
  host = mysql_user.admin_user.host
  database = mysql_database.database.name
  privileges = ["ALL"]
}

resource "mysql_user" "application_user" {
  user = var.database_application_user_username
  plaintext_password = var.database_application_user_password
  host = var.database_application_user_host
}

resource "mysql_grant" "application_user" {
  user = mysql_user.application_user.user
  host = mysql_user.application_user.host  
  database = mysql_database.database.name
  privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

Terraformのバージョンおよび、MySQL Providerのバージョンの下限をそれぞれ指定。

terraform {
  required_version = ">= 0.12.24"

  required_providers {
    mysql = ">= 1.9.0"
  }
}

Providerの設定は、Variablesにしています。

provider "mysql" {
  endpoint = var.mysql_endpoint
  username = var.mysql_admin_username
  password = var.mysql_admin_password
}

作成するデータベース。

resource "mysql_database" "database" {
  name = var.database_name

  default_character_set = "utf8mb4"
  default_collation = "utf8mb4_ja_0900_as_cs_ks"
}

管理用、アプリケーション用のユーザーの作成と、権限付与。

resource "mysql_user" "admin_user" {
  user = var.database_admin_user_username
  plaintext_password = var.database_admin_user_password
  host = var.database_admin_user_host
}

resource "mysql_grant" "admin_user" {
  user = mysql_user.admin_user.user
  host = mysql_user.admin_user.host
  database = mysql_database.database.name
  privileges = ["ALL"]
}

resource "mysql_user" "application_user" {
  user = var.database_application_user_username
  plaintext_password = var.database_application_user_password
  host = var.database_application_user_host
}

resource "mysql_grant" "application_user" {
  user = mysql_user.application_user.user
  host = mysql_user.application_user.host  
  database = mysql_database.database.name
  privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

Variablesは、こんな感じで宣言。descriptionを書くべきなのですが、今回は省略…。
modules/my_mysql/variables.tf

variable "mysql_endpoint" {
  default = "localhost:3306"
}

variable "mysql_admin_username" { }

variable "mysql_admin_password" { }

variable "database_name" { }

variable "database_admin_user_username" {
  default = "adminuser"
}

variable "database_admin_user_password" {
  default = "password"
}

variable "database_admin_user_host" {
  default = "%"
}

variable "database_application_user_username" {
  default = "appuser"
}

variable "database_application_user_password" {
  default = "password"
}

variable "database_application_user_host" {
  default = "%"
}

Outputs。 データベース名やエンコーディング、Collation、ユーザー名を出力として定義します。
modules/my_mysql/outputs.tf

output "database_name" {
  value = mysql_database.database.name
}

output "database_default_character_set" {
  value = mysql_database.database.default_character_set
}

output "database_default_collation" {
  value = mysql_database.database.default_collation
}

output "admin_user_username" {
  value = mysql_user.admin_user.user
}

output "application_user_username" {
  value = mysql_user.application_user.user
}

これで、モジュールの作成は完了です。

作成したモジュールを使う

モジュールができたので、作成したモジュールを利用する定義ファイルを書いていきます。

こんな感じで作成。
main.tf

module "my_mysql" {
  source = "./modules/my_mysql"

  ### variable
  mysql_endpoint = "172.17.0.2:3306"

  mysql_admin_username = "root"
  mysql_admin_password = "password"

  database_name = "practice"
}

sourceで、モジュールが存在するディレクトリを指定します。今回は、モジュールのソースの指定方法のうち、Local Pathsを
使っていることになりますね。

Local Paths

あとは、モジュールが要求するVariablesを指定してあげます。こちらは、最低限の指定でいきました。

Outputsでは、このリソース定義を実行した時の出力内容を定義しますが、ここではモジュールのOutputsをそのまま出力にしています。 outputs.tf

output "database_name" {
  value = module.my_mysql.database_name
}

output "database_default_character_set" {
  value = module.my_mysql.database_default_character_set
}

output "database_default_collation" {
  value = module.my_mysql.database_default_collation
}

output "admin_user_username" {
  value = module.my_mysql.admin_user_username
}

output "application_user_username" {
  value = module.my_mysql.application_user_username
}

「module.[モジュール名].[モジュールで定義したOutput Variable名]」で、モジュールの実行結果を利用できます、と。

output "database_name" {
  value = module.my_mysql.database_name
}

では、「terraform init」。

$ terraform init

MySQL Provider 1.9.0がインストールされました。

$ terraform version
Terraform v0.12.24
+ provider.mysql v1.9.0

フォーマットの確認と、バリデーション。

$ terraform fmt -recursive -check
$ terraform validate

Planを見てから

$ terraform plan

apply。

$ terraform apply

実行結果。

module.my_mysql.mysql_database.database: Creating...
module.my_mysql.mysql_user.admin_user: Creating...
module.my_mysql.mysql_user.application_user: Creating...
module.my_mysql.mysql_user.admin_user: Creation complete after 3s [id=adminuser@%]
module.my_mysql.mysql_user.application_user: Creation complete after 3s [id=appuser@%]
module.my_mysql.mysql_database.database: Creation complete after 3s [id=practice]
module.my_mysql.mysql_grant.application_user: Creating...
module.my_mysql.mysql_grant.admin_user: Creating...
module.my_mysql.mysql_grant.application_user: Creation complete after 1s [id=appuser@%:`practice`]
module.my_mysql.mysql_grant.admin_user: Creation complete after 1s [id=adminuser@%:`practice`]

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:

admin_user_username = adminuser
application_user_username = appuser
database_default_character_set = utf8mb4
database_default_collation = utf8mb4_ja_0900_as_cs_ks
database_name = practice

Outputsでは、モジュールの値が出力されていますね。

MySQL側も確認してみましょう。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| practice           |
| sys                |
+--------------------+
5 rows in set (0.25 sec)


mysql> show create database practice;
+----------+------------------------------------------------------------------------------------------------------------------------------------------+
| Database | Create Database                                                                                                                          |
+----------+------------------------------------------------------------------------------------------------------------------------------------------+
| practice | CREATE DATABASE `practice` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_ja_0900_as_cs_ks */ /*!80016 DEFAULT ENCRYPTION='N' */ |
+----------+------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)


mysql> select host, user from mysql.user where user in ('appuser', 'adminuser');
+------+-----------+
| host | user      |
+------+-----------+
| %    | adminuser |
| %    | appuser   |
+------+-----------+
2 rows in set (0.00 sec)


mysql> show grants for adminuser@'%';
+---------------------------------------------------------+
| Grants for adminuser@%                                  |
+---------------------------------------------------------+
| GRANT USAGE ON *.* TO `adminuser`@`%`                   |
| GRANT ALL PRIVILEGES ON `practice`.* TO `adminuser`@`%` |
+---------------------------------------------------------+
2 rows in set (0.00 sec)


mysql> show grants for appuser@'%';
+-----------------------------------------------------------------------+
| Grants for appuser@%                                                  |
+-----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `appuser`@`%`                                   |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `practice`.* TO `appuser`@`%` |
+-----------------------------------------------------------------------+
2 rows in set (0.00 sec)

OKそうですね。

モジュールに渡す値をVariableにしてみる

先ほどは、モジュールを使っているところで直接Variableの値を指定していましたが、できればこちらも変数にしたいところです。
main.tf

module "my_mysql" {
  source = "./modules/my_mysql"

  ### variable
  mysql_endpoint = "172.17.0.2:3306"

  mysql_admin_username = "root"
  mysql_admin_password = "password"

  database_name = "practice"
}

というわけで、そのように変更してみましょう。

先ほどのリソースは、いったん破棄しておきます。

$ terraform destroy

モジュールに値を渡すために、利用側にもVariableの宣言が必要になります。
variables.tf

variable "mysql_endpoint" { }
variable "mysql_admin_username" { }
variable "mysql_admin_password" { }
variable "mysql_database_name" { }

もう、最低限に絞りました。

値は、「terraform.tfvars」で定義。
terraform.tfvars

mysql_endpoint = "172.17.0.2:3306"
mysql_admin_username = "root"
mysql_admin_password = "password"
mysql_database_name = "practice"

Planを見て、apply。

$ terraform plan
$ terraform apply

結果自体は、先ほどと同じなので割愛します。

まとめ

今回、初めてTerraformのモジュールを試してみました。

モジュールのVariableへの値の指定の方法、モジュールの利用側もVariableにしたい場合は?というところでやや混乱したり
したのですが、なんとか理解は進んだかなと思います。

あとは、もうちょっと複雑な例とかを見て慣れていきたいところですね。