Terraform for さくらのクラウド スタートガイド (第二回) ~便利なビルトイン機能~

前回に引き続き、連載第2回である今回はTerraformにビルトインされている便利な機能を紹介していきます。

  • 複数リソースの利用方法
  • リソースの依存関係
  • プロビジョニング
  • 変数
  • アウトプット

いずれの機能もTerraformを使いこなす上で重要な役割を持っていますので、着実に押さえておきましょう。

複数のリソース、リソース間の依存関係

前回は単一のEC2インスタンスを構成する例を取り上げましたが、実環境においては複数のリソースを扱うことが多いと思います。
ここではTerraformにおいて複数のリソースを扱う方法、リソースから他のリソースの情報を参照する方法について取り上げます。
また、複数のリソースを扱うことで、リソースとリソースの間に依存関係が生じることがあります。
Terraformがリソース間の依存関係をどう扱うかもここで見ていきます。

複数のリソース

Terraformにて複数のリソースを扱いたい場合、単にtfファイル内にそれぞれのリソースの定義を記載すればOKです。
それでは前回の単一のEC2インスタンスのみの構成にElastic IP(固定グローバルIPアドレス)リソースを追加してみます。

参考:AWS Elastic IP

前回のtfファイルを以下のように変更(追記)してみましょう。

# AWSプロバイダの設定
provider "aws" {
  access_key = "ACCESS_KEY_HERE"
  secret_key = "SECRET_KEY_HERE"
  region = "ap-northeast-1"
}

# EC2インスタンス
resource "aws_instance" "example" {
  ami = "ami-be4a24d9"
  instance_type = "t2.micro"
}

# 今回追加分(Elastic IP)
resource "aws_eip" "ip" {
    instance = "${aws_instance.example.id}"
}

今回はEIPの分のリソース定義が追加となっています。
リソースタイプとしてaws_eip、リソース名としてipを指定しています。
参考:aws_eipドキュメント

aws_eipリソースではパラメータにinstanceを指定しています。
ここにはEIPを紐づけるEC2インスタンスリソースのIDを指定します。

instanceの値に指定している"${aws_instance.example.id}"という値がポイントです。

Terraformでは${}という記法を用いることでリソースの属性を参照したり、変数を展開したり、組み込みの関数を呼び出したりすることが可能です。

この例での"${aws_instance.example.id}"という指定は、

- (他所で定義されている)リソースタイプがaws_instanceのリソースのうち、
- リソース名がexampleのリソースの、
- id属性を参照する

という内容になっています。

リソースの属性を参照する場合は${リソースタイプ.リソース名.属性名}という形で指定します。

この記法についての詳細は以下のドキュメントを参照ください。
参考:Terraformドキュメント INTERPOLATION SYNTAX

リソース間の依存関係

早速先ほどのtfファイルを使ってterraform applyを実行してみましょう。
この時、EC2インスタンス -> EIPの順でリソースの作成が行われるはずです。

これは、今回のtfファイルではEIPの属性(instance)にてEC2インスタンスの値を利用しているために「EIPがEC2インスタンスに依存している」とTerraformが判断したためです。

Terraformはtfファイルを解析し、依存関係のあるリソースについては直列(依存される側を作成してから依存する側を作成)に、異存関係のないリソースについては並列にリソース構築を行うようになっています。

試しにEIPリソースのinstance属性をコメントアウトしてからterraform applyを実行すると、EC2インスタンスとEIPの間には異存関係がなくなるため、並列でリソース作成が行われます。

依存関係の確認

Terraformにはリソース間の依存関係をグラフィカルに確認できるterraform graphというコマンドがあります。
これはリソース間の依存関係をGraphvizで描画できるようにdot言語(プレーンテキスト)で出力するためのコマンドで、出力されたテキストをGraphvizに与えることで依存関係を画像で確認することができます。

以下の例は実際に今回のtfファイルでterraform graphを実行しGraphvizで画像を生成したものです。

例1:EIPがEC2に依存している場合

例2:EIPのinstance属性をコメントアウトした場合(依存関係なし)

 

プロビジョニング

続いてはプロビジョニング機能についてです。
Terraformでは、作成したリソースに対してファイルのアップロードや任意のソフトウェアのインストールなどの初期設定を行うためにプロビジョニング機能がサポートされています。

プロビジョニングの定義

以下の例ではEC2インスタンスに対して"local-exec"プロビジョナを定義しています。

resource "aws_instance" "example" {
  ami = "ami-be4a24d9"
  instance_type = "t2.micro"
  # プロビジョナの定義
  provisioner "local-exec" {
    command = "echo ${aws_instance.example.public_ip} > ip_address.txt"
  }
}

"local-exec"プロビジョナは、terraformを実行するローカルマシン上で任意のコマンドを実行するためのものです。

[TIPS]
サポートされているプロビジョナとしては、実際にEC2インスタンスなどに接続してサーバー上で任意のコマンドを実行する"remote-exec"やファイルのアップロードを行う"file"などがあります。これらは次回以降の連載で実際に利用する際に改めて解説します。

プロビジョニングの実行

プロビジョニングはterraform applyを実行することで行います。

ただし、プロビジョニングはリソースの作成時のみ行われます。
このため、すでにここまでの例でEC2インスタンスを作成済みの場合は一度terraform destroyを実行してからterraform applyを実行してください。

うまくいけば手元のマシン上にip_address.txtが作成されているはずです。

利用できるプロビジョナや設定可能な値などは以下のドキュメントを参照ください。
参考:Terraform ドキュメント Provisioners

変数

続いてtfファイルで変数を扱う方法について見ていきます。

変数の定義

ここまでの例でのtfファイルではAWSのアクセスキーを直接記載していました。まずはこれらを変数として切り出してみましょう。

tfファイルと同じディレクトリ内に"variables.tf"という名前のファイルを以下の内容で作成します。(ファイル名は任意でかまいません)

variable "access_key" {}
variable "secret_key" {}
variable "region" {
  default = "ap-northeast-1"
}

ここでは3つの変数が定義されており、最初の2つは{}という空のブロックを持っています。最後の1つはデフォルト値を持っています。
デフォルト値が指定されていない最初の2つについては、terraformコマンド実行時に(後述する)いくつかの方法で値を指定する必要があります。

変数の利用

続いて定義した変数を利用するようにtfファイルを変更します。

プロバイダ定義部分を以下のように変更します。

provider "aws" {
  access_key = "${var.access_key}"
  secret_key = "${var.secret_key}"
  region = "${var.region}"
}

${}という記法がまた出てきました。
先ほどは他のリソースの値を利用するために${リソースタイプ.リソース名.属性名}という形式でしたが、変数を参照する場合は${var.変数名}という形式で指定します。

変数への値の設定

定義した変数に値を設定します。設定は以下のような方法で行います。

  • コマンドライン上で対話的に設定
  • コマンドライン上で"-var"フラグ指定
  • tfvarsファイルで設定
  • 環境変数で設定

コマンドライン上で対話的に設定

他の方法で値が与えられなかった場合のデフォルトの挙動です。
terraformコマンド実行時にキーボードなどから直接値を入力します。

コマンドライン上で"-var"フラグ指定

terraformコマンド実行時に"-var"フラグにて設定する方法です。

$ terraform plan \
  -var 'access_key=foo' \
  -var 'secret_key=bar'
...

この方法の場合、terraformコマンドを実行する都度"-var"フラグを指定する必要があります。
また、コマンドラインの履歴に指定した値が残ってしまうことにも注意が必要です。

tfvarsファイルで設定

tfファイルを同じディレクトリ内に"terraform.tfvars"という名前で以下のファイルを作成します。

access_key = "foo"
secret_key = "bar"

この"terraform.tfvars"というファイル名は特別扱いされ、tfファイルと同じディレクトリにこの名前のファイルがあると自動的に読み込まれるようになっています。

もし別のファイル名にしたい場合や別のディレクトリにファイルを置きたい場合はterraformコマンド実行時に"-var-file"フラグを指定することができます。

$ terraform plan \
  -var-file="secret.tfvars"

なお、"-var-file"フラグは以下のように複数指定することも可能です。

terraform plan \
  -var-file="secret.tfvars" \
  -var-file="production.tfvars"

重複した項目がある場合は後から指定したファイルの値が優先されますので、本番用の値だけ別のファイルに切り出しておくといった使い方も可能です。

環境変数で設定

"TF_VAR_変数名"という環境変数を設定しておくことで値を指定することも可能です。"access_key"という変数に値を設定する場合は"TF_VAR_access_key"という環境変数を設定することになります。

なお、環境変数経由で設定できるのは文字列型の変数のみです。
後述するMap型変数やList型変数は環境変数では設定できません。

[TIPS]
各プロバイダ/リソースの実装次第ですが、terraformの変数を利用せずに直接環境変数を利用してくれる属性もあります。
例えばAWSプロバイダの場合、"AWS_ACCESS_KEY_ID"や"AWS_SECRET_ACCESS_KEY"という環境変数を設定しておくことでaccess_keyやsecret_keyの設定が可能です。対応状況はプロバイダ/リソースごとにまちまちですので、利用する際はそれぞれのドキュメントを参照してみてください。

変数のデータ型

ここまでで扱った変数は文字列型と呼ばれる単純な文字列のデータ型でした。
TerraformではこのほかにList型変数とMap型変数があります。
それぞれ以下のように定義/利用します。

List型変数

List型変数は以下のように定義します。

# デフォルト値にブラケット[]を指定することで暗黙的に定義
variable "cidrs" { default = [] }

# typeを指定することで明示的に定義することも可能
variable "cidrs" { type = "list" }

値の設定は以下のように行います。

cidrs = [ "10.0.0.0/16", "10.1.0.0/16" ]

値を利用する場合は以下のようにします。

# インデックスで指定(インデックスは0開始)
${var.cidrs[0]}

# element関数で指定
${element(var.cidrs , 0)}

Map型変数

Map型変数はキーと値を持ちます。以下のように定義します。

# デフォルト値を[キー = 値]の形で与えることで暗黙的に定義
variable "amis" {
  default = {
    us-east-1 = "ami-13be557e"
    us-west-2 = "ami-06b94666"
    ap-northeast-1 = "ami-0c11b26d"
  }
}

# typeを指定することで明示的に定義
variable "amis" {
  type = "map"
}

Map型変数を利用する場合は以下のようにします。

# キーを直接指定
resource "aws_instance" "example" {
  ami           = "${var.amis[var.resion]}"
  # キーを文字列で指定することもできる
  # ami         = "${var.amis["ap-northeast-1"]}"
  instance_type = "t2.micro"
}
# lookup関数で指定
resource "aws_instance" "example" {
  ami           = "${lookup(var.amis, var.region)}"
  instance_type = "t2.micro"
}

アウトプット

Terraformが管理しているリソースの情報はterraform showコマンドで確認することができます。

$ terraform show
aws_eip.ip:
  id = xxxxxxxx-xxxxxxxx
  association_id = xxxxxxxx-xxxxxxxx
  domain = vpc
  instance = i-xxxxxxxx10xxxxxxxx9
  network_interface = eni-xxxxxxxx
  private_ip = nnn.nnn.nnn.nnn
  public_ip = nnn.nnn.nn.nn
  vpc = true
aws_instance.example:
  id = i-xxxxxxxxxxxxxxxxx
  ami = ami-xxxxxxxx
  associate_public_ip_address = true
  availability_zone = ap-northeast-1a
  disable_api_termination = false
  ebs_block_device.# = 0
  ebs_optimized = false
  ephemeral_block_device.# = 0
  iam_instance_profile = 
  instance_state = running
  instance_type = t2.micro
  key_name = 
  monitoring = false
  network_interface_id = eni-xxxxxxxx
  private_dns = ip-nnn-nnn-nnn-nnn.ap-northeast-1.compute.internal
  private_ip = nnn.nnn.nnn.nnn
  public_dns = ec2-nnn-nnn-nnn-nnn.ap-northeast-1.compute.amazonaws.com
  public_ip = nnn.nnn.nnn.nnn
  root_block_device.# = 1
  root_block_device.0.delete_on_termination = true
  root_block_device.0.iops = 100
  root_block_device.0.volume_size = 8
  root_block_device.0.volume_type = gp2
  security_groups.# = 0
  source_dest_check = true
  subnet_id = subnet-xxxxxxxx
  tags.% = 0
  tenancy = default
  vpc_security_group_ids.# = 1
  vpc_security_group_ids.nnnnnnnnn = sg-xxxxxxxx

terraform showコマンドではterraformが管理している各リソースごとに全属性が表示されます。
この例ではリソースは2つしかないですが、実運用環境にて複雑なインフラ構築を行う場合、リソース/属性が数百〜数千という非常に大きな数に膨れ上がることがあります。

この中で関心のある項目だけ抜き出して確認できるように「アウトプット」という仕組みが用意されています。

アウトプットは以下のように定義します。

output "ip" {
    value = "${aws_eip.ip.public_ip}"
}

アウトプットが定義された状態でterraform applyを実行することでアウトプット用の値が生成されます。

生成されたアウトプットはterraform outputコマンドで表示できます。

# 生成されたアウトプットをすべて表示
$ terraform output

# 名称を指定することで特定のアウトプットだけ表示することも可能
$ terraform output ip

今回はTerraformが持つ以下のような様々な便利機能について見てきました。

  • 複数リソースの利用方法
  • リソースの依存関係
  • プロビジョニング
  • 変数
  • アウトプット

いずれもTerraformを利用する上で欠かせない重要な機能ですので、実際に手を動かしてみて確実に身につけておくのがオススメです。

次回からはいよいよTerraformからさくらのクラウド上にインフラを構築していきます。お楽しみに!