ゆるふわカウンターアタック

Qiitaっぽい時はQiitaで、slideshareっぽい時はslideshareで、preziっぽい時はpreziで、ブログっぽい時はここで

Terraformを小さく初めてみよう

そもそもTerraformって?

AWSやCGPやAzureなどなどに対応したマルチクラウドなIaaS構築をコード化できるもの

Terraformをいじってみる

Terraform歴はざっと2日のペーペーですが、本番で小さく始められそうなのでその備忘禄。
環境
・Terraform v0.6.9
・CentOS6.6
※インストールはいたるところで書かれているので割愛します。簡単です。 安定のClassmethodさんブログ参考にしました↓

AWSでTerraformに入門 | Developers.IO

ここら辺で使ってみる

うちは小さいサービスが次々生まれるので、都度VPC作ってSubnetをAZごとに作ってRouteTable作って監視サーバーいるVPCとPeering張ってそこからの通信をSecurityGroupで開けたりとか・・
この辺いつも同じ構成なわけで、もうやりたかない。。
が、手でポチポチやってたので人に引き継ぎやすいジョブにしないと、、
マネコンのキャプチャ取って手順書くとかないし、やはりAWSだしCloudFormationか?でも実行時しかエラー判断できないしjsonだしコメント入れられないから適当にやっといてと言いづらい・・

こんな感じを作る↓

f:id:pioho:20160113193552p:plain

AnsibleとSpinnakerは試してみた

Ansibleは既に使ってたので、AnsibleのAWSモジュールでなんとか出来たらいいなぁと思ってました(これで済めば1番やっといてと言いやすいw)。ただ半年くらい前はPeering作れなかったり、R53のプライベートゾーンにVPCアタッチできなかったり(これはAnsible2.0で出来るみたい)なので片手落ちなものが出来るのでやめときました。

http://docs.ansible.com/ansible/route53_module.html

次はNetflixがSpinnakerというOSSを出したので試しました。UIはリッチでマルチクラウドで良さそうでしたが結構重い、、キャッシュにクセがありメンバーもはまるかなぁってのと裏でCassandraを使っているらしくCassandraにトラウマがあったのでやめました;

遠回りしましたが、じゃあTerraformでも試すべかと。
いい感じに熟してきてると思います。情報も多く改善もされまくってますね。

よいなぁ~と思った点

  • terraform plan xxx.tfでドライラン出来るので慣れてない人にも優しい
  • HCL(Hashicorp Configuration Language)が可読性高く慣れてなくても読める
  • AnsibleよりCloudリソース多い(そりゃそうだよね)
  • 依存関係は自動でよろしくやってくれる(depends_onで明示的にもできる)
  • かる~い
  • terraform graph | dot -Tpng > ./graph.pngでグラフが作れる。こんな感じ↓ リソースの量が増えればカオスなマップになるでしょうねきっと^^;
    ※実際に下の方に張ったテンプレートの実行結果をグラフ化しました。
    ※事前にsudo yum install -y graphviz graphviz-gdしておく

f:id:pioho:20160113151945p:plain

  • crash.logが出る。1回クラッシュしたけどこんな感じ出てくれた↓
[root@tfsvr defaultvpc]# tail crash.log
2016/01/11 21:44:13 [DEBUG] vertex provider.aws (close), got dep: aws_route.r_default2
2016/01/11 21:44:13 [DEBUG] vertex provider.aws (close), got dep: aws_route.r_default3
2016/01/11 21:44:13 [DEBUG] vertex root, got dep: provider.aws (close)
2016/01/11 21:44:13 [DEBUG] vertex root, got dep: provisioner.local-exec
2016/01/11 21:44:13 [DEBUG] vertex root, got dep: provisioner.remote-exec
2016/01/11 21:44:13 [DEBUG] waiting for all plugin processes to complete...
2016/01/11 21:44:13 [DEBUG] /usr/local/src/terraform_0.6.9/terraform-provisioner-remote-exec: plugin process exited
2016/01/11 21:44:13 [DEBUG] /usr/local/src/terraform_0.6.9/terraform-provisioner-local-exec: plugin process exited
2016/01/11 21:44:13 [DEBUG] /usr/local/src/terraform_0.6.9/terraform-provisioner-chef: plugin process exited
2016/01/11 21:44:13 [DEBUG] /usr/local/src/terraform_0.6.9/terraform-provisioner-file: plugin process exited

むむむって思った点

  • 既存環境にはテンプレート当てれない(多分)。
    ステート持つのでステートをファイルにエクスポートしてからテンプレート起こすみたなことが出来そうな気がしないでもないですが今後に期待
  • 冪等性はない!?のかな
    冪等性があるとも謳ってないけど。
    そもそもイケてないテンプレートの書き方をしたのですが、ルーティングテーブルを作成する"aws_route_table"でルートAを追加し、ルーティングテーブルエントリを作成する"aws_route"で同じルートテーブルIDに対してルートBを追加した場合

    ・まずドライランは成功します。
    ・1回目はaws_route_tableで追加されるルートAとaws_routeのルートBが両方追加されたルートテーブルができます。
    ・2回目はaws_routeに書かれたルートBがaws_route_tableにはないので削除されました。(毎回aws_route_tableの後にaws_routeが読まれればいいのですが違うのかな?)
    ・3回目は1回目と同じく全てのルートAもルートBも追加されます。あとは繰り返しですが何度やっても同じ構成に収束はしないですね;
    ※結局はaws_route_tableにルートAもルートBも書くべきってだけの話ですが、できれば標準のTerraformリソース使ってる範囲ではエラーで怒られたい

まずは小さく始めよう^^

とりあえずは、冒頭に戻って最初の想いを忘れずに、使い切りでいいので"AWSあるある構成"を誰がやっっても同じになる。そして確認も簡単にできればOK。
以下のようなテンプレートを作りました。

VPC 1
・Subnet
2(AZごと)
・SecurityGroup 2(EC2用、ELB用)
・ELB
1
・EC2 2(MultiAZ、ELB追加済)
・InternetGateWay
1
・RouteTable 1
・VPCPeering
1
・既存RouteTableにルート追加
・既存SecurityGroupにルール追加
・既存R53にVPCアタッチ
※鍵は鍵を配置したOSをAMIにしてしまってるのでTerraformでは作ってません。ただ作れます。

変数はファイルで外出しし、変数だけ変えて汎用的に使えるようにしました(引数で渡したり環境変数に持たせたりもできます)。-var-fileで変数ファイルを食わせます。
applyで実行ですがテンプレートファイルとなるxxx.tfファイルを指定しないとカレントディレクトリのxxx.tfファイルを勝手に読み込みます。テンプレートファイルは分割もOKです。
用途別にディレクトリを分けるのがいいかと思います。

[root@tfsvr defaultvpc]# pwd
/usr/local/src/terraform/defaultvpc
[root@tfsvr defaultvpc]# terraform apply default.tf -var-file /root/terraform.tfvars

"xxxx"でマスクしているところはご自身の環境に合わせた値を入れて、"example"を"サービス名"に置換してもらえば使えるかと。多分・・
作成も削除もだいたい3分弱でできました
SGとかEC2のオプションやIPは一例ですので、それぞれポリシーに合わせて適宜変えてもらえればと
何かあればコメントください><

[root@tfsvr defaultvpc]# cat /root/terraform.tfvars
##
## よくあるAWS構成Template
##   1vpc,2subnet(multiaz),igw,routetable,peering*2,securitygroup*2,ec2*2,elb
##
## usage:
##   exampleをサービス名に変更
##   (例:service1のInternetGatewayならexamplegw ==> service1gw)
##   IPアドレスをサービスに割り当てたものに変更
##   (例:service1のVPCならネットワークアドレスを172.1.0.0/16 == > 172.2.0.0/16)
##
##

access_key = "xxxxxxxxxxx"
secret_key = "xxxxxxxxxxx"

### AWS
region_oregon = "us-west-2"
az_oregon1 = "us-west-2a"
az_oregon2 = "us-west-2b"
##region_tokyo = "ap-northeast-1"
##az_tokyo1 = "ap-northeast-1a"
##az_tokyo2 = "ap-northeast-1c"
awsid_example = "xxxxxxxxxxx"

### IGW
igw_example = "example"

### VPC
vpc_example = "example"
ciderblock_example = "172.1.0.0/16"
dhcpoptions_example = "dopt-xxxxxxxxxxx"
vpc_kanshi ="vpc-xxxxxxxxxxx"
vpcpeering_example1 = "kanshi--example"

### Subnet
subnet_example1 = "example-2a"
subnet_example2 = "example-2b"
ciderblock_example1 = "172.1.0.0/24"
ciderblock_example2 = "172.1.1.0/24"

### RouteTable
rt_example = "example"
ciderblock_any = "0.0.0.0/0"
ciderblock_kanshi = "xxxxxxxxxxx/16"
rt_kanshi ="rtb-xxxxxxxxxxx"

### SG
sg_example1 = "example-web"
sg_example2 = "example-lb"
ciderblock_office1 = "xxxxxxxxxxx/32"
sg_zabbix = "sg-xxxxxxxxxxx"

### EC2
instancetype_example = "t2.micro"
ec2_example1 = "example-web01"
ec2_example2 = "example-web02"

### ELB
elb_example = "example-weblb"

### R53
route53_privatezone = "xxxxxxxxxxx"
[root@tfsvr defaultvpc]# cat default.tf
variable "access_key" {}
variable "secret_key" {}

#variable "region_tokyo" {}
variable "region_oregon" {}
variable "igw_example" {}
variable "vpc_example" {}
variable "subnet_example1" {}
variable "subnet_example2" {}
variable "instancetype_example" {}
variable "ciderblock_example" {}
variable "ciderblock_example1" {}
variable "ciderblock_example2" {}
variable "rt_example" {}
#variable "az_tokyo1" {}
#variable "az_tokyo2" {}
variable "az_oregon1" {}
variable "az_oregon2" {}
variable "ciderblock_any" {}
variable "elb_example" {}
variable "ec2_example1" {}
variable "ec2_example2" {}
variable "dhcpoptions_example" {}
variable "vpc_kanshi" {}
variable "vpcpeering_example1" {}
variable "vpcpeering_example2" {}
variable "awsid_example" {}
variable "ciderblock_kanshi" {}
variable "rt_kanshi" {}
variable "sg_example1" {}
variable "sg_example2" {}
variable "sg_kanshizabbix" {}
variable "ciderblock_office1" {}
variable "route53_privatezone" {}

variable "images" {
    default = {
        us-east-1 = ""
        us-west-2 = "ami-xxxxxxxx"
        us-west-1 = ""
        eu-west-1 = ""
        eu-central-1 = ""
        ap-southeast-1 = ""
        ap-southeast-2 = ""
        ap-northeast-1 = ""
    }
}

provider "aws" {
    access_key = "${var.access_key}"
    secret_key = "${var.secret_key}"
    region = "${var.region_oregon}"
}


### IGW

resource "aws_internet_gateway" "defaultgw" {
    vpc_id = "${aws_vpc.defaultvpc.id}"

    tags {
        Name = "${var.igw_example}"
    }
}


### VPC

resource "aws_vpc" "defaultvpc" {
    cidr_block = "${var.ciderblock_example}"
    enable_dns_hostnames = true
    tags {
        Name = "${var.vpc_example}"
    }
}

### DHCP Option

resource "aws_vpc_dhcp_options_association" "defaultdns" {
    vpc_id = "${aws_vpc.defaultvpc.id}"
    dhcp_options_id = "${var.dhcpoptions_example}"
}


### R53 VPC Add

resource "aws_route53_zone_association" "private" {
  zone_id = "${var.route53_privatezone}"
  vpc_id = "${aws_vpc.defaultvpc.id}"
}


### Subnet

resource "aws_subnet" "defaultsubnet1" {
    vpc_id = "${aws_vpc.defaultvpc.id}"
    cidr_block = "${var.ciderblock_example1}"
    availability_zone = "${var.az_oregon1}"

    tags {
        Name = "${var.subnet_example1}"
    }
}

resource "aws_subnet" "defaultsubnet2" {
    vpc_id = "${aws_vpc.defaultvpc.id}"
    cidr_block = "${var.ciderblock_example2}"
    availability_zone = "${var.az_oregon2}"

    tags {
        Name = "${var.subnet_example2}"
    }
}

### Route

resource "aws_route" "r_kanshi" {
    route_table_id = "${var.rt_kanshi}"
    destination_cidr_block = "${var.ciderblock_example}"
    vpc_peering_connection_id = "${aws_vpc_peering_connection.kanshipeering.id}"
}


### RouteMainTable association

resource "aws_main_route_table_association" "a" {
    vpc_id = "${aws_vpc.defaultvpc.id}"
    route_table_id = "${aws_route_table.r.id}"
}


### RouteTable

resource "aws_route_table" "r" {
    vpc_id = "${aws_vpc.defaultvpc.id}"
    route {
        cidr_block = "${var.ciderblock_any}"
        gateway_id = "${aws_internet_gateway.defaultgw.id}"
    }
    route {
        cidr_block = "${var.ciderblock_kanshi}"
        vpc_peering_connection_id = "${aws_vpc_peering_connection.kanshipeering.id}"
    }

    tags {
        Name = "${var.rt_example}"
    }
}


### VPC Peering

resource "aws_vpc_peering_connection" "kanshipeering" {
  peer_owner_id = "${var.awsid_example}"
  peer_vpc_id = "${aws_vpc.defaultvpc.id}"
  vpc_id = "${var.vpc_kanshi}"
  auto_accept = true
    tags {
        Name = "${var.vpcpeering_example1}"
    }
}

### Instance

resource "aws_instance" "defaultinstance1" {
    ami = "${var.images.us-west-2}"
    availability_zone = "${var.az_oregon1}"
    instance_type = "${var.instancetype_example}"
    subnet_id = "${aws_subnet.defaultsubnet1.id}"
    security_groups = ["${aws_security_group.defaultec2sg.id}"]
    associate_public_ip_address = "true"
    root_block_device = {
      volume_type = "gp2"
      volume_size = "20"
    }

    tags {
        Name = "${var.ec2_example1}"
    }
}

resource "aws_instance" "defaultinstance2" {
    ami = "${var.images.us-west-2}"
    availability_zone = "${var.az_oregon2}"
    instance_type = "${var.instancetype_example}"
    subnet_id = "${aws_subnet.defaultsubnet2.id}"
    security_groups = ["${aws_security_group.defaultec2sg.id}"]
    associate_public_ip_address = "true"
    root_block_device = {
      volume_type = "gp2"
      volume_size = "20"
    }

    tags {
        Name = "${var.ec2_example2}"
    }
}


### SecurityGroup

resource "aws_security_group" "defaultec2sg" {
    name = "exampleec2sg"
    description = "Used in the terraform"
    vpc_id = "${aws_vpc.defaultvpc.id}"

    # kanshi access from anywhere
    ingress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["${var.ciderblock_kanshi}"]
    }

    # office access from anywhere
    ingress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["${var.ciderblock_office1}"]
    }

    # elb HTTP access from anywhere
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        security_groups = ["${aws_security_group.defaultelbsg.id}"]
    }

    tags {
        Name = "${var.sg_example1}"
    }
}


resource "aws_security_group" "defaultelbsg" {
    name = "exampleelbsg"
    description = "Used in the terraform"
    vpc_id = "${aws_vpc.defaultvpc.id}"

    # office HTTP access from anywhere
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["${var.ciderblock_office1}"]
    }

    tags {
        Name = "${var.sg_example2}"
    }
}

resource "aws_security_group_rule" "kanshizabbixsg" {
    type = "ingress"
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["${var.ciderblock_example}"]

    security_group_id = "${var.sg_kanshizabbix}"
}


### ELB

resource "aws_elb" "defaultelb" {
  name = "${var.elb_example}"

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

 health_check {
    healthy_threshold = 2
    unhealthy_threshold = 2
    timeout = 5
    target = "HTTP:80/"
    interval = 10
  }

  # The instance is registered automatically
  instances = ["${aws_instance.defaultinstance1.id}","${aws_instance.defaultinstance2.id}"]
  subnets = ["${aws_subnet.defaultsubnet1.id}","${aws_subnet.defaultsubnet2.id}"]
  security_groups =  ["${aws_security_group.defaultelbsg.id}"]
  cross_zone_load_balancing = true
}

所感

なにやるにもTerraformでやるようになってしまった。もうぽちぽちする気が起きなくなった。