HashiCorpの新オーケストレーションツールTerraformを試してみた

Terraform
344件のシェア(そこそこ話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ども、大瀧です。
VagrantPackerSerfを開発するHashiCorpが手がける新オーケストレーションツール、Terraformが発表されました。zembutsuさんが神速で日本語チュートリアル記事を既に公開しているので、terraformコマンドの使い方はそちらを参照ください。

Terraformとは

Terraformは、あらかじめインフラ構成をテンプレートファイルに記述し、terraformコマンドでクラウド環境に適用・管理するツールです。一見するとAWS CloudFormationと非常に良く似た作りですが、以下の特徴があります。

複数のクラウドサービスに対応し、高レイヤーのアプリケーション構成に特化

現時点でTerraformが対応するサービス/プロダクトは以下です。

  • AWS
  • CloudFlare
  • Consul
  • DigitalOcean
  • DNSimple
  • Heroku

誤解を恐れずに率直な感想で言うと、カオスですね(いい意味で)。fogRightScaleのような複数のIaaSをサポートする既存のツールがありますが、TerraformはIaaS/PaaS/SaaSそれぞれを同一テンプレートに混在させることができます。

IaaS/PaaS/SaaSという括りはアプリケーションの構成としては本来あまり意味のない区分であり、Terraformはアプリ開発者の視点に立ち、複数のサービスから必要なものだけをピックアップするシンプルな構成管理を目指していると考えられます。個人的にはConsulサポートが燦然と輝いて見えるので、その方面のサポート拡大に期待します。

CloudFormationの弱いところはカバー済み

CloudFormationはAWSの構成ツールとして非常に強力な機能を持ちますが、いくつか扱いづらい点がありました。Terraformはそれらの弱点がほとんどカバーされており、CloudFormationの代替としても期待できる印象です。

  • Dry-Run(terraform plan)があり、アップデート時に差分が確認できる
  • テンプレートにコメントが書け、自然な変数参照ができる(JSON独自拡張形式)
  • terraform graphでコンポーネントを可視化できる(Graphizのdot形式で出力)
  • VagrantやPackerで実績のある柔軟性の高いプラグイン構造

ただし、現在はTerraformがサポートするAWSリソースはまだまだ少ないため、今後に期待です。

Terraformのインストール

TerraformはGo言語で開発されており、Packerと同様ダウンロードしたバイナリをPATHの通ったディレクトリに配置するだけでインストール完了です。お手軽ですね。

今回はMac OS X MarvericksにDownloadページのMac OS X amd64版をダウンロードしました。zipファイルを展開し、任意のパスに展開(今回は~/.terraform-0.1.0)し、~/.bash_profileファイルに以下を追記します。

export PATH=$PATH:${HOME}/.terraform-0.1.0/

ターミナルを起動し、terraformコマンドを実行、以下のイメージが表示されればOKです。

ikkomon:~ ryuta$ terraform
usage: terraform [--version] [--help] <command> [<args>]

Available commands are:
    apply      Builds or changes infrastructure
    graph      Create a visual graph of Terraform resources
    output     Read an output from a state file
    plan       Generate and show an execution plan
    refresh    Update local state file against real resources
    show       Inspect Terraform state or plan
    version    Prints the Terraform version
ikkomon:~ ryuta$ terraform

Terraformの実行

公式ドキュメントにある以下のサンプルテンプレートを実行してみます。セキュリティグループ、EC2インスタンス、ELBをそれぞれ1つずつ作成するテンプレートです。

example.tf

variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "key_path" {}
variable "key_name" {}
variable "aws_region" {
    default = "us-west-2"
}

# Ubuntu Precise 12.04 LTS (x64)
variable "aws_amis" {
    default = {
        "eu-west-1": "ami-b1cf19c6",
        "us-east-1": "ami-de7ab6b6",
        "us-west-1": "ami-3f75767a",
        "us-west-2": "ami-21f78e11"
    }
}

# Specify the provider and access details
provider "aws" {
    access_key = "${var.aws_access_key}"
    secret_key = "${var.aws_secret_key}"
    region = "${var.aws_region}"
}

# Our default security group to access
# the instances over SSH and HTTP
resource "aws_security_group" "default" {
    name = "terraform_example"
    description = "Used in the terraform"

    # SSH access from anywhere
    ingress {
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    # HTTP access from anywhere
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}


resource "aws_elb" "web" {
  name = "terraform-example-elb"

  # The same availability zone as our instance
  availability_zones = ["${aws_instance.web.availability_zone}"]

  listener {
    instance_port = 80
    instance_protocol = "http"
    lb_port = 80
    lb_protocol = "http"
  }

  # The instance is registered automatically
  instances = ["${aws_instance.web.id}"]
}


resource "aws_instance" "web" {
  # The connection block tells our provisioner how to
  # communicate with the resource (instance)
  connection {
    # The default username for our AMI
    user = "ubuntu"

    # The path to your keyfile
    key_file = "${var.key_path}"
  }

  instance_type = "m1.small"

  # Loookup the correct AMI based on the region
  # we specified
  ami = "${lookup(var.aws_amis, var.aws_region)}"

  # The name of our SSH keypair you've created and downloaded
  # from the AWS console.
  #
  # https://console.aws.amazon.com/ec2/v2/home?region=us-west-2#KeyPairs:
  #
  key_name = "${var.key_name}"

  # Our Security group to allow HTTP and SSH access
  security_groups = ["${aws_security_group.default.name}"]

  # We run a remote provisioner on the instance after creating it.
  # In this case, we just install nginx and start it. By default,
  # this should be on port 80
  provisioner "remote-exec" {
    inline = [
        "sudo apt-get -y update",
        "sudo apt-get -y install nginx",
        "sudo service nginx start",
    ]
  }
}

output "address" {
  value = "${aws_elb.web.dns_name}"
}

まずはterraform planで作成されるコンポーネントを確認します。AWS APIにアクセスするterraformコマンドの実行時には、AWSのAPIキー/シークレットキーを指定する必要があります。指定方法はいくつかありますが、今回は手っ取り早くコマンド実行時にオプションで指定しました。

ikkomon:Temp ryuta$ terraform plan \
> -var 'aws_access_key=AKIAXXXXXXXXXXXXXXXXX' \
> -var 'aws_secret_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' \
> -var 'key_path=/Users/ryuta/.ssh/OtakiKet.pem' \
> -var 'key_name=otaki-kp1'
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_elb.web
    availability_zones.#:         "" => "1"
    availability_zones.0:         "" => "${aws_instance.web.availability_zone}"
    dns_name:                     "" => "<computed>"
    instances.#:                  "" => "1"
    instances.0:                  "" => "${aws_instance.web.id}"
    listener.#:                   "" => "1"
    listener.0.instance_port:     "" => "80"
    listener.0.instance_protocol: "" => "http"
    listener.0.lb_port:           "" => "80"
    listener.0.lb_protocol:       "" => "http"
    name:                         "" => "terraform-example-elb"

+ aws_instance.web
    ami:               "" => "ami-21f78e11"
    availability_zone: "" => "<computed>"
    instance_type:     "" => "m1.small"
    key_name:          "" => "otaki-kp1"
    private_dns:       "" => "<computed>"
    private_ip:        "" => "<computed>"
    public_dns:        "" => "<computed>"
    public_ip:         "" => "<computed>"
    security_groups:   "" => "<computed>"
    security_groups.#: "" => "1"
    security_groups.0: "" => "terraform_example"
    subnet_id:         "" => "<computed>"

+ aws_security_group.default
    description:             "" => "Used in the terraform"
    ingress.#:               "" => "2"
    ingress.0.cidr_blocks.#: "" => "1"
    ingress.0.cidr_blocks.0: "" => "0.0.0.0/0"
    ingress.0.from_port:     "" => "22"
    ingress.0.protocol:      "" => "tcp"
    ingress.0.to_port:       "" => "22"
    ingress.1.cidr_blocks.#: "" => "1"
    ingress.1.cidr_blocks.0: "" => "0.0.0.0/0"
    ingress.1.from_port:     "" => "80"
    ingress.1.protocol:      "" => "tcp"
    ingress.1.to_port:       "" => "80"
    name:                    "" => "terraform_example"
    owner_id:                "" => "<computed>"
    vpc_id:                  "" => "<computed>"
ikkomon:Temp ryuta$

では、terraform applyで実際に各リソースを作成してみます。今回はテンプレート記述の通り、us-west-2(Oregon)リージョンに作成されます。

ikkomon:Temp ryuta$ terraform apply \
> -var 'aws_access_key=AKIAXXXXXXXXXXXXXXXXX' \
> -var 'aws_secret_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' \
> -var 'key_path=/Users/ryuta/.ssh/OtakiKet.pem' \
> -var 'key_name=otaki-kp1'
aws_security_group.default: Creating...
  description:             "" => "Used in the terraform"
  ingress.#:               "" => "2"
  ingress.0.cidr_blocks.#: "" => "1"
  ingress.0.cidr_blocks.0: "" => "0.0.0.0/0"
  ingress.0.from_port:     "" => "22"
  ingress.0.protocol:      "" => "tcp"
  ingress.0.to_port:       "" => "22"
  ingress.1.cidr_blocks.#: "" => "1"
  ingress.1.cidr_blocks.0: "" => "0.0.0.0/0"
  ingress.1.from_port:     "" => "80"
  ingress.1.protocol:      "" => "tcp"
  ingress.1.to_port:       "" => "80"
  name:                    "" => "terraform_example"
aws_security_group.default: Creation complete
aws_instance.web: Creating...
  ami:               "" => "ami-21f78e11"
  instance_type:     "" => "m1.small"
  key_name:          "" => "otaki-kp1"
  security_groups.#: "" => "1"
  security_groups.0: "" => "terraform_example"
aws_instance.web: Provisioning with 'remote-exec'...
aws_instance.web: Creation complete
aws_elb.web: Creating...
  availability_zones.#:         "" => "1"
  availability_zones.0:         "" => "us-west-2a"
  instances.#:                  "" => "1"
  instances.0:                  "" => "i-53650c58"
  listener.#:                   "" => "1"
  listener.0.instance_port:     "" => "80"
  listener.0.instance_protocol: "" => "http"
  listener.0.lb_port:           "" => "80"
  listener.0.lb_protocol:       "" => "http"
  name:                         "" => "terraform-example-elb"
aws_elb.web: Creation complete

Apply complete! Resources: 2 added, 0 changed, 1 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

Outputs:

  address = terraform-example-elb-786403037.us-west-2.elb.amazonaws.com
ikkomon:Temp ryuta$

AWS管理コンソールを確認してみると、確かにEC2インスタンスが作成されています!

terraform01

まとめ

新しいオーケストレーションツール、Terraformをご紹介しました。作成後のアップデートなど、CloudFormationだとハマりどころの部分がTerraformだとどのようになるか気になる部分もありますので、引き続き検証していきたいと思います。

参考記事