TerraformでVPCを管理するmoduleを作る

awsterraform

Terraform

$ brew install terraform
$ terraform -v
Terraform v0.9.11

Terraformの設定要素

provider

IaaS(e.g. AWS)、PaaS(e.g. Heroku)、SaaS(e.g. CloudFlare)など。

AWS Providerはこんな感じ。 ここに直接access_keyやsecret_keyを書くこともできるが、誤って公開されてしまわないように環境変数か variableで渡す。

provider "aws" {
  # access_key = "${var.access_key}"
  # secret_key = "${var.secret_key}"
  region = "us-east-1"
}
$ export AWS_ACCESS_KEY_ID="anaccesskey"
$ export AWS_SECRET_ACCESS_KEY="asecretkey"

varibale

CLIでオーバーライドできるパラメーター。typeにはstringのほかにmapやlistを渡すことができ、 何も渡さないとdefault値のものが、それもなければstringになる。

variable "key" {
  type    = "string"
  default = "value"
  description = "description"
}

値を渡す方法はTF_VAR_をprefixとする環境変数、-var、-var-fileがある。 また、moduleのinputとして渡されることもある。

$ export TF_VAR_somelist='["ami-abc123", "ami-bcd234"]'
$ terraform apply -var foo=bar -var foo=baz
$ terraform apply -var-file=foo.tfvars -var-file=bar.tfvars
$ cat foo.tfvars
foo = "bar"
xyz = "abc"

somelist = [
  "one",
  "two",
]

somemap = {
  foo = "bar"
  bax = "qux"
}

output

variableがinputなのに対して、こちらはoutput。

output "ip" {
  value = "${aws_eip.ip.public_ip}"
}

実行した後に取得できる。

$ terraform apply
...

$ terraform output ip
50.17.232.209

resource

物理サーバーやVMのような低レベルのものからDNSレコードのような高レベルのものまで含むインフラのコンポーネント。

resource "aws_instance" "web" {
  ami           = "ami-408c7f28"
  instance_type = "t1.micro"
}

provisioner

デフォルトでは作成されたときに実行されるコマンド。when = “destroy” で終了時に実行させることもできる。 on_failureで失敗したときの挙動を設定することができ、デフォルトはコマンド自体が失敗する"fail"になっている。

resource "aws_instance" "web" {
  # ...

  provisioner "local-exec" {
    command = "echo ${self.private_ip_address} > file.txt"
  }
}

data

情報を取得する。Terraform以外で作られたリソースのものも取れる。

data "aws_ami" "web" {
  filter {
    name   = "state"
    values = ["available"]
  }

  filter {
    name   = "tag:Component"
    values = ["web"]
  }

  most_recent = true
}

module

設定をまとめたもの。variableの値を渡すことができ、再利用することができる。 GitHubのurlをsourceに指定することもできる。最初に terraform get する必要がある。

module "assets_bucket" {
  source = "./publish_bucket"
  name   = "assets"
}

module "media_bucket" {
  source = "./publish_bucket"
  name   = "media"
}
# publish_bucket/bucket-and-cloudfront.tf
variable "name" {} # this is the input parameter of the module
...

backend

0.9.0から terraform remote の代わりに使われるようになったもの。 管理下のresourceと今の状態を表すtfstateファイルを各自のローカルではなくリモートで一元的に管理する。 オプションではあるが、applyしたあとにtfstateを上げるのを忘れたりするのを防ぐこともできるため 相当変わった用途でもない限り使わない理由がないと思う。最初に terraform init する必要がある。

S3に置く場合はこんな感じ。 DynamoDBでロックをかけられる。

terraform {
  backend "s3" {
    bucket = "mybucket"
    key    = "path/to/my/key"
    region = "us-east-1"
    dynamodb_table = "tflocktable"
  }
}

VPCのmoduleを作る

コードはここ

community-moduleにもVPCのモジュールがあるんだが、今回は自分で作ってみる。

variableはこんな感じ。同じファイルに書くこともできるが別に分けた方が見やすい。

variable "vpc_name" {
    description = "vpc's name, e.g. main-vpc"
}

variable "vpc_cidr_block" {
    description = "vpc's cidr block, e.g. 10.0.0.0/16"
}

variable "public_subnet_cidr_blocks" {
    type = "list"
    description = "public subnets' cidr blocks, e.g. [\"10.0.0.0/24\", \"10.0.1.0/24\"]"
}

variable "public_subnet_availability_zones" {
    type = "list"
    description = "public subnets' cidr blocks, e.g. [\"ap-northeast-1a\",\"ap-northeast-1c\"]"
}

variable "private_subnet_cidr_blocks" {
    type = "list"
    description = "private subnets' cidr blocks, e.g. [\"10.0.2.0/24\", \"10.0.3.0/24\"]"
}

variable "private_subnet_availability_zones" {
    type = "list"
    description = "public subnets' cidr blocks, e.g. [\"ap-northeast-1a\",\"ap-northeast-1c\"]"
}

まずVPCを作成する。

resource "aws_vpc" "vpc" {
    cidr_block = "${var.vpc_cidr_block}"
    enable_dns_hostnames = true
    tags {
        Name = "${var.vpc_name}"
    }
}

次にVPCにpublicとprivate用のサブネットを、 それぞれcidr_block分作成する。

vpc_idでvpcのresourceを参照している。したがって、これを実行するためには既にvpcが作られている必要がある。 depends_onで明示的に依存関係を示すこともできるのだが、 大抵はそうする必要がなくて暗黙的な依存関係をterraformが解決してくれる。 これはterraform graphで確認できる。

AZで使われているelement(list, index) は要素数以上のindexを渡してもmodの要領で選ぶので数を合わせなくてもよい。

複数作ったものは aws_subnet.public-subnet.0 のように0から始まるindexで参照でき、aws_subnet.public-subnet.*.id のようにすると要素のリストを得られる。

resource "aws_subnet" "public-subnet" {
    vpc_id = "${aws_vpc.vpc.id}"
    count = "${length(var.public_subnet_cidr_blocks)}"
    cidr_block = "${var.public_subnet_cidr_blocks[count.index]}"
    availability_zone = "${element(var.public_subnet_availability_zones, count.index)}"
    map_public_ip_on_launch = true
    tags {
        Name = "${var.vpc_name}-public-${element(var.public_subnet_availability_zones, count.index)}"
    }
}

resource "aws_subnet" "private-subnet" {
    vpc_id = "${aws_vpc.vpc.id}"
    count = "${length(var.private_subnet_cidr_blocks)}"
    cidr_block = "${var.private_subnet_cidr_blocks[count.index]}"
    availability_zone = "${element(var.private_subnet_availability_zones, count.index)}"
    tags {
        Name = "${var.vpc_name}-private-${element(var.private_subnet_availability_zones, count.index)}"
    }
}

Private IPアドレスとPublic IPアドレスを1:1でStatic NATするインターネットゲートウェイ をVPCにアタッチし、これを登録したカスタムルートテーブル をpublic用のサブネットに関連付ける。 これによりPublic IPを割り当てたインスタンスとインターネットが相互に接続できるようになる。

Public IPを割り当てるとコンソール上ではインスタンス自体にIPが紐づいているように見えるが、これはStatic NATのマッピング先で、 ifconfig 等には表示されない

resource "aws_internet_gateway" "igw" {
    vpc_id = "${aws_vpc.vpc.id}"
    tags {
        Name = "${var.vpc_name}-igw"
    }
}

resource "aws_route_table" "public-route-table" {
    vpc_id = "${aws_vpc.vpc.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igw.id}"
    }
    tags {
        Name = "${var.vpc_name}-public-route-table"
    }
}

resource "aws_route_table_association" "route-table-association" {
    count          = "${length(var.public_subnet_cidr_blocks)}"
    subnet_id      = "${element(aws_subnet.public-subnet.*.id, count.index)}"
    route_table_id = "${aws_route_table.public-route-table.id}"
}

privateのサブネットからはN:1のDynamic NATの後インターネットゲートウェイを通って外に出られるようにする。外から中には入れない。 publicなサブネットにそれ用のインスタンスを立ててもいいが、NATゲートウェイを使うと自分でメンテする必要がなくて楽。 料金は時間と通信量による。

ということで、EIPを割り当て、 適当なpublicのサブネットにNATゲートウェイを作成する。 ドキュメントに書いてある通り、明示的にigwを依存に入れている。

NATゲートウェイをメインルートテーブルに登録する。 これはAWSのドキュメントに書いてある通りの構成で、 明示的にルートテーブルと関連付けていないサブネットは メインルートテーブルに 関連付けられる

resource "aws_eip" "nat" {
    vpc = true
}

resource "aws_nat_gateway" "ngw" {
    allocation_id = "${aws_eip.nat.id}"
    subnet_id     = "${aws_subnet.public-subnet.0.id}"
    depends_on = ["aws_internet_gateway.igw"]
}

resource "aws_route_table" "main-route-table" {
    vpc_id = "${aws_vpc.vpc.id}"
    route {
        cidr_block = "0.0.0.0/0"
        nat_gateway_id = "${aws_nat_gateway.ngw.id}"
    }
    tags {
        Name = "${var.vpc_name}-main-route-table"
    }
}

resource "aws_main_route_table_association" "main-route-table-association" {
  vpc_id         = "${aws_vpc.vpc.id}"
  route_table_id = "${aws_route_table.main-route-table.id}"
}

実行

terraform {
  backend "s3" {
    bucket = "terraform"
    key    = "terraform.tfstate"
    region = "ap-northeast-1"
  }
}

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

module "test-vpc" {
  source                            = "./vpc"
  vpc_name                          = "test-vpc"
  vpc_cidr_block                    = "10.0.0.0/16"
  public_subnet_cidr_blocks         = ["10.0.0.0/24", "10.0.1.0/24"]
  public_subnet_availability_zones  = ["ap-northeast-1a", "ap-northeast-1c"]
  private_subnet_cidr_blocks        = ["10.0.2.0/24", "10.0.3.0/24"]
  private_subnet_availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
}

planして問題なければapplyする流れ。

$ terraform init
$ terraform get
$ terraform plan
+ module.test-vpc.aws_eip.nat
    allocation_id:     "<computed>"
    association_id:    "<computed>"
    domain:            "<computed>"
    instance:          "<computed>"
    network_interface: "<computed>"
    private_ip:        "<computed>"
    public_ip:         "<computed>"
    vpc:               "true"

+ module.test-vpc.aws_internet_gateway.igw
    tags.%:    "1"
    tags.Name: "test-vpc-igw"
    vpc_id:    "${aws_vpc.vpc.id}"

...
Plan: 13 to add, 0 to change, 0 to destroy.

$ terraform apply
...
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.

applyするとresourceが作成・更新され、tfstateファイルがbackendまたはローカルに出力される。 次回以降はこのtfstateとの差分を取って変更されるので、このファイルがないとまた同じものが作成されてしまう。

{
    "version": 3,
    "terraform_version": "0.9.11",
    "serial": 1,
    "lineage": "f97ad997-5a19-4a3d-9921-b553c5f2532b",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        },
        {
            "path": [
                "root",
                "test-vpc"
            ],
            "outputs": {},
            "resources": {
                "aws_eip.nat": {
                    "type": "aws_eip",
                    "depends_on": [],
                    "primary": {
                        "id": "eipalloc-3046f054",
                        "attributes": {
                            "association_id": "",
                            "domain": "vpc",
                            "id": "eipalloc-3046f054",
                            "instance": "",
                            "network_interface": "",
                            "private_ip": "",
                            "public_ip": "13.114.59.186",
                            "vpc": "true"
                        },
                        "meta": {},
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": ""
                },
                "aws_internet_gateway.igw": {
                    "type": "aws_internet_gateway",
                    "depends_on": [
                        "aws_vpc.vpc"
                    ],
                    "primary": {
                        "id": "igw-d2659bb6",
                        "attributes": {
                            "id": "igw-d2659bb6",
                            "tags.%": "1",
                            "tags.Name": "test-vpc-igw",
                            "vpc_id": "vpc-3cf6a358"
                        },
                        "meta": {},
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": ""
                },
                ...
            },
            "depends_on": []
        }
    ]
}

planすると変更なしになっている。

$ terraform plan
...
No changes. Infrastructure is up-to-date.

terafform destroy で管理下のresourceを消すことができる。

$ terraform plan -destroy
...
Plan: 0 to add, 0 to change, 13 to destroy.

$ terraform destroy
...
Destroy complete! Resources: 13 destroyed.

$ terraform plan
...
Plan: 13 to add, 0 to change, 0 to destroy.

参考

お金をかけずに、TerraformでAWSのVPC環境を準備する - Qiita