Terraformは、あらかじめインフラ構成を設定ファイルに記述して、クラウド環境に適用・管理するツールです。
Vagrantなどを開発しているHashiCorpのツールになります。
AWSだけではなく様々なプロバイダに対応していますが、AWSで使用してみました。
インストール
Linuxサーバに入れてみました。
ダウンロードしてきて展開するだけで使えます。
1cd /opt
2mkdir terraform
3cd terraform
4wget https://releases.hashicorp.com/terraform/0.6.16/terraform_0.6.16_linux_amd64.zip
5unzip terraform_0.6.16_linux_amd64.zip
パスを通すと便利でした。
vi ~/.bashrc
1export PATH=/opt/terraform:$PATH
AWS側事前準備
- AWSアカウント作成
- terraformで触るリソースをいじれるポリシーを付与したIAMユーザを作成
terraformの動作イメージ
必ず使うコマンドは以下の三つ
terraform plan
dry-runです。設定ファイルの記述ミスなどをチェックしてくれます
terraform apply
実行です。設定ファイル通りの構成を実装してくれます
terraform destroy
作成した構成を削除してくれます
terraform初期設定
まずはAWSアクセスキーなどの設定。
terraformは「terraform.tfvars」というファイルがカレントディレクトリにあると
そちら中を読んで変数として取り扱ってくれます。
注意点として、このファイルでは配列は設定できないようです。
このようにアクセスキーなどを変数として設定しておく事ができます。
file : terraform.tfvars
1#####################################
2#Variable Settings
3#####################################
4#AWS Settings
5access_key = "xxxxxxxxxxxxxxxxxxx"
6secret_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
7region = "ap-northeast-1"
このファイルは.gitignoreとかでリポジトリでは管理しないようにします。
次のこの変数をtfファイル内で使用できるようにvariableで定義します。
上記のterraform.tfvarsの記述内容は環境変数に入るので、それを定義します。
file : variables.tf
1#####################################
2# Variable Settings
3#####################################
4#AWS Settings
5variable "access_key" {}
6variable "secret_key" {}
7variable "region" {}
上記で設定した変数を使ってterraformのAWS設定を記述します
file : aws.tf
1provider "aws" {
2access_key = "${var.access_key}"
3secret_key = "${var.secret_key}"
4region = "${var.region}"
5}
以上で初期設定は終わり。
terraform planやterraform applyをするとカレントにある *.tf ファイルをすべて読み込んでくれます。
また、一度applyを実行すると、terraform.tfstateというファイルが作成されます。
terraform.tfstate は、Terraform が管理しているリソースの状態が保存されているのだと思います。
おそらく次回実行時にこちらのstateファイルとの差分でどのようなAPIを投げるか決定しているのだと思います。
VPCを作成してみる
実際にいろいろ設定をしてみたので紹介です。
以下は
- VPCの作成
- サブネット二つ作成(いずれもパグリックサブネット)
- DHCPオプションの作成
- インターネットゲートウェイ作成
- ルーティングテーブル作成
を行っています。
file aws_vpc.tf
1#####################################
2# VPC Settings
3#####################################
4resource "aws_vpc" "vpc_main" {
5cidr_block = "${var.root_segment}"
6enable_dns_support = true
7enable_dns_hostnames = true
8tags {
9Name = "${var.app_name}"
10}
11}
12#####################################
13# DHCP option sets
14#####################################
15resource "aws_vpc_dhcp_options" "vpc_main-dhcp" {
16domain_name = "${var.dhcp_option_domain_name}"
17domain_name_servers = ["AmazonProvidedDNS"]
18tags {
19Name = "${var.app_name} DHCP"
20}
21}
22resource "aws_vpc_dhcp_options_association" "vpc_main-dhcp-association" {
23vpc_id = "${aws_vpc.vpc_main.id}"
24dhcp_options_id = "${aws_vpc_dhcp_options.vpc_main-dhcp.id}"
25}
26#####################################
27# Internet Gateway Settings
28#####################################
29resource "aws_internet_gateway" "vpc_main-igw" {
30vpc_id = "${aws_vpc.vpc_main.id}"
31tags {
32Name = "${var.app_name} igw"
33}
34}
35#####################################
36# Public Subnets Settings
37#####################################
38resource "aws_subnet" "vpc_main-public-subnet1" {
39vpc_id = "${aws_vpc.vpc_main.id}"
40cidr_block = "${var.public_segment1}"
41availability_zone = "${var.public_segment1_az}"
42tags {
43Name = "${var.app_name} public-subnet1"
44}
45}
46resource "aws_subnet" "vpc_main-public-subnet2" {
47vpc_id = "${aws_vpc.vpc_main.id}"
48cidr_block = "${var.public_segment2}"
49availability_zone = "${var.public_segment2_az}"
50tags {
51Name = "${var.app_name} public-subnet2"
52}
53}
54#####################################
55# Routes Table Settings
56#####################################
57resource "aws_route_table" "vpc_main-public-rt" {
58vpc_id = "${aws_vpc.vpc_main.id}"
59route {
60cidr_block = "0.0.0.0/0"
61gateway_id = "${aws_internet_gateway.vpc_main-igw.id}"
62}
63tags {
64Name = "${var.app_name} public-rt"
65}
66}
67resource "aws_route_table_association" "vpc_main-rta1" {
68subnet_id = "${aws_subnet.vpc_main-public-subnet1.id}"
69route_table_id = "${aws_route_table.vpc_main-public-rt.id}"
70}
71resource "aws_route_table_association" "vpc_main-rta2" {
72subnet_id = "${aws_subnet.vpc_main-public-subnet2.id}"
73route_table_id = "${aws_route_table.vpc_main-public-rt.id}"
74}
この例の通り、作成したリソースのIDなどは他のリソースで読み込む事が可能です。
一番上で作成したVPCのIDは以下のように取得できます。
${aws_vpc.vpc_main.id}
セキュリティグループを作成してみる
特筆するところはないですが、ソース元をセキュリティグループにする例も載せてみました。
ソース元を配列にする事で複数の設定ができます。
terraform.tfvarsに配列が指定できたらいいんですが・・・
1resource "aws_security_group" "lb_sg" {
2name = "${var.app_name} Load Balancer"
3vpc_id = "${aws_vpc.vpc_main.id}"
4ingress {
5from_port = 80
6to_port = 80
7protocol = "tcp"
8cidr_blocks = ["0.0.0.0/0"]
9}
10egress {
11from_port = 0
12to_port = 0
13protocol = "-1"
14cidr_blocks = ["0.0.0.0/0"]
15}
16description = "${var.app_name} Load Balancer"
17}
18resource "aws_security_group" "web_sg" {
19name = "${var.app_name} WEB Server"
20vpc_id = "${aws_vpc.vpc_main.id}"
21ingress {
22from_port = 22
23to_port = 22
24protocol = "tcp"
25cidr_blocks = ["${var.root_segment}"]
26}
27ingress {
28from_port = 80
29to_port = 80
30protocol = "tcp"
31cidr_blocks = ["${var.root_segment}"]
32security_groups = ["${aws_security_group.lb_sg.id}"]
33}
34egress {
35from_port = 0
36to_port = 0
37protocol = "-1"
38cidr_blocks = ["0.0.0.0/0"]
39}
40description = "${var.app_name} WEB Server"
41}
RDSの作成
RDS用のサブネットグループを作成する部分が少しはまりました
1resource "aws_db_instance" "default" {
2allocated_storage = 5
3identifier = "${var.rds_name}"
4engine = "${var.rds_engine}"
5engine_version = "${var.rds_engine_version}"
6instance_class = "${var.rds_instane_type}"
7name = "${var.rds_db_name}"
8username = "${var.rds_user}"
9password = "${var.rds_pass}"
10db_subnet_group_name = "${aws_db_subnet_group.default.id}"
11parameter_group_name = "${var.rds_parameter_group}"
12vpc_security_group_ids = ["${aws_security_group.mysql_sg.id}"]
13backup_retention_period = "1"
14backup_window = "17:08-17:38"
15storage_type = "gp2"
16maintenance_window = "Sat:13:38-Sat:14:08"
17multi_az = false
18apply_immediately = true
19publicly_accessible = true
20}
21resource "aws_db_subnet_group" "default" {
22name = "default-${aws_vpc.vpc_main.id}"
23description = "Our main group of subnets"
24subnet_ids = ["${aws_subnet.vpc_main-public-subnet1.id}","${aws_subnet.vpc_main-public-subnet2.id}"]
25}
s3の作成
ポリシーを設定する場合は外部に用意したポリシーのjsonを読み込ませて設定してみました。
1resource "aws_s3_bucket" "b" {
2bucket = "${var.s3_bucket_name}"
3acl = "private"
4policy = "${file("s3_bucket_policy.json")}"
5}
elbの作成
1# Create a new load balancer
2resource "aws_elb" "default" {
3name = "${var.elb_name}"
4security_groups = ["${aws_security_group.lb_sg.id}"]
5subnets = ["${aws_subnet.vpc_main-public-subnet1.id}","${aws_subnet.vpc_main-public-subnet2.id}"]
6listener {
7instance_port = 80
8instance_protocol = "http"
9lb_port = 80
10lb_protocol = "http"
11}
12health_check {
13healthy_threshold = 2
14unhealthy_threshold = 2
15timeout = 5
16target = "HTTP:80/index.html"
17interval = 30
18}
19}
dynamoDBのテーブル作成
range_keyがソートキーになります
1resource "aws_dynamodb_table" "users" {
2name = "${var.dynamodb_prefix_name}.users"
3read_capacity = 1
4write_capacity = 1
5hash_key = "id"
6range_key = "username"
7attribute {
8name = "id"
9type = "N"
10}
11attribute {
12name = "username"
13type = "S"
14}
15}
Nが数値、Sが文字列、Bがバイナリです。
elasticacheの作成
redisを作成してみた
1resource "aws_elasticache_cluster" "redis" {
2cluster_id = "${var.cache_cluster_name}"
3engine = "redis"
4node_type = "${var.cache_instane_type}"
5port = 6379
6num_cache_nodes = 1
7parameter_group_name = "${var.cache_parameter_group}"
8security_group_ids = ["${aws_security_group.redis_sg.id}"]
9subnet_group_name = "${aws_elasticache_subnet_group.subnet.id}"
10}
11resource "aws_elasticache_subnet_group" "subnet" {
12name = "${var.cache_cluster_name}"
13description = "${var.cache_cluster_name}"
14subnet_ids = ["${aws_subnet.vpc_main-public-subnet1.id}","${aws_subnet.vpc_main-public-subnet2.id}"]
15}
IAMロールの作成
いろいろ躓きました・・・。
IAMロールの作成の場合、
- ロールの作成
- 管理ポリシーをアタッチ
- インラインポリシーをアタッチ
という形なのですが、terraformではポリシーのリソース作成でIMAロールを指定します。
また、IAMロール作成時、「信頼されたエンティティ」のID プロバイダーとして
amazonを追加しないと使えないところがはまりました。(普段意識していなかったので・・・)
IAMロールの作成。
信頼されたエンティティにec2.amazonaws.comを設定しています。
1resource "aws_iam_role" "sample_role" {
2name = "${var.sample_iam_role_name}"
3assume_role_policy = <<EOF
4{
5"Version": "2012-10-17",
6"Statement": [
7{
8"Sid": "",
9"Effect": "Allow",
10"Principal": {
11"Service": "ec2.amazonaws.com"
12},
13"Action": "sts:AssumeRole"
14}
15]
16}
17EOF
18}
作成したロールに管理ポリシーをアタッチ
管理ポリシーの場合はポリシーarnを直接記述します。
1resource "aws_iam_policy_attachment" "dynamodb" {
2name = "AmazonDynamoDBFullAccess"
3roles = ["${aws_iam_role.sample_role.id}"]
4policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
5}
作成したロールにインラインポリシーをアタッチ
インラインポリシーの場合はpolicyのjsonを直接記述します
1resource "aws_iam_role_policy" "sqs_policy" {
2name = "AmazonSQSFullAccess"
3role = "${aws_iam_role.sample_role.id}"
4policy = <<EOF
5{
6"Version": "2012-10-17",
7"Statement": [
8{
9"Action": [
10"sqs:*"
11],
12"Effect": "Allow",
13"Resource": "*"
14}
15]
16}
17EOF
18}
EC2インスタンスの作成
作成したIAMロールを関連付けたインスタンスを作成します。
ついでに固定IPもつけてみました。
インスタンスに関連づける場合は「aws_iam_instance_profile」でまず
インスタンスプロフィールを作成する必要があるようです。
こちらのリソースで作成したnameの値をaws_instanceのiam_instance_profileで指定すれば
うまくいきました。
1resource "aws_iam_instance_profile" "sample_role" {
2name = "sample_role"
3roles = ["${aws_iam_role.sample_role.name}"]
4}
5resource "aws_instance" "sample" {
6ami = "${var.sample_ami}"
7instance_type = "t2.micro"
8vpc_security_group_ids = ["${aws_security_group.sample_sg.id}"]
9subnet_id = "${aws_subnet.vpc_main-public-subnet1.id}"
10iam_instance_profile = "sample_role"
11associate_public_ip_address = true
12key_name = "${var.key_pair_name}"
13tags {
14Name = "${var.sample_tag_name}"
15}
16root_block_device {
17delete_on_termination = "true"
18}
19}
20# 固定IPもつけてみる
21resource "aws_eip" "lb" {
22instance = "${aws_instance.sample.id}"
23vpc = true
24}
AutoScalingの設定
こちらはあまりうまくいってないので使ってないですが、うまくいかなかったりうまくいったりします。
1resource "aws_launch_configuration" "as_conf" {
2name_prefix = "${var.launch_config_name}"
3image_id = "${var.sample_ami}"
4instance_type = "t2.micro"
5iam_instance_profile = "api_role"
6security_groups = ["${aws_security_group.sample_sg.id}"]
7key_name = "${var.key_pair_name}"
8associate_public_ip_address = true
9lifecycle {
10create_before_destroy = true
11}
12root_block_device {
13volume_type = "gp2"
14volume_size = "24"
15delete_on_termination = true
16}
17}
18resource "aws_autoscaling_group" "sample" {
19name = "${var.auto_scaling_group_name}"
20vpc_zone_identifier = ["${aws_subnet.vpc_main-public-subnet1.id}","${aws_subnet.vpc_main-public-subnet2.id}"]
21launch_configuration = "${aws_launch_configuration.as_conf.name}"
22max_size = 0
23min_size = 0
24desired_capacity = 0
25default_cooldown = 300
26health_check_grace_period = 300
27health_check_type = "EC2"
28load_balancers = ["${aws_elb.default.name}"]
29force_delete = true
30tag {
31key = "Name"
32value = "${var.api_tag_name}"
33propagate_at_launch = true
34}
35}
root_block_deviceやelb_block_deviceを使うと以下のようなエラーが出てしまう為、現在はあまり使用していません。
1Error refreshing state: 1 error(s) occurred:
2* aws_launch_configuration.as_conf: Invalid address to set: []string{"root_block_device", "0", "encrypted"}
こちら、お分かりにな方がいたらご教授頂きたいです・・・
怖いところ
「割と軽微な修正」だと思って修正すると
インスタンスが削除されたりする可能性があります。
例えばインスタンスのIAMロールを変更はインスタンスの立ち上げ時にしか変更できないのですが、terraformでIAMロールを変更してterraform applyすると、インスタンスは削除されて新規作成される、、
という動作となります。
これはどう考えてもterraformが正しい動作をしているのですが、本番環境などでterraform applyを実行するのは怖すぎるな・・・と思いました。
個人的なTerraformの使いどころ
おなじような機能にAWS公式サービスのCloudFormationがありますが、構成を管理してしまうところが嫌だと感じていました。
つまり初回構築だけ実行したい、、、という事ができないわけです。
もちろんTerraformにも構成管理をする機能はありますが、プロバイダ非依存である為、このあたりは柔軟に対応できます。
あと設定ファイルがjsonではないのでコメントなども書けるのもいいですね。
InteliJにはterraform用のプラグインもありますので
設定ファイル作成もしやすいです。
というわけで個人的には、「初期構築用ツール」、もしくは「開発環境構築ツール」として利用していこうと思います。
destroyでサクッと全部消えるのはとても便利。
逆に構成を管理させたい場合は、WEB GUIにて変数をフォームで渡して実行、、って事ができるので、やはりCloudFormationの方がいいかなぁと感じています。
(AutoScaleはCloudFormationを使う事が多いです)