febc技術メモ

Japanese version of http://febc-yamamoto.hatenablog.com

【モダンTerraform】意外と便利!? Miscプロバイダーたち(Templateプロバイダー編)

f:id:febc_yamamoto:20180130182943p:plain

モダンTerraformシリーズです。

今回は前回の続きとしてMiscプロバイダーの中からTemplateプロバイダーについて扱います。

Templateプロバイダーとは

その名の通りテンプレート機能を提供してくれるプロバイダーです。
以下のリソースとデータソースが含まれます。

分類 名称 概要
データソース template_file 単一のファイル(など)用のテンプレート機能
データソース template_cloudinit_config cloud-initのconfigファイル用テンプレート機能
リソース template_dir ディレクトリ内のテンプレートファイルに対し一括処理を行う機能

template_dirだけリソースになっている点が注意ですね。
詳しくは後述しますが、template_dirのみファイルが作成されるという作用があるためです。

それでは各リソース/データソースについて見ていきます。

template_fileデータソース

単一のファイル(など)用のテンプレート機能を提供してくれます。

基本的な使い方

まず次のようなテンプレートを用意しておきます。

#!/bin/bash

echo "CONSUL_ADDRESS = ${consul_address}" > /tmp/iplist

このテンプレートを描画するには以下のようにデータソースを定義します。

data "template_file" "init" {
  # テンプレートとして<モジュールのディレクトリ>/init.tpl を指定
  template = "${file("${path.module}/init.tpl")}"

  # テンプレートに渡す変数の定義
  vars {
    consul_address = "${sakuracloud_server.consul.ipaddress}"
  }
}

あとはこのリソースのrenderedという属性を参照することで描画済み文字列が取得可能です。

# 例: スタートアップスクリプトとして利用する
resource sakuracloud_note "init-script" {
  name     = "init"
  content = "${data.template_file.init.rendered}" # rendered属性から描画済み文字列を参照
}

# 例: プロビジョナーに渡す
resource sakuracloud_server "server" {
  name = "server"

  # 描画した内容をサーバ上の/tmp/init.shに書き込み
  provisioner "file" {
    destination = "/tmp/init.sh"
    content     = "${data.template_file.init.rendered}"
  }
}

変数の指定

テンプレートに渡す変数は先程の例の通りvarsブロックで指定します。

data "template_file" "init" {
  ...

  # テンプレートに渡す変数の定義
  vars {
    consul_address = "${sakuracloud_server.consul.ipaddress}"
  }
}

この例ではconsul_addressというキーを用いていますが、任意のキーを複数指定可能です。

  vars {
    ipaddress   = "192.2.0.1"
    nw_mask_len = 24
  }

注意点としては値の指定にはリスト型/マップ型を指定できないということです。

# !バリデーションエラーとなります!

locals {
  # リスト型変数
  iplist = ["192.2.0.1", "192.2.0.2"]

  # マップ型変数
  rules = {
    allow = "80,443"
    deny  = "all"
  }
}

data "template_file" "init" {
  template = "foobar"

  # テンプレートに渡す変数の定義
  vars {
    target_hosts = "${local.iplist}" # リスト型は指定不可 
    target_rules = "${local.rules}"  # マップ型は指定不可
  }
}

この場合は何らかの形で文字列に変換して渡してあげましょう。

# 文字列にする例

  # テンプレートに渡す変数の定義
  vars {
    target_hosts = "${join(",", local.iplist)}"  # joinで文字列化
    target_rules = "${jsonencode(local.rules)}"  # jsonencodeでJSON化
  }

テンプレートをインラインで指定

テンプレートはファイルとして用意しておかなくてもtfファイルの中にインライン指定可能です。

data "template_file" "init" {
  # テンプレートをインライン指定
  template = "$${consul_address}:1234"

  vars {
    consul_address = "${sakuracloud_server.consul.ipaddress}"
  }
}

この場合、$エスケープして$$として書く必要があることに注意してください。
エスケープを忘れて$ひとつだけにした場合はテンプレート描画前にterraformが参照の解決をしようとしてしまいます。

使い所

基本的には以下のような場合に利用することになると思います。

  • tfファイル中にインラインで書くには長すぎる内容を扱う場合
  • 複数のリソースに展開する場合

ちょっと長い程度の文字列なら以下のようにヒアドキュメントで書くことも可能ですので、状況に応じて使い分けましょう。

resource sakuracloud_server "server" {
  name = "server"

  # 描画した内容をサーバ上の/tmp/init.shに書き込み
  provisioner "file" {
    destination = "/tmp/init.sh"
    # ちょっと長い程度ならヒアドキュメントで指定もあり
    content     = <<EOL

#!/bin/bash

echo "CONSUL_ADDRESS = ${sakuracloud_server.consul.ipaddress}" > /tmp/iplist
EOL
  }
}

template_cloudinit_configデータソース

template_fileデータソースの機能に加え、cloud-initで利用するconfigに特化した機能が追加されたデータソースです。
write-mime-multipartコマンドなどを利用せずともマルチパートなconfigを組み立てることが可能です。

以下のように利用します。

# Render a part using a `template_file`
data "template_file" "script" {
  template = "${file("${path.module}/init.tpl")}"

  vars {
    consul_address = "${aws_instance.consul.private_ip}"
  }
}

# Render a multi-part cloudinit config making use of the part
# above, and other source files
data "template_cloudinit_config" "config" {
  gzip          = true
  base64_encode = true

  # Setup hello world script to be called by the cloud-config
  part {
    filename     = "init.cfg"
    content_type = "text/part-handler"
    content      = "${data.template_file.script.rendered}"
  }

  part {
    content_type = "text/x-shellscript"
    content      = "baz"
  }

  part {
    content_type = "text/x-shellscript"
    content      = "ffbaz"
  }
}

# Start an AWS instance with the cloudinit config as user data
resource "aws_instance" "web" {
  ami           = "ami-d05e75b8"
  instance_type = "t2.micro"
  user_data     = "${data.template_cloudinit_config.config.rendered}"
}

これを描画すると以下のようになります(gzip/base64エンコードはoffの状態)

Content-Type: multipart/mixed; boundary="MIMEBOUNDARY"
MIME-Version: 1.0

--MIMEBOUNDARY
Content-Disposition: attachment; filename="init.cfg"
Content-Transfer-Encoding: 7bit
Content-Type: text/part-handler
Mime-Version: 1.0

#!/bin/bash

echo "CONSUL_ADDRESS = 192.2.0.1" > /tmp/iplist
--MIMEBOUNDARY
Content-Transfer-Encoding: 7bit
Content-Type: text/x-shellscript
Mime-Version: 1.0

baz
--MIMEBOUNDARY
Content-Transfer-Encoding: 7bit
Content-Type: text/x-shellscript
Mime-Version: 1.0

ffbaz
--MIMEBOUNDARY--

template_fileと比較すると以下の属性が追加で指定可能となっています。

  • gzip: 出力をgzip圧縮するか(デフォルトtrue)
  • base64_encode : 出力をBASE64エンコードするか(デフォルトtrue)
  • part : 各パートの内容(指定可能な属性は以下参照)
    • file_name: ファイル名
    • content_type: コンテントタイプ
    • content: ボディ
    • merge_type: マージタイプ(詳細はドキュメントを参照)

user_dataの組み立てに便利ですね。

template_dirリソース

template_fileデータソースのディレクトリ版です。
指定のディレクトリ配下のファイルをテンプレートとみなし、一括して描画してくれます。 実際にファイル(ディレクトリも)が作成されることに注意してください。

使い方は以下の通りです。

resource "template_dir" "config" {
  source_dir      = "${path.cwd}/source"
  destination_dir = "${path.cwd}/dest"

  vars {
    consul_addr = "${var.consul_addr}"
  }
}

この例だとterraform applyを実行するとsourceディレクトリ配下の各ファイルをテンプレートとして読み込み、描画したものをdestディレクトリ配下に同名のファイルとして作成してくれます。

ちなみにtemplate_dirリソースはIDにdestination_dirのコンテンツから作成されたハッシュ値が用いられており、ファイル/ディレクトリの変更やリネーム、削除が行われた場合は次回のapply時に再作成されるようになっています。

template_dirの注意点

前述の通りこのリソースはIDが宛先ディレクトリのコンテンツから作成されたハッシュとなっているため、ステートを複数人で共有していてもterraformコマンドを実行するマシンに宛先ディレクトリ(と描画後のコンテンツ)が存在しない場合はapply時に再作成が行われることとなります。

例えばステートレスなCIでterraform planに差分があったら通知するような仕組みを取っている場合は問題となるケースもありますので注意が必要です。

終わりに

今回はTemplateプロバイダーを紹介しました。
ちょっとしたプロビジョニングをTerraformで行う際の強力な武器となりますので是非使いこなしましょう。

次回はMiscプロバイダーの中からRandomかTLSを扱う予定です。

以上です。Enjoy!!