【さくらのクラウド】TerraformでWindowsの展開を自動化する

はじめに
今回はTerraform for さくらのクラウドを用いてWindows Serverの展開を自動化する方法をご紹介します。
さくらのクラウドではWindows Serverのセットアップを行うにはコントロールパネルからコンソール接続を行う必要がありましたが、
この方法を使えばterraformコマンドだけで一気に展開可能です。
なお、今回の方法は残念ながらリソースマネージャーには非対応です。
terraformコマンドを直接使う方向けとなっています。
背景
まずはさくらのクラウドでのWindows Serverプランの扱いについて確認しておきます。
さくらのクラウドのWindows Serverプラン
さくらのクラウドではWindows Serverプランが提供されています。
Datacenter EditionやRDS、さらにはOffice付きのものやSQLServerインストール済みのものまで様々なプランがあります。
さくらのクラウドのWindows Serverプランの仕様
便利なWindows Serverプランなのですが、その他のLinux系OSなどとは異なる仕様がいくつかあります。
例えばディスクサイズは100GB以上が要求されることなどがあります。
その中でも構築の自動化の際に大きく関わってくる仕様として「ディスクの修正が一部項目のみ対応」というものがあります。
「ディスクの修正」とは
さくらのクラウドでは各サーバ固有の設定(グローバルIPアドレスやホスト名、管理者パスワードなど)はディスクの修正という機能で各サーバ(のディスク)に設定されます。
SSH用の公開鍵の登録やスタートアップスクリプトなどもこのディスクの修正で反映できるようになっています。
設定できる項目は以下の通りです。
しかしWindows Serverプランではこのディスクの修正機能が一部のみしか利用できないという仕様になっています。
Windows Serverプランでのディスクの修正機能
Windows Serverプランの場合、ディスクの修正機能で設定可能な項目は以下のみとなっています。
- IPアドレス、ネットマスク、デフォルトゲートウェイ
また、パブリックアーカイブからの新規作成時のみディスク修正が行えるという制限もあります。
つまり、Windows Serverプランを利用する場合はサーバ作成後にコントロールパネルからサーバのコンソールを利用して管理者パスワードの設定などの初期設定を手作業で行う必要があります。
(一応VNCも使えます。usacloudからだとusacloud server vnc <your-server-name>でOK)
これが数台程度であれば良いですが、数十〜数百台となってくるとブラウザから手作業で構築するのは非常に大変な作業となります。
そこで何か自動化する手段は無いか、、、ということで自動化する方法を考えました。
Windows展開の自動化といえばsysprep+応答ファイル
Windows環境構築の自動化といえばsysprep+応答ファイルという方法がありますね。
実はさくらのクラウドで提供されているWindowsパブリックアーカイブはISOイメージからのインストール直後の状態ではなく、NIC用のVirtIOドライバなどがインストールされた状態となっており、起動直後はわずかな入力を行うだけで直ぐに利用できる状態となります。 (おそらくさくらのクラウド側であらかじめある程度構築後にsysprepされた状態になっているのでしょう)
なので、応答ファイルもごく一部の項目のみ作成すればOKとなっています。 具体的には最低限以下の設定をすれば直ぐにリモートデスクトップ接続ができる状態に持っていけます。
これにchocolateyなどをインストールするスクリプトを仕込めば一気に環境構築できそうです。
応答ファイルはサーバにどうやって渡す? -> ISOイメージを使う
応答ファイルを作成しておけば構築の自動化はできそうですが、サーバ作成時にどうやって応答ファイルを渡せば良いでしょうか? いくつか方法はありますが、さくらのクラウドではISOイメージのアップロードに対応していますのでこれを利用してあげれば良いでしょう。
以下のドキュメントにも記載がありますが、応答ファイルをUnattend.xmlという名前でリムーバブルメディアのドライブのルートにおいておけば初回起動時に参照してくれます。
なので、応答ファイルを格納したISOイメージを作成し、サーバ作成時にISOイメージを挿入した状態で起動すれば良さそうですね。
Terraform for さくらのクラウドならISOイメージも一発構築
Terraform for さくらのクラウドはISOイメージの作成もサポートしています。
ISOイメージの作成処理はGo言語のみで実装していますのでTerraformを実行するのがWindowsの場合でも問題なくISOイメージの作成が行えます。
また、ISOイメージに格納する応答ファイルUnattend.xmlについてはTerraformの変数やテンプレート機能を活用すれば作成できますね。
ということで実際にTerraform for さくらのクラウドで自動化を行なってみます。
構築
Windows Server向け環境構築用のtfファイル
tfファイルは以下のようになります。
### 概要 # Windows Serverの初期設定を行うテンプレート # # このテンプレートはWindows Serverの管理者パスワードの設定や任意のスクリプトの実行を行うテンプレートです。 # 初期設定は応答ファイル(Unattend.xml)を格納したISOイメージをサーバにアタッチすることで行なっています。 # ### 変数定義 # サーバ管理者(Administrator)のパスワード variable password {} locals { #********************************************* # プロビジョニング #********************************************* # サーバ上で実行したいコマンドを指定 run_commands = [ # パッケージマネージャー"chocolatey"のインストール "powershell -Command \"Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))\"", # chocolateyでChromeをインストール "C:\\ProgramData\\chocolatey\\bin\\choco.exe install -y googlechrome", ] #********************************************* # サーバ/ディスク #********************************************* # 利用するアーカイブ種別: 指定できる値についてはTerraform for さくらのクラウドのマニュアルを参照してください os_type = "windows2016" # サーバ名 server_name = "windows-server" # ホスト名(コンピューター名) host_name = "windows-server" # サーバ コア数 server_core = 2 # サーバ メモリサイズ(GB) server_memory = 4 # ディスクサイズ(100GB以上) disk_size = 100 } ### 構成構築 # 利用するアーカイブ data sakuracloud_archive "windows" { os_type = "${local.os_type}" } # ディスク resource "sakuracloud_disk" "disk" { name = "${local.server_name}" size = "${local.disk_size}" source_archive_id = "${data.sakuracloud_archive.windows.id}" lifecycle { ignore_changes = ["source_archive_id"] } } # サーバ resource "sakuracloud_server" "server" { name = "${local.server_name}" disks = ["${sakuracloud_disk.disk.id}"] core = "${local.server_core}" memory = "${local.server_memory}" cdrom_id = "${sakuracloud_cdrom.settings.id}" } # ISOイメージ(応答ファイル用) resource sakuracloud_cdrom "settings" { name = "${local.server_name}" # 単一ファイルを内包するISOイメージを作成する content = "${local.unattend_body}" content_file_name = "Unattend.xml" } # 応答ファイルの組み立て locals { command_elm_format = <<EOL <SynchronousCommand wcm:action="add"> <Order>$${index}</Order> <CommandLine>$${body}</CommandLine> </SynchronousCommand> EOL commands_format = <<EOL <FirstLogonCommands> %s </FirstLogonCommands> EOL commands_body = "${length(local.run_commands) > 0 ? format(local.commands_format, join("", data.template_file.run_commands.*.rendered)) : "" }" } data template_file "run_commands" { count = "${length(local.run_commands)}" template = "${local.command_elm_format}" vars = { index = "${count.index + 1}" body = "${local.run_commands[count.index]}" } } locals { unattend_body = <<EOL <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend"> <settings pass="oobeSystem"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OOBE> <HideEULAPage>true</HideEULAPage> </OOBE> <UserAccounts> <AdministratorPassword> <Value>${var.password}</Value> <PlainText>true</PlainText> </AdministratorPassword> </UserAccounts> <AutoLogon> <Password> <Value>${var.password}</Value> <PlainText>true</PlainText> </Password> <Enabled>true</Enabled> <LogonCount>1</LogonCount> <Username>Administrator</Username> </AutoLogon> ${local.commands_body} </component> <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>0411:E0010411</InputLocale> <SystemLocale>ja-JP</SystemLocale> <UILanguage>ja-JP</UILanguage> <UILanguageFallback>ja-JP</UILanguageFallback> <UserLocale>ja-JP</UserLocale> </component> </settings> <settings pass="specialize"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ComputerName>${local.host_name}</ComputerName> </component> </settings> </unattend> EOL }
あとはterraform applyを実行すればプロビジョニング済みのWindows Serverの出来上がりです。
各要素の解説
一応各要素について解説しておきます。
ISOイメージと応答ファイルの作成
ISOイメージは以下のように定義することで応答ファイルを格納したイメージの作成が可能です。
# ISOイメージ(応答ファイル用) resource sakuracloud_cdrom "settings" { name = "${local.server_name}" # 単一ファイルを内包するISOイメージを作成する場合 content = "${local.unattend_body}" content_file_name = "Unattend.xml" }
ポイントはcontentとcontent_file_nameの部分ですね。
Terraform for さくらのクラウドのISOイメージリソースはすでに存在するISOイメージのアップロードも可能ですが、文字列を指定して単一のファイルを内包するISOイメージの作成が行えるようになっています。
contentに作成するファイルの内容を、content_file_nameに作成するファイル名を指定すればOKです。
(なお、content_file_nameに指定した値はボリュームラベルとしても利用されます。)
ここではファイル名にUnattend.xmlを指定してますね。
contentには応答ファイルのXMLを組み立てた結果を格納した変数が指定されています。
応答ファイルには以下の内容を記載しています。
- EULA(ソフトウェア仕様許諾)ヘの同意
- ロケール/キーボードの設定
- Administratorのパスワードの設定
- コンピューター名の変更
- 指定のスクリプトを初回ログオン時に実行
- サーバ作成後に1回だけ自動ログイン
実行したいスクリプトはLocalValuesのrun_commandsに指定しています。
この例ではChocolateyをインストールし、chocolateyを用いてChromeをインストールしています。
好みに応じてこの辺りを編集してみてください。
サーバへのISOイメージの挿入
これはサーバリソースの定義にISOイメージのIDを指定するだけでOKです。 例では以下のように指定しています。
# サーバ resource "sakuracloud_server" "server" { # ... cdrom_id = "${sakuracloud_cdrom.settings.id}" }
注意点
ISOイメージは安価とはいえお金がかかります(現時点では月額108円)。
サーバ構築後はISOイメージは利用しませんので削除してしまってもOKです。
削除する場合はtfファイルからISOイメージ関連の記述をコメントアウト & applyなどしておきましょう。
応用: WinRMを有効化してAnsibleやTerraformのプロビジョナーを利用する
簡単なスクリプト程度なら先程のrun_commandsに実行したいコマンドを書いていけばいいですが、
少し複雑なことをやる場合はやはりAnsibleなどのツールを利用するのが楽だと思います。
そのためにはWinRMを有効にする必要があります。
この場合、以下のようにtfファイルに記載すれば良いでしょう。
#********************************************* # プロビジョニング #********************************************* # サーバ上で実行したいコマンドを指定 run_commands = [ #***************************************************************** # WinRMを有効化(5985:httpと5986:httpsポートが解放される) #***************************************************************** "powershell -Command \"Invoke-WebRequest -Uri https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1 -OutFile ConfigureRemotingForAnsible.ps1\"", "powershell -Command \"powershell ConfigureRemotingForAnsible.ps1\"", "powershell -Command \"Remove-Item -path ConfigureRemotingForAnsible.ps1 -force\"", ]
この記述をしておけば、Ansibleが提供しているスクリプト(powershell)を利用してWinRMを有効化できます。
WinRMさえ有効になってしまえば以下のようにTerraformのプロビジョナーも利用できます。
# サーバ
resource "sakuracloud_server" "server" {
name = "${local.server_name}"
disks = ["${sakuracloud_disk.disk.id}"]
core = "${local.server_core}"
memory = "${local.server_memory}"
cdrom_id = "${sakuracloud_cdrom.settings.id}"
# > WinRMを有効化しておけばプロビジョナーも利用可能
provisioner "remote-exec" {
# 接続情報の指定
connection {
type = "winrm"
host = "${sakuracloud_server.server.ipaddress}"
port = 5986
https = true
insecure = true
user = "Administrator"
password = "${var.password}"
}
# サーバ上で実行するコマンド
inline = [
"echo foobar",
]
}
}
Ansibleを利用する場合はfileリソースなどで以下のようにインベントリファイルを作成しておくと楽でしょう。
resource "local_file" "foo" {
filename = "${path.module}/hosts"
content = <<EOL
[windows]
${sakuracloud_server.server.ipaddress}
[windows:vars]
ansible_user=Administrator
ansible_password=${var.password}
ansible_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
EOL
}
あとはansible -i hosts windows -m win_pingみたいに実行できるはずです。
(これをlocal_execプロビジョナーで実行してもいいですね)
終わりに
これでWindows環境の展開が楽になりますね!! 是非ご活用ください!!!
以上です。
【モダンTerraform】意外と便利!? Miscプロバイダーたち(Templateプロバイダー編)

今回は前回の続きとして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!!
【モダンTerraform】意外と便利!? Miscプロバイダーたち(概要編&Nullプロバイダー編)

今回はTerraformのプロバイダーのうち、最近充実してきているMiscプロバイダーについてご紹介します。
Miscプロバイダーってなに?
最近Terraformのプロダイバーが増えてきたため、ドキュメント上プロダイバーの分類が行われました。
現在は以下のような分類がなされています。
| 分類 | 概要 |
|---|---|
| Major Cloud | いわゆる3大クラウド(AWS/Azure/GCP)+α |
| Cloud | ↑以外のパブリック/プライベートクラウドやHerokuなどのPaaSなど |
| Infrastructure Software | KubernetesやRancherなどの基盤系ソフトウェア(ConsulやRabbitMQなども) |
| Network | CDNやDNSなどネットワーク系 |
| VCS | バージョン管理システム系(Bitbucket/GitHub/GitLab) |
| Monitor & System Management | モニタリングやシステム管理系(OpsGenieやPagerDutyなども) |
| Database | データベース(InfluxDB/MySQL/PostgreSQL) |
| Misc. | その他諸々 |
| Community | サードパーティ製プロバイダー |
Misc.プロダイバーはこの中でも分類しきれなかったその他のものですね。
Miscプロバイダーにはどんなものが含まれてるの?
現在は以下のプロバイダーが含まれています。
| 分類 | 概要 |
|---|---|
| Archive | ファイルやディレクトリのアーカイブを行う(現在はzipのみ対応) |
| Cobbler | Linuxのネットワークインストール環境を構築/管理してくれるツールCobbler用プロバイダー |
| External | 任意のプログラムをData Sourceとして利用する |
| Ignition | ContainerLinux(CoreOS)で利用されているプロビジョニングユーティリティIgnition用の構成ファイルの出力 |
| Local | ローカルファイルの作成/参照を行う |
| Null | 特殊な空リソース |
| Random | ランダム値の生成を行う |
| Template | テンプレート機能の提供 |
| TLS | キーペアの生成やCSRの生成、自己署名証明書の発行など |
この中でも特によく使われるのは以下2つだと思います。
今回はこの中からNullプロバイダーについて紹介し、Templateプロバイダーやそれ以外のプロバイダーについては次回以降に紹介します。
Nullプロバイダー
NullプロバイダーはTerraformのリソースライフサイクルにおいて何も処理を行わないという特殊なリソースを提供します。
と言われてもわからないですよね。
これは通常のTerraformのリソースだけでは実現が難しいいくつかのケースに対応するためのものです。
Terraformは待ち合わせ処理が苦手
Terraformではtfファイルで定義されているリソース間の依存関係を検出し、自動的に構築順序を制御してくれます。
例えば以下のtfファイルの場合、リソースの構築順序はどうなるでしょうか?
# ディスクの定義
resource sakuracloud_disk "disk" {
name = "foobar"
}
# サーバの定義
resource sakuracloud_server "server" {
name = "foobar"
# 先程定義したディスクのIDを参照
disks = ["${sakuracloud_disk.disk.id}"]
}
この場合、sakuracloud_server.serverリソースはsakuracloud_disk.diskリソースのIDを参照しているため、
sakuracloud_server.serverリソースがsakuracloud_disk.diskに依存している
状態となります。
Terraformはサーバを作成するにはディスクが必要という状態を検出し、
- 1) ディスク
- 2) サーバ
の順に作成してくれます。
(なお、依存関係はterraform graphコマンドで以下例のように表示できるGraphViz形式でグラフを出力してくれます)

では以下の場合はどうなるでしょうか?
# ディスクの定義(1本目)
resource sakuracloud_disk "disk01" {
name = "disk01"
}
# ディスクの定義(2本目)
resource sakuracloud_disk "disk02" {
name = "disk02"
}
# サーバの定義(1台目)
resource sakuracloud_server "server01" {
name = "server01"
# 先程定義したディスクのIDを参照
disks = ["${sakuracloud_disk.disk01.id}"]
}
# サーバの定義(2台目)
resource sakuracloud_server "server02" {
name = "server02"
# 先程定義したディスクのIDを参照
disks = ["${sakuracloud_disk.disk02.id}"]
}
先程の例にディスク/サーバを追加しそれぞれ2つずつリソースを作成する例となっています。
依存関係は以下のようになります。

この場合、ディスク -> サーバの組み合わせの作成順序はこれまで通りですが、2つのディスク/サーバの作成順序はどうなるのでしょうか?
この辺りはTerraformの賢いところで、互いに関連しない(依存しない)リソースについては並列で処理が行われます。
先程の図の①の2つのディスクについては互いに関連しないので並列で作成が行われます。
しかし、中には並列で作成されると困る場面もあります。
例えばクラスタの構築などでは、先にマスターとなるノードをプロビジョニングしてからスレーブ側のノードをプロビジョニングする必要がある場合があります。
並列でサーバ作成されるとこの順序を守ることができません。
また、全てのノードがクラスタに参加した後でプロビジョニングを別途実行したいというような場合もあるでしょう。
そこでTerraformではこの問題を解決するいくつかの方法を提供しており、そのうちの一つがNullリソースです。
以下の例では2台のサーバの作成が完了してからプロビジョニングを行う例となっています。
2台のサーバの作成後というタイミング指定のためにNullリソースを利用しています。
# ...ディスクやサーバの定義(省略)...
# 2台のサーバ作成後に実行するプロビジョニング
locals {
server_ids = ["${sakuracloud_server.server01.id}", "${sakuracloud_server.server02.id}"]
}
resource null_resource "provisioning" {
triggers = {
depends = "${join(",", local.server_ids)}" # サーバ01と02のIDを指定
}
provisioner "local-exec" {
command = "run_initialize_cluster_playbook.sh"
}
}
この例の場合の依存関係は以下のようになります。

Nullリソースのtriggersに(localsを通じて)サーバ01/02のIDを指定することで依存関係を示しています。
依存される側が先に作成されますので、この例では結果的にサーバ2台の作成後にNullリソースに設定されたプロビジョニング処理が実行されることになります。
おまけ: 別解としてdepends_on
今回みたいに単純な例ならNullリソースでなくてもMeta-Parametersの中のdepends_onを利用することでも順序の制御が可能です。
各リソースにdepends_onパラメータを指定することで依存関係が指定できますので、これを利用して順序の制御を行います。
# サーバの定義(2台目)
resource sakuracloud_server "server02" {
# ...
# 1台目のサーバに依存することを明示
# (結果的に1台目のサーバ作成後に2台目の作成が行われる)
depends_on = ["sakuracloud_server.server01"]
provisioner "local-exec" {
command = "run_initialize_cluster_playbook.sh"
}
}

この方法は単純な反面、関連するリソース数が増えてくるとdepends_onを各所に書かないといけないというデメリットもありますので適材適所で使い分けましょう。
Null データソース(今ではほぼ出番なし)
NullプロバイダーにはNullリソースのデータソース版であるNullデータソースというものも存在します。
これは以下のようにOutput用に値を参照する部分を一箇所にまとめたり、再利用するために利用します。
data "null_data_source" "values" {
inputs = {
all_server_ids = "${concat(aws_instance.green.*.id, aws_instance.blue.*.id)}"
all_server_ips = "${concat(aws_instance.green.*.private_ip, aws_instance.blue.*.private_ip)}"
}
}
resource "aws_elb" "main" {
# ...
instances = "${data.null_data_source.values.outputs["all_server_ids"]}"
}
output "all_server_ids" {
value = "${data.null_data_source.values.outputs["all_server_ids"]}"
}
output "all_server_ips" {
value = "${data.null_data_source.values.outputs["all_server_ips"]}"
}
が、現在ではLocal Valuesの仕組みがあるためほぼ使うことはないでしょう。
Local Valuesとの違いとしてはterraform graphへの表示有無があります(Local Valuesはgraph上表示されない)が、実用上これが問題になるケースはほとんどないと思います。
ここでもやっぱりLocal Valuesを積極的に使いましょう。
終わりに
ということで今回はNullプロバイダーを扱いました。
うまく使わないと見通しの悪いtfファイルになってしまいますが、順序や依存関係の細かな調整を行うのにはNullリソースが便利ですので状況に応じて適宜使っていきましょう。
以上です。
【モダンTerraform】v0.11以降でdynamicとfor_eachが実装されるかも

今回は最近のhashicorp/terraformでの開発状況から、現時点での最新版であるv0.11.3で未実装な機能の中で個人的にかなり期待している機能について紹介します。
countパラメータとその限界
全てのリソースにはMeta-parametersが指定できる
Terraformには全てのリソースに対しMeta-parametersという属性を指定できます。
これはData Sourcesに対しても指定可能となっています。
具体的には以下のようなものがあります。(詳細はドキュメントを参照)
count (int)
リソースの作成数を指定 ※現時点ではモジュールには指定不可depends_on (list of strings)
リソースが依存するリソースを指定 -> 具体的には作成順序などが制御されるprovider (string)
マルチプロバイダーの定義をしている場合に利用するプロバイダーを指定できる
(例: AWSのリージョンごとにプロバイダー定義している場合にaws.westみたいに指定)lifecycle (configuration block)
リソースの作成/更新/破棄の挙動を調整する
それぞれに興味深いトピックはあるのですが、今日はcountについて扱います。
countパラメータの基本的な使い方
countパラメータは以下のように利用します。
以下の例ではディスクリソースを2つ作成しています。
resource sakuracloud_disk "disks" {
name = "${format("disk-%02d", count.index+1)}"
count = 2
}
countパラメータを指定すると、そのリソースの定義内でcount.indexという属性が利用できます。
これは自身のインデックスを参照するもので、0から始まる数値型の値となっています。
これを利用することで、localsなどとの併用で各リソースに固有の値を指定しておくことが可能です。
locals {
# 監視先のドメインとパスを指定
targets = [
{
domain = "example.com"
path = "/"
},
{
domain = "example.jp"
path = "/foo/"
},
]
}
resource sakuracloud_simple_monitor "monitors" {
# 自身のインデックスでマップ(targetsの各要素)を取得、キーがdomainの値を参照する
target = "${lookup(locals.target[count.index], "domain"}"
health_check = {
protocol = "https"
delay_loop = 60
path = "${lookup(locals.target[count.index], "path"}"
status = "200"
}
count = "${length(locals.target)}"
}
countが利用できない場面
上手く使えばtfファイルを非常にシンプルにしてくれるcountですが、利用できない場面というのがあります。
- リソースの属性
- モジュール
例えば以下の例ではリソースにsettingという属性があるのですが、これに対してのcount指定はできません。
resource "aws_elastic_beanstalk_environment" "myEnv" {
name = "test_environment"
application = "testing"
setting {
# !ここではcountが使えない!
namespace = "aws:elasticbeanstalk:application:environment"
name = "HTTP_PROXY"
value = "10.1.2.1:8080"
}
setting {
namespace = "aws:elasticbeanstalk:application:environment"
name = "TMPDIR"
value = "/var/myapp/tmp"
}
}
議論中: dynanicとfor_each
この問題はGitHub上でも解決に向けた議論が行われています。
参考: GitHub(hashicorp/terraform) - Support count in resource fields #7034
このIssueの中で提案されているのがdynamicブロックとfor_eachです。
これを利用して先ほどの例を書き直すと以下のようになります。
# DRAFT: まだ提案段階でv0.11時点ではこの書き方は利用できません
resource "aws_elastic_beanstalk_environment" "myEnv" {
name = "test_environment"
application = "testing"
dynamic "setting" {
for_each = var.environment_variables # map型の変数を指定
content {
namespace = "aws:elasticbeanstalk:application:environment"
name = setting.foreach.key # 現在のイテレーションの要素は`foreach`キーで参照できる
value = setting.foreach.value
}
}
}
マップ型変数や他で定義したリソースの属性などを利用して、各要素をイテレーションしながら値の参照ができるようです。
この例だとsettingブロック内のfor_eachでイテレーションされる各要素はsetting.foreachで参照でき、それぞれkeyとvalueのように参照できるようです。
以下のようにイテレーターに名前をつけることでネストも可能なようです。
# DRAFT: Not yet integrated into Terraform, and details may change before final release
dynamic "example" {
for_each = var.example_items
iterator = parent # 外側のループのイテレータの名前を指定
content {
dynamic "example" {
for_each = parent.children
iterator = child # 内側のループのイテレータの名前を指定
content {
value = child.value
parent_value = parent.value
}
}
}
}
これは嬉しいですね!
これまでこれを行うにはプロバイダー側で親リソース/子リソースに分割して実装する必要があり、
プロバイダーの実装者としてはなかなか辛い思いをしていました。
(リソースを親子に分割すると作成順序とかロックとか色々考慮が増えるのです…)
現在の実装状況は?
本日の時点では@apparentlymart氏が実験的に実装を進めているようです。
(前出のIssue内のコメントを参照)
期待して待ちましょう!!!
以上です。
【モダンTerraform】ベストプラクティスはTerraform Module Registryを参照しよう

今回は小ネタです。
Terraformでのベストプラクティス?
Terraformでのベストプラクティスは従来GitHubにて専用のリポジトリで公開されていました。
GitHub: hashicorp/best-practices
が、このリポジトリ、すでに「 Deprecated 」です。
じゃあどこ見ればいいのよ?
best-practicesリポジトリでも言及されていますが今後はTerraform Module Registryを参照すべしとのことです。
Twitterでも@mitchellh氏らが質問に答える形で言及していました。

ただし、RegistryにはHashiCorpの方が作成したもの以外も含まれるため、 参考にする際は「HashiCorp Verified Modules」を見るのがオススメです。
Registryでモジュールを検索する時に条件として「Verified」を指定できるのでそれを利用しましょう。
Terraform Module Registry 検索ページ

ということで
Terraform Module Registryを使いましょう。 以上です。
【モダンTerraform】VariableとLocal Valuesの使い分けについて

はじめに
ナウでイケてるヤングな皆様におかれましてはTerraformを使うのはもはや当たり前ですよね?
このTerraformですが日々バージョンが上がっており、ネット上で公開されているtfファイルの書き方が若干古いものもちょいちょい見受けられます。
特にTerraform v0.10.3(2017/8/30リリース)で導入されたLocal Valuesについては利用している例が少ないように思いますので今回通常のvariableとの違いなどについてまとめてみます。
TL; DR 今北産業
- tfファイル内の変数は基本的に
Local Valuesを使おう - 特に判定処理は
Local Valuesで明確な名前をつけよう Variableを使うのは外部からのインプットにする場合だけ
そもそもLocal Valuesってなによ?
Local Valuesとはモジュール内に閉じて使える変数です。モジュール内でのローカル変数のようなものですね。
参考: Terraform ドキュメント - Local Values
なお通常の(これまでもあった)variableについてのドキュメントはこちらです。
参考: Terraform ドキュメント - Input Variables
Local Valuesは以下のように使います。
# Local Valuesとして変数定義
locals {
switch_name = "my-switch-name"
}
# Local Valuesを使う
resource sakuracloud_switch "sw" {
# "local." というプレフィックスで参照できる
name = "${local.switch_name}"
}
localsブロックは複数記述可能
localsブロックはモジュール内に複数記述できます。もちろん変数名はモジュール内で一意である必要があります。
# サーバ関連の変数を定義
locals {
server_name = "foobar"
server_core = 2
server_memory = 4
}
# ディスク関連の変数を定義
locals {
disk_name = "foobar"
disk_size = "20"
}
様々なデータ型が使える
Terraformで使える様々なデータ型を指定することが可能です。
locals {
# bool型
enabled = true
# 数値型(10進数)
num10 = 10
# 数値型(16進数)
num16 = 0x16
# 文字列
strvar = "example"
# リスト
listvar = ["item1", "item2", "item3"]
# マップ
mapvar = {
item1 = "foo"
item2 = "bar"
item3 = "baz"
}
# 複合型(map/リスト/文字列など)
compvar = {
tags = ["tag1", "tag2"]
metadata = {
foo = "1"
bar = "2"
nested = ["foo", "bar"]
}
}
}
Variableとの違い
関数や他リソースの参照が使える
Local Valuesには関数や他リソースの参照などが書けます。
例えば任意の変数が設定されているか(空文字以外が指定されているか)を判定した結果を変数として保持しておけます。
variableを利用する場合、三項演算子などでvariableの値を判定して分岐させるというような処理を行っていました。
variable use_load_balancer {}
resource sakuracloud_load_balancer "lb" {
# use_load_balancer変数が設定されていたらcountを1に、以外の場合は0にしてリソース作成しない
count = "${var.use_load_balancer == "" ? 1 : 0}"
}
resource sakuracloud_switch "sw" {
# use_load_balancer変数が設定されていたらcountを1に、以外の場合は0にしてリソース作成しない
count = "${var.use_load_balancer == "" ? 1 : 0}"
}
同じ判定を行なっている箇所が複数あってDRYじゃないですね。
これをLocal Valuesを使って書き直すと以下のようになります。
variable use_load_balancer {}
# 判定処理をlocalsブロック内に局所化
locals {
load_balancer_count = "${var.use_load_balancer == "" ? 1 : 0}"
switch_count = "${local.load_balancer_count}"
}
resource sakuracloud_load_balancer "lb" {
count = "${local.load_balancer_count}"
}
resource sakuracloud_switch "sw" {
count = "${local.switch_count}"
}
判定処理についてはわかりやすい名前をつけておくことで可読性も上がりますし、どういう判定をしているのか追いやすい(定義を見ればよい)ですね。
Local Valuesは外部からの値の設定ができない
variableは以下のように様々な方法で値の設定を行うことができます。
apply実行時に対話的に入力- コマンドラインから
-varオプションや-ver-fileオプションで指定 terraform.tfvarsファイルで指定- 環境変数(
TF_VAR_xxxなど)で指定 - variableの定義時にデフォルト値を明示
このため、variableをtfファイルの簡易化といった目的で利用していた場合は意図しない値が入力される可能性もあったりします。
例えばcount構文と組み合わせる場合に以下のような書き方をすることがありました。
#
# Local Valuesがない時代の書き方
#
# サーバに割り当てるIPアドレスのリスト
variable ip_list {
default = ["192.2.0.1", "192.2.0.2", "192.2.0.3"]
}
resource sakuracloud_server "servers" {
# ip_listの要素数分のサーバを作成
count = "${length(var.ip_list)}"
# 自身のインデックスでIPアドレスリストを参照
ipaddress = "${var.ip_list[count.index]}"
}
ip_listはvariableなため、外部から意図しない値が入力される可能性があります。
Local Valuesであればこの辺りを気にせずに使用可能です。
#
# Local Valuesを利用した書き方
#
# サーバに割り当てるIPアドレスのリスト
locals {
ip_list = ["192.2.0.1", "192.2.0.2", "192.2.0.3"]
}
resource sakuracloud_server "servers" {
# ip_listの要素数分のサーバを作成
count = "${length(local.ip_list)}"
# 自身のインデックスでIPアドレスリストを参照
ipaddress = "${local.ip_list[count.index]}"
}
ということで、意図しない値の設定を防ぐためにもtfファイル上で変数を扱う際はまずLocal Valuesを利用し、外部から値の入力が必要な場合のみvariableを利用するのがオススメです。
まとめ
ということでLocal Valuesを積極的に使いましょう。 以上です。
続・さくらのクラウド上にMetabaseを構築する【HTTPS対応版】
前回はMetabase環境構築を行いました。
しかし、実運用の際はHTTPS対応は必須だと思いますので対応版を作りました。
Metabase環境構築 on さくらのクラウド(HTTPS版)
今回はLet's encryptにてHTTPS対応を行うバージョンとなっています。 Let's encrypt対応にはsteveltn/https-portalコンテナを利用します。
構築手順は前回とほぼ同じですが、あらかじめさくらのクラウド上にDNSゾーンの登録を行っておく必要があります。
テンプレート
前回との差分は以下の通りです。
- コメントを追加
- Let's encrypt用にDNS関連の記述を追加
- RancherOSのcloud-configにLet's encrypt用のコンテナを記述
### 概要
#
# データベースアプライアンス(PostgreSQL)とRancherOSでMetabase実行環境を構築するテンプレート
#
# このテンプレートはRancherOS上のDockerでMetabaseを実行する構成となっています。
# Metabaseのバックエンドとしてデータベースアプライアンス(PostgreSQL)を利用します。
#
# MetabaseサーバのHTTPS対応としてLet's encryptでの証明書取得も行います。
#
# <事前準備>
#
# 1) さくらのクラウド上にSSH用の公開鍵を登録します。
# 2) さくらのクラウド上にDNSゾーンを登録しネームサーバの設定などを行っておきます。
# (すでに登録済みのゾーンがあればそれを利用可能です。)
#
# <構築手順>
# 1) リソースマネージャーにて新しいテンプレートを作成し、このtffileの内容を貼り付けます。
# 2) tffile編集画面の"変数定義"タブにて以下の値を編集します。
# - サーバ管理者のパスワード(server_password)
# - データベース接続ユーザーのパスワード(database_password)
# - さくらのクラウドに登録済みの公開鍵の名称(ssh_public_key_name)
# - さくらのクラウドに登録済みのDNSゾーン名(dns_zone_name)
# 3) リソースマネージャー画面にて"計画/反映"を実行
#
# <動作確認>
#
# ブラウザから以下のURLにアクセスするとMetabaseの画面が開きます。
# https://<MetabaseサーバのFQDN>/
#
# MetabaseサーバのFQDNは以下の形式です。
# ${server_name}.${dns_zone_name}
#
# FQDNの例:
# - server_name: "metabase"
# - dns_zone_name: "example.com"
# この場合FQDNは以下のようになります。
# FQDN: metabase.example.com
#
# <サーバへのSSH接続>
#
# サーバへのSSH接続は、指定した公開鍵による公開鍵認証のみ許可されるようになっています。
# SSH接続の際は秘密鍵を指定して接続してください。
#
# > usacloudでのSSH接続例
# $ usacloud server ssh -i <your-private-key-file> <your-server-name>
#
# SSH接続後はdocker logsコマンドなどでMetabaseコンテナのログを確認可能です。
#
### 変数定義
locals {
#*********************************************
# パスワード/公開鍵関連(要変更)
#*********************************************
# サーバ管理者のパスワード
server_password = "<put-your-password-here>"
# データベース接続ユーザーのパスワード
database_password = "<put-your-password-here>"
# さくらのクラウドに登録済みの公開鍵の名称
ssh_public_key_name = "<put-your-public-key-name>"
# さくらのクラウドに登録済みのDNSゾーン名
dns_zone_name = "<put-your-zone-name>"
#*********************************************
# サーバ/ディスク
#*********************************************
# サーバ名
server_name = "metabase"
# サーバホスト名
host_name = "${local.server_name}"
# サーバ コア数
server_core = 2
# サーバ メモリサイズ(GB)
server_memory = 4
# ディスクサイズ
disk_size = 20
#*********************************************
# ネットワーク(スイッチ/パケットフィルタ)
#*********************************************
# スイッチ名
switch_name = "metabase-internal"
# パケットフィルタ名
packet_filter_name = "metabase-filter"
#*********************************************
# データベースアプライアンス
#*********************************************
# データベースアプライアンス名
database_name = "metabase-db"
# プラン
database_plan = "30g" # 10g/30g/90g/240g
# 接続ユーザー名
database_user_name = "metabase"
# バックアップ時刻
database_backup_time = "01:00"
}
### サーバ/ディスク
# パブリックアーカイブ(OS)のID参照用のデータソース(RancherOS)
data sakuracloud_archive "rancheros" {
os_type = "rancheros"
}
# 公開鍵のID参照用のデータソース
data "sakuracloud_ssh_key" "ssh_public_key" {
name_selectors = ["${local.ssh_public_key_name}"]
}
# ディスク
resource "sakuracloud_disk" "disk" {
name = "${local.server_name}"
source_archive_id = "${data.sakuracloud_archive.rancheros.id}"
hostname = "${local.host_name}"
password = "${local.server_password}"
note_ids = ["${sakuracloud_note.provisioning.id}"]
ssh_key_ids = ["${data.sakuracloud_ssh_key.ssh_public_key.id}"]
disable_pw_auth = true
lifecycle {
ignore_changes = ["source_archive_id"]
}
}
# サーバ
resource "sakuracloud_server" "server" {
name = "${local.server_name}"
disks = ["${sakuracloud_disk.disk.id}"]
core = "${local.server_core}"
memory = "${local.server_memory}"
packet_filter_ids = ["${sakuracloud_packet_filter.filter.id}"]
additional_nics = ["${sakuracloud_switch.sw.id}"]
}
# スタートアップスクリプト(IP設定、metabaseコンテナ起動)
locals {
fqdn = "${local.server_name}.${local.dns_zone_name}"
}
resource "sakuracloud_note" "provisioning" {
name = "provisioning-metabase"
class = "yaml_cloud_config"
content = <<EOF
#cloud-config
rancher:
console: default
docker:
engine: docker-17.09.1-ce
network:
interfaces:
eth1:
address: 192.168.100.10/28
dhcp: false
services:
https-portal:
image: sacloud/https-portal
ports:
- "80:80"
- "443:443"
volumes:
- https-portal:/var/lib/https-portal
environment:
DOMAINS: "${local.fqdn} -> http://192.168.100.10:3000"
STAGE: production
restart: always
metabase:
image: metabase/metabase:latest
ports:
- "3000:3000"
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: ${local.database_user_name}
MB_DB_PORT: 5432
MB_DB_USER: ${local.database_user_name}
MB_DB_PASS: ${local.database_password}
MB_DB_HOST: 192.168.100.2
restart: always
EOF
}
### データベースアプライアンス
resource "sakuracloud_database" "db" {
name = "${local.database_name}"
database_type = "postgresql"
plan = "${local.database_plan}"
user_name = "${local.database_user_name}"
user_password = "${local.database_password}"
allow_networks = ["192.168.100.0/28"]
port = 5432
backup_time = "${local.database_backup_time}"
switch_id = "${sakuracloud_switch.sw.id}"
ipaddress1 = "192.168.100.2"
nw_mask_len = 28
default_route = "192.168.100.1"
}
### パケットフィルタ
resource "sakuracloud_packet_filter" "filter" {
name = "${local.packet_filter_name}"
expressions = {
protocol = "tcp"
dest_port = "22"
description = "Allow external:SSH"
}
expressions = {
protocol = "tcp"
dest_port = "80"
description = "Allow external:HTTP(for Let's encrypt)"
}
expressions = {
protocol = "tcp"
dest_port = "443"
description = "Allow external:HTTPS"
}
expressions = {
protocol = "icmp"
}
expressions = {
protocol = "fragment"
}
expressions = {
protocol = "udp"
source_port = "123"
}
expressions = {
protocol = "tcp"
dest_port = "32768-61000"
description = "Allow from server"
}
expressions = {
protocol = "udp"
dest_port = "32768-61000"
description = "Allow from server"
}
expressions = {
protocol = "ip"
allow = false
description = "Deny ALL"
}
}
### スイッチ
resource sakuracloud_switch "sw" {
name = "${local.switch_name}"
}
### DNS
data sakuracloud_dns "zone" {
filter = {
name = "Name"
values = ["${local.dns_zone_name}"]
}
}
#DNSレコード
resource sakuracloud_dns_record "records" {
dns_id = "${data.sakuracloud_dns.zone.id}"
name = "${local.server_name}"
type = "A"
value = "${sakuracloud_server.server.ipaddress}"
}
後はリソースマネージャーで展開するだけでOKです。
以上です。