ちょっと話題の記事

Terraformで複数台のEC2インスタンスを構築する場合のTIPS

2016.05.15

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

EC2でサーバーを構築する場合、負荷分散や可用性を考慮して複数台構成とする場合が多いかと思います。 全く同じ構成のEC2インスタンスを複数台構築する、、今回はTerraformでをそれを行う場合のTIPSをいくつかご紹介いたします。

前提

以下のEC2のリソース定義をベースに、各種TIPSを適用していきます。サブネット、セキュリティグループは既存のものを利用する前提で話を進めます。

variable aws_access_key {}
variable aws_secret_key {}

provider "aws" {
    access_key = "${var.aws_access_key}"
    secret_key = "${var.aws_secret_key}"
    region = "ap-northeast-1"
}

resource "aws_instance" "web" {
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "subnet-a1b3bad6"
    tags {
        Name = "web"
    }
}

単純な複数台構成

Terraformでは作成するリソース数をcountパラメータで指定できます。EC2を2台構築したい場合はcount = 2と指定します。

resource "aws_instance" "web" {
    count = 2
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "subnet-a1b3bad6"
    tags {
        Name = "web"
    }
}

タグの値をインスタンス毎に変更したい

count.indexで、リソースのインデックスが取得できます。インデックスは0から始まりインクリメントされていくので、count.indexの値は1台目は「0」、2台目は「1」、3台目は「2」、、となります。このインデックス値をタグの値に含めることで、インスタンス毎に異なるタグの値を設定することができます。

resource "aws_instance" "web" {
    count = 2
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "subnet-a1b3bad6"
    tags {
        Name = "${format("web%02d", count.index + 1)}"
    }
}

タグの値にインデックスを含めるには、format関数を利用します。format関数では標準的なsprintfのフォーマットで文字列を整形することができます。(詳しい構文についてはこちらが参考になります。)上記の例では、タグの値は「web01」、「web02」、、となります。Terraformの${}(式の埋め込み構文)の中では算術演算子が使えるというのもポイントです。

Terraformで同じ設定のリソースを複数作成する場合は、このcount.indexを駆使してあれこれやっていくのが基本となります。

EC2インスタンスを異なるAZに配置したい

EC2を複数台構成にする場合は、2つのAZに分散して配置する場合が多いかと思います。count.indexを利用して、4台のEC2を、1台目はAZ-aに、2台目はAZ-cに、3台目はAZ-aに、、という具合に、2つのAZに交互に配置してみます。

variable "subnets" {
  default = {
    "0" = "subnet-a1b3bad6"
    "1" = "subnet-2278597b"
  }
}

resource "aws_instance" "web" {
    count = 4
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "${lookup(var.subnets, count.index%2)}"
    tags {
        Name = "${format("web%02d", count.index + 1)}"
    }
}

map形式で、「"0" = "AZ-aのサブネット"」、「"1" =" AZ-cのサブネット"」という変数を定義します。EC2のリソース定義側ではlookup関数でこのmapの値を取得します。lookup関数に渡すkeyはcount.index%2とします(%は算術演算子です)。count.index%2の実行結果は「0」または「1」となりますので、結果的にEC2が2つのAZに交互に配置される形となります。

変数をmapではなく"subnet-000e1111,subnet-000e2222"のようにカンマ区切りの文字列で定義して、EC2のリソース定義側ではelement関数split関数を使ってサブネットIDを取得する方法もあります。

variable subnets {
    default = "subnet-a1b3bad6,subnet-2278597b"
}

resource "aws_instance" "web" {
    count = 4
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "${element(split(",", var.subnets), count.index%length(split(",", var.subnets)))}"
    tags {
        Name = "${format("web%02d", count.index + 1)}"
    }
}

split関数で文字列をリストに変換し、element関数でリストから値を取り出します。length関数でリストの長さが取得できますので、count.index%4count.index%length(element(split(",", var.subnets)))と書き換えることができます。

EC2インスタンスにEIPを付与したい

作成したEC2インスタンスのIDのリストはaws_instance.web.*.idで参照できますので、EIPリソース定義の中で${element(aws_instance.web.*.id, count.index)}と書いてインスタンスのIDを一つずつ取り出してEIPを付与します。

variable subnets {
    default = "subnet-a1b3bad6,subnet-2278597b"
}

resource "aws_instance" "web" {
    count = 4
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "${element(split(",", var.subnets), count.index%length(split(",", var.subnets)))}"
    tags {
        Name = "${format("web%02d", count.index + 1)}"
    }
}

resource "aws_eip" "web" {
    count = 4
    instance = "${element(aws_instance.web.*.id, count.index)}"
    vpc = true
}

この場合EC2とEIPのcountの数は同一になるので、countの値を変数で定義するのも良いかと思います。

variable subnets {
    default = "subnet-a1b3bad6,subnet-2278597b"
}
variable web_servers_count {
    default = 4
}

resource "aws_instance" "web" {
    count = "${var.web_servers_count}"
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "${element(split(",", var.subnets), count.index%length(split(",", var.subnets)))}"
    tags {
        Name = "${format("web%02d", count.index + 1)}"
    }
}

resource "aws_eip" "web" {
    count = "${var.web_servers_count}"
    instance = "${element(aws_instance.web.*.id, count.index)}"
    vpc = true
}

ちなみに、Terraformのv0.6.16ではaws_eip_associationリソースが追加され、既存EIPの割当が可能になっています。

EC2に対してプロビジョニングを実行したい

本題からは話が逸れますが、EC2に対してプロビジョニングを実行したい場合を考えてみます。EC2のリソース定義の中にプロビジョニングの定義を追加したいところですが、EC2インスタンス作成時にはEIPが付与されていないため(インターネット経由でEC2に接続できないため)、プロビジョニングに失敗します。ワークアラウンドとして、EIPリソース側にプロビジョニングの定義を追加する方法があります。

variable subnets {
    default = "subnet-a1b3bad6,subnet-2278597b"
}
variable web_servers_count {
    default = 4
}
variable ssh_key_file {
    default = "~/.ssh/cm-yawata.yutaka.pem"
}

resource "aws_instance" "web" {
    count = "${var.web_servers_count}"
    ami = "ami-29160d47"
    instance_type = "t2.nano"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
        "sg-761b9512",
    ]
    subnet_id = "${element(split(",", var.subnets), count.index%length(split(",", var.subnets)))}"
    tags {
        Name = "${format("web%02d", count.index + 1)}"
    }
}

resource "aws_eip" "web" {
    count = "${var.web_servers_count}"
    instance = "${element(aws_instance.web.*.id, count.index)}"
    vpc = true
    provisioner "remote-exec" {
        connection {
            host = "${self.public_ip}"
            type = "ssh"
            user = "ec2-user"
            key_file = "${var.ssh_key_file}"
        }
        inline = [
            "sudo yum -y update",
        ]
    }
}

リソース定義のブロック内で自リソースの属性を参照するにはselfを使います。上記の例ではself.public_ipでEIP(グローバルIP)を参照しています。

remote-execを使用したプロビジョニングについては、以下ブログエントリも合わせてご参照ください。

上で「EC2インスタンス作成時にはEIPが付与されていないため(インターネット経由でEC2に接続できないため)」と書きましたが、正確にはEC2の起動時に自動的にパブリックIPを割り当てることが可能なので、この自動割り当て設定を有効にしておけばEC2リソース定義内にプロビジョニングの定義を書くことができます。パブリックIPは自動的に割り当てない&EIPを付与する、という場合には、このワークアラウンドをお試しください。

OutputでEC2にアタッチされたEIPを出力したい

output定義ではcountは使えません。aws_eip.web.1.public_ipaws_eip.web.2.public_ip、、とインスタンス数分のoutput定義を書くのは面倒なので、ワークアラウンドとしてjoin関数を使います。

output "EIPs of Web Servers" {
  value = "${join(", ", aws_eip.web.*.public_ip)}"
}

joinでリストを結合して文字列に変換します(上記の例ではカンマ区切りの文字列に変換しています)。出力結果は以下のようになります。

EIP for Web Servers = 52.xxx.xxx.xxx, 52.xxx.xxx.xxx, 52.xxx.xxx.xxx, 52.xxx.xxx.xxx

まとめ

今回ご紹介したTIPSはもちろんEC2以外のリソース作成にも使えますので、tfファイルの最適化の一助になれば幸いです。

また、Githubで公開されているterraform-community-modulesを見ると、Terraformの組み込み関数の使いどころなど参考になるところが多いので、一度目を通しておくことをお勧めします。

参考