【朗報】Terraform v0.8.0でREPL機能が限定的にサポートされます

2016.11.15

はじめに

こんにちは、中山です。

実質的なメジャーバージョンアップであるv0.8.0に向けてベータリリースが続いています。現在(2016年11月15日)、v0.8.0系とv0.7.X系が同時にリリースされている状況です。今回はv0.8.0に搭載予定の機能である console サブコマンドについてご紹介します。これは、まだ制限がありますが、TerraformをREPL形式で実行できる機能です。

機能概要

この機能はこちらのPRで取り込まれたものです。 terraform console を実行するとREPL形式で変数展開が実行可能です。また、stateファイルがある場合はその中から各種属性を確認できます。今までは、Terraformの変数展開の挙動を確認するために、いちいちtfファイルを用意する必要がありました。これは少々面倒です。この機能によりさっと動作を確認できるようになったのは素晴らしいアップデートだと思います。

ただし、上記PRにも書かれていますが、現時点ではTerraformそのものの実行( plan / apply など )はサポートされていません。将来的には搭載予定のようなので期待したいと思います。

使ってみる

では、早速使ってみましょう。現時点ではv0.8.0がリリースされていないため、自分でTerraformをコンパイルする必要があります。詳細はこちらのドキュメントに詳しいですが、簡単に説明すると以下のコマンドで実行可能です(事前にGo環境は整えておく必要はあります)。

$ go get github.com/hashicorp/terraform
$ cd $GOPATH/src/github.com/hashicorp/terraform
$ git checkout aaf1ad05324153c88113da7cdcc3bf36e5df9adb
$ git log -1
commit aaf1ad05324153c88113da7cdcc3bf36e5df9adb
Merge: 25d19ef 1a6056b
Author: Mitchell Hashimoto <xmitchx@gmail.com>
Date:   Mon Nov 14 11:53:49 2016 -0800

    Merge pull request #10093 from hashicorp/f-console

    Add `terraform console` for REPL
$ make dev
$ $GOPATH/bin/terraform version
Terraform v0.8.0-dev (aaf1ad05324153c88113da7cdcc3bf36e5df9adb)

この機能からstateファイルも扱ってみたいので以下のようなサンプルとなるコードを用意しました。VPC内に複数のサブネットを作るだけのものです。 main.tf など適当なファイル名で保存すればそのまま利用可能です。

variable "name" {
  default = "test-vpc"
}

variable "vpc_cidr" {
  default = "192.168.0.0/16"
}

provider "aws" {
  region = "ap-northeast-1"
}

data "aws_availability_zones" "az" {}

resource "aws_vpc" "vpc" {
  cidr_block           = "${var.vpc_cidr}"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags {
    Name = "${var.name}-vpc"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = "${aws_vpc.vpc.id}"

  tags {
    Name = "${var.name}-igw"
  }
}

resource "aws_subnet" "frontend_subnet" {
  count                   = 2
  vpc_id                  = "${aws_vpc.vpc.id}"
  cidr_block              = "${cidrsubnet(var.vpc_cidr, 8, count.index)}"
  availability_zone       = "${data.aws_availability_zones.az.names[count.index]}"
  map_public_ip_on_launch = true

  tags {
    Name = "${var.name}-frontend-subnet-${count.index + 1}"
  }
}

resource "aws_route_table" "frontend_subnet" {
  vpc_id = "${aws_vpc.vpc.id}"

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = "${aws_internet_gateway.igw.id}"
  }

  tags {
    Name = "${var.name}-frontend-rtb"
  }
}

resource "aws_route_table_association" "frontend_subnet" {
  count          = 2
  subnet_id      = "${element(aws_subnet.frontend_subnet.*.id, count.index)}"
  route_table_id = "${aws_route_table.frontend_subnet.id}"
}

resource "aws_subnet" "application_subnet" {
  count                   = 2
  vpc_id                  = "${aws_vpc.vpc.id}"
  cidr_block              = "${cidrsubnet(var.vpc_cidr, 8, count.index + 100)}"
  availability_zone       = "${data.aws_availability_zones.az.names[count.index]}"
  map_public_ip_on_launch = true

  tags {
    Name = "${var.name}-application-subnet-${count.index + 1}"
  }
}

resource "aws_route_table" "application_subnet" {
  vpc_id = "${aws_vpc.vpc.id}"

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = "${aws_internet_gateway.igw.id}"
  }

  tags {
    Name = "${var.name}-application-rtb"
  }
}

resource "aws_route_table_association" "application_subnet" {
  count          = 2
  subnet_id      = "${element(aws_subnet.application_subnet.*.id, count.index)}"
  route_table_id = "${aws_route_table.application_subnet.id}"
}

resource "aws_subnet" "datastore_subnet" {
  count                   = 2
  vpc_id                  = "${aws_vpc.vpc.id}"
  cidr_block              = "${cidrsubnet(var.vpc_cidr, 8, count.index + 200)}"
  availability_zone       = "${data.aws_availability_zones.az.names[count.index]}"
  map_public_ip_on_launch = false

  tags {
    Name = "${var.name}-datastore-subnet-${count.index + 1}"
  }
}

resource "aws_network_acl" "acl" {
  vpc_id     = "${aws_vpc.vpc.id}"
  subnet_ids = ["${aws_subnet.frontend_subnet.*.id}", "${aws_subnet.application_subnet.*.id}"]

  ingress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  egress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags {
    Name = "${var.name}-acl"
  }
}

以下のコマンドで環境を構築してください。

$ $GOPATH/bin/terraform plan
$ $GOPATH/bin/terraform apply
<snip>
Apply complete! Resources: 15 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

では早速 console を利用してみます。stateファイルがあるディレクトリ上で以下のコマンドを実行してください。すると、以下のようにREPLが開始されます。

$ $GOPATH/bin/terraform console
>

ヘルプは help で確認できます。下の方に記載されているように exit などでREPLから抜けることが可能です。readline機能があるので基本的なシェルと同じショートカットが利用できます(補完は未実装)。

> help
The Terraform console allows you to experiment with Terraform interpolations.
You may access resources in the state (if you have one) just as you would
from a configuration. For example: "aws_instance.foo.id" would evaluate
to the ID of "aws_instance.foo" if it exists in your state.

Type in the interpolation to test and hit <enter> to see the result.

To exit the console, type "exit" and hit <enter>, or use Control-C or
Control-D.

この状態で各種変数展開の挙動が確認できます。例えば、 cidrhost 関数の挙動を確認したい場合は以下のように実行します。

> cidrhost("10.0.0.0/8", 2)
10.0.0.2

stateファイルに記載されている変数の内容を確認したい場合は、一度REPLを抜けて state list の出力を表示させると便利です。

$ $GOPATH/bin/terraform state list
aws_availability_zones.az
aws_internet_gateway.igw
aws_network_acl.acl
aws_route_table.application_subnet
aws_route_table.frontend_subnet
aws_route_table_association.application_subnet[0]
aws_route_table_association.application_subnet[1]
aws_route_table_association.frontend_subnet[0]
aws_route_table_association.frontend_subnet[1]
aws_subnet.application_subnet[0]
aws_subnet.application_subnet[1]
aws_subnet.datastore_subnet[0]
aws_subnet.datastore_subnet[1]
aws_subnet.frontend_subnet[0]
aws_subnet.frontend_subnet[1]
aws_vpc.vpc

VPCの各種属性を確認してみます。

> aws_vpc.vpc.id
vpc-98cc00fc
> aws_vpc.vpc.cidr_block
192.168.0.0/16
> aws_vpc.vpc.instance_tenancy
default

もちろん、 * でリストの展開もOKです。

> concat(aws_subnet.frontend_subnet.*.id, aws_subnet.application_subnet.*.id)
[
  subnet-9637bbe0,
  subnet-c235e39a,
  subnet-a937bbdf,
  subnet-c035e398
]

まとめ

いかがでしょうか。

まだまだ限定的な機能しかサポートされていませんが、結構夢がひろがりんぐな機能だと思います。例えば、障害時などで緊急に対応する必要がある場合、Terraformのコードをいちいち修正するのはちょっとしんどいと思います。ただし、マネジメントコンソールで操作してしまうとstateファイルとの差分が出てしまう。そういった場合、この機能を使うとstateファイルを更新しつつ、アドホックにTerraformが実行できるかもしれません。

あるいは、awscliの代替としての使い道もありそうです。stateファイルにAWSリソースの情報が保存されるので、awscliの用に describe 系コマンドでいちいちパース処理をしなくても良いかもしれません。または、かっちりとTerraformでコードを組む前に、動作検証含めREPL形式で構築するといった使い方もできそうです。

いずれにせよ、今後のアップデートに期待できる機能になりそうです。

本エントリがみなさんの参考になれば幸いです。