フューチャー技術ブログ

はじめてのTerraform 0.12 ~実践編~

はじめに

こんにちはー
TIG DXチーム 1のゆるふわエンジニアの前原です。

前回のはじめてのTerraform 0.12 ~環境構築~に続き実践編です。実際の構築を通して、最近バージョンアップしたTerraform 0.12の構文がこんな感じで変わったよー的な話を伝えていければと思っています。

では、Terraformを用いてAWSのリソースを作成していきましょう。

構築する環境構成図

下図の環境(VPC, VPC Endpoint, NAT Gatewayなど)をTerraformで構築していきます。

  • VPC

VPCは、Staging(stg)とProduction(prd)に二つのVPCを構成します。AZ(Availability Zone)は3つのゾーンを利用し、サブネットはパブリックとプライベートに分けて構成します。

  • VPC Endpoint

Endpointとして、S3をセットします。

  • NAT Gateway

パブリックプライベートにNAT Gatewayを構築します。コストを抑えるために1台とします。

Terraformのディレクトリ構成

最終的に以下のディレクトリ構成になります。前回の記事でお伝えしたように、1つのディレクトリにtfファイルを配置する設計にします。

.
├── backend.tf // 前回の記事で説明
├── provider.tf // 同上
├── versions.tf // 同上
|
├── eip.tf
├── igw.tf
├── nat_gateway.tf
├── route.tf
├── route_association.tf
├── route_table.tf
├── subnet.tf
├── variable.tf
├── vpc.tf
└── vpc_endpoint.tf

VPCの構築

ここでは前回の記事で作成したbackend.tfなど以外のtfファイルを作成します。
基本的にリソース単位でファイルを分けておりますが、好みでひとまとめにしても問題ありません。

VPCリソースの定義

VPCを構築するためのVPCリソースを定義します。
このリソースで必須の項目は、cidr_blockのみですが、タグを付与したいため記述します。

vpc.tf
resource "aws_vpc" "vpc" {
cidr_block = local.vpc_cidr[terraform.workspace]

tags = {
Name = "${terraform.workspace}-${local.project_name}"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

vpc.tfは、ローカル変数を呼び出しているので、以下のようにvariable.tfに定義する必要があります。
また、variable.tfは、変数を定義するため今後も追記していきますので、これが最終的な内容でありませんのでご注意ください。

variable.tf
locals {
project_name = "example"

vpc_cidr = {
stg = "10.0.0.0/16"
prd = "10.1.0.0/16"
}
}

準備ができたのでTerraformを実行します。
が、実行する前に構文や設定に問題がないかを確認するためのコマンドを実行します。

terraform validate

構文に問題ないかをvalidateコマンドで確認します。
問題なければSuccessと出力されます。

$ terraform validate
Success! The configuration is valid.

terraform fmt

次にterraform fmtというインデントなどのスタイルを揃えるコマンドを実行します。

$ terraform fmt

terraform plan

それでは、設定に問題ないかを確認します。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# aws_vpc.vpc will be created
+ resource "aws_vpc" "vpc" {
+ arn = (known after apply)
+ assign_generated_ipv6_cidr_block = false
+ cidr_block = "10.0.0.0/16"
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_classiclink = (known after apply)
+ enable_classiclink_dns_support = (known after apply)
+ enable_dns_hostnames = (known after apply)
+ enable_dns_support = true
+ id = (known after apply)
+ instance_tenancy = "default"
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Env" = "stg"
+ "Name" = "stg-example"
+ "Project" = "example"
}
}

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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

terraform apply

最後にapplyを実行し、Apply complete!と出力されたら完了です。

$ terraform apply
### 以下の質問が出力されるので`yes`を入力
Enter a value: yes
aws_vpc.vpc: Creating...
aws_vpc.vpc: Creation complete after 9s [id=vpc-0f1c8121f72d84ee9]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

ちょっとした解説①

ここでは上記で説明できていない部分について解説します。

Workspace

vpc.tfで記述されていた${terraform.workspace}についてですが、これはWorkspaceの環境名を割り当てるための変数です。
そのため、今回の実行結果を見るとタグに指定した値にstgと入っていることがわかります。

### terraform apply結果の抜粋
+ tags = {
+ "Env" = "stg"
+ "Name" = "stg-example"
+ "Project" = "example"
}
}

また、cidr_blockには、10.0.0.0/24のネットワークアドレスが入っていることがわかります。

+ cidr_block                       = "10.0.0.0/16"

これは、Workspaceによって値が変わる部分のため、以下のように二つ定義をしています。
そのため、vpc.tfで呼び出すときは、terraform.workspaceを利用します。

variable.tf(抜粋)
locals {
project_name = "example"

vpc_cidr = {
stg = "10.0.0.0/16"
prd = "10.1.0.0/16"
}
}

Local変数

タグのprojectには、exampleという文字列が入っております。
これは、variable.tfで定義したローカル変数が割り当てられています。

vpc.tf(抜粋)
Project = local.project_name
variable.tf(抜粋)
locals {
project_name = "example"

ネットで調べているとVariable変数を使うケースをよく見かけることがあるかと思います。
しかし、個人的には、変数の組み込みやコマンド時の変数挿入などを防ぐことができるため、Local変数を利用しています。

SubnetやNAT Gatewayなどの構築

続いて、残りのリソースも作成していきます

Subnet

パブリックサブネットとプライベートサブネットを合計で6つ作成します。
以下のtfファイルを作成します。

subnet.tf
resource "aws_subnet" "public_subnet" {
for_each = local.subnet_numbers
vpc_id = aws_vpc.vpc.id
availability_zone = each.key
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, each.value)

tags = {
Name = "${terraform.workspace}-${local.project_name}-private"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

resource "aws_subnet" "private_subnet" {
for_each = local.subnet_numbers
vpc_id = aws_vpc.vpc.id
availability_zone = each.key
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, each.value + 3)

tags = {
Name = "${terraform.workspace}-${local.project_name}-private"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

Internet Gatewayとルートテーブル

Internet Gatewayを作成します。

igw.tf
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id

tags = {
Name = "${terraform.workspace}-${local.project_name}"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

ルートテーブルを作成します。

route_table.tf
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id

tags = {
Name = "${terraform.workspace}-${local.project_name}-public"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

resource "aws_route_table" "private" {
vpc_id = aws_vpc.vpc.id

tags = {
Name = "${terraform.workspace}-${local.project_name}-private"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

パブリックサブネットとプライベートサブネットをルートテーブルに紐付けます。

route_association.tf
resource "aws_route_table_association" "public" {
for_each = local.subnet_numbers
subnet_id = aws_subnet.public_subnet[each.key].id
route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
for_each = local.subnet_numbers
subnet_id = aws_subnet.private_subnet[each.key].id
route_table_id = aws_route_table.private.id
}

ルーティングを定義します。

  • パブリックサブネットから0.0.0.0/0にアクセスする場合は、Internet Gatewatへの向き先を指定
  • プライベートサブネットから0.0.0.0/0にアクセスする場合は、NAT Gatewatへの向き先を指定
route.tf
resource "aws_route" "public" {
route_table_id = aws_route_table.public.id
gateway_id = aws_internet_gateway.igw.id
destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route" "private" {
route_table_id = aws_route_table.private.id
gateway_id = aws_nat_gateway.nat_gateway.id
destination_cidr_block = "0.0.0.0/0"
}

NAT Gateway

パブリックサブネットにNAT Gatewayを1台構築します。
NAT Gatewayは、固定グローバルIPをアタッチする必要があるのでEIPを作成します。
また、各サブネットに対してのルートテーブルも定義します。

nat_gateway.tf
resource "aws_nat_gateway" "nat_gateway" {
allocation_id = aws_eip.nat_gateway.id
subnet_id = aws_subnet.public_subnet["ap-southeast-2a"].id
depends_on = [aws_internet_gateway.igw]

tags = {
Name = "${terraform.workspace}-${local.project_name}"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

eip.tf
resource "aws_eip" "nat_gateway" {
vpc = true
depends_on = [aws_internet_gateway.igw]

tags = {
Name = "${terraform.workspace}-${local.project_name}-nat"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

S3 Endpoint

S3 Endpointを作成します。

vpc_endpoint.tf
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.vpc.id
service_name = "com.amazonaws.${local.region}.s3"

tags = {
Name = "${terraform.workspace}-${local.project_name}-s3"
Env = "${terraform.workspace}"
Project = local.project_name
}
}

resource "aws_vpc_endpoint_route_table_association" "private_s3" {
vpc_endpoint_id = aws_vpc_endpoint.s3.id
route_table_id = aws_route_table.private.id
}

現在利用しているリージョンをデータリソースから取得し、ローカル変数で定義しています。

variable(抜粋)
data "aws_region" "current" {}

locals {
region = data.aws_region.current.name

ちょっとした解説②

リソースの参照

以下は、先ほど作成したvpcのidを取得するための構文です。

vpc_id            = aws_vpc.vpc.id

cidrsubnet

本記事のサブネットは、以下のレンジで作成しています。

  • Public Subnetework
    • 10.0.0.0/24
    • 10.0.1.0/24
    • 10.0.2.0/24
  • Private subnetwork
    • 10.0.3.0/24
    • 10.0.4.0/24
    • 10.0.5.0/24

そこで役に立つのが、cidrsubnet Functionです。
cidrsubnetは、以下のように3つの引数を持つ関数で、IPレンジをいい感じに分割してくれます。

cidrsubnet(prefix, newbits, netnum)

今回のケースで、値を割り当てると以下のかたちになります。

  • prefix: 10.0.0.0/16
  • newbits: 8
  • netnum: 0(ここはcountでインクリメントされる)

具体的にどう変換されるかをterraform consoleで確認してみます。

$ terraform console
> cidrsubnet("10.0.0.0/16", 8, 0)
10.0.0.0/24
> cidrsubnet("10.0.0.0/16", 8, 1)
10.0.1.0/24

このようにサブネットの結果が不安な場合は、terraform consoleを利用すると捗ります。

depends_on

NAT GatewayとEIPは、Internet Gatewayに依存しています。
そこで、depends_onを記述することで明示的に依存関係を記すことで、先にInternet Gatewayを構築し、その後にEIPとNAT Gatewayを構築するという流れを確立できます。

depends_on = [aws_internet_gateway.igw]

Production 環境の構築

コードの変更は不要です。
Workspaceのprdに切り替えてterraform applyするだけです。

$ terraform workspace selece prd
$ terraform plan
$ terraform apply

めっちゃ簡単ですね!

ここまででVPCの構築が完了しました。

最終的なVariable.tf

本記事で作成したvariable.tfです。

variable.tf
data "aws_region" "current" {}

locals {
project_name = "example"
region = data.aws_region.current.name

vpc_cidr = {
stg = "10.0.0.0/16"
prd = "10.1.0.0/16"
}

subnet_numbers = {
"ap-southeast-2a" = 0
"ap-southeast-2b" = 1
"ap-southeast-2c" = 2
}
}

Terraform 0.12 変更点

この章では、Terraform 0.12とそれ以前での変更点をまとめていきます。

面倒だったブロックが不要になったよ

ブロックやダブルクォーテーション("${}")で囲う必要がなくなりました。
ただし、変数同士を繋いで表現する場合(NameやEnv)は、囲う必要があります(tagの部分)

Terraform 0.11系

resource "aws_vpc" "vpc" {
cidr_block = "${local.vpc_cidr[terraform.workspace]}"

tags = {
Name = "${terraform.workspace}-${local.project_name}"
Env = "${terraform.workspace}"
Project = "${local.project_name}"
}
}

Terraform 0.12系

resource "aws_vpc" "vpc" {
cidr_block = local.vpc_cidr[terraform.workspace]

tags = {
Name = "${terraform.workspace}-${local.project_name}"
Env = "${terraform.workspace}"
Project = "${local.project_name}"
}
}

無口なValidateが返信してくれるようになった

terraform validateを実行するとSuccess!って、反応が返ってくるようになりました。

Terraform 0.11系

$ terraform validate

Terraform 0.12系

$ terraform validate
Success! The configuration is valid.

Planなどの実行結果がわかりやすくなった

劇的な変化はないですが、見やすくなりました。

Terraform 0.11系

$ terraform plan
+ create

Terraform will perform the following actions:

+ aws_vpc.vpc
id: <computed>
arn: <computed>
assign_generated_ipv6_cidr_block: "false"
cidr_block: "10.0.0.0/24"
default_network_acl_id: <computed>
default_route_table_id: <computed>
default_security_group_id: <computed>
dhcp_options_id: <computed>
enable_classiclink: <computed>
enable_classiclink_dns_support: <computed>
enable_dns_hostnames: <computed>
enable_dns_support: "true"
instance_tenancy: "default"
ipv6_association_id: <computed>
ipv6_cidr_block: <computed>
main_route_table_id: <computed>
owner_id: <computed>
tags.%: "3"
tags.Env: "stg"
tags.Name: "stg-example"
tags.Project: "example"


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

Terraform 0.12系

$ terraform plan
+ create

Terraform will perform the following actions:

# aws_vpc.vpc will be created
+ resource "aws_vpc" "vpc" {
+ arn = (known after apply)
+ assign_generated_ipv6_cidr_block = false
+ cidr_block = "10.0.0.0/24"
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_classiclink = (known after apply)
+ enable_classiclink_dns_support = (known after apply)
+ enable_dns_hostnames = (known after apply)
+ enable_dns_support = true
+ id = (known after apply)
+ instance_tenancy = "default"
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Env" = "stg"
+ "Name" = "stg-example"
+ "Project" = "example"
}
}

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

for_eachの導入

for_eachが使えるようになりました!
これは個人的には革新的で今まで抱えていた問題を解決する一つの武器となると思っています。

Terraform 0.11系

サブネットを作成するときに以下のようにcountを利用するケースがありました。

sample_subnet.tf(抜粋)
data "aws_availability_zones" "available" {
state = "available"
}

resource "aws_subnet" "public_subnet" {
count = 3
vpc_id = aws_vpc.vpc.id
availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index + 0)
}

一見問題ないように見えるのですが、countを利用しているため、数字をインクリメントしてリストが作成されていきます。
その結果、リストの変更などでインデックスがずれてしまう問題が発生する可能性がありました。

# aws_subnet.public_subnet[0] will be created

Terraform 0.12系

0.12.6からfor_eachをマップ形式でアクセスできるようになりました。
以下のようにfor_eachで定義し、each.keyeach.valueで各要素にアクセスできます。

subnet.tf(抜粋)
resource "aws_subnet" "public_subnet" {
for_each = local.subnet_numbers
vpc_id = aws_vpc.vpc.id
availability_zone = each.key
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, each.value)
}

resource "aws_subnet" "private_subnet" {
for_each = local.subnet_numbers
vpc_id = aws_vpc.vpc.id
availability_zone = each.key
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, each.value + 3)
}

for_eachで参照する定義をvariable.tfに追記します。

subnet_numbers = {
"ap-southeast-2a" = 1
"ap-southeast-2b" = 2
"ap-southeast-2c" = 3
}

NAT Gatewayのように一つのサブネットに作成したい場合などに役立ちます。

subnet_id     = aws_subnet.public_subnet["ap-southeast-2a"].id

ちなみに、0.12.6より古いバージョンでterraform validateを実行するとエラーが発生します。

Error(terrform0.12.5実行結果)
Error: Reserved argument name in resource block

on subnet.tf line 2, in resource "aws_subnet" "public_subnet":
2: for_each = local.subnet_numbers

The name "for_each" is reserved for use in a future version of Terraform.

lookupについて

今回の構成では、お伝えすることができなかったのですが、Terraform 0.12からlookupの書き方が変わったので変更点を以下に記載します。

Terraform 0.11系

instance_type               = "${lookup(local.ec2_config[terraform.workspace], "instance_type")}"

Terraform 0.12系

instance_type               = local.ec2_config[terraform.workspace]["instance_type"]

Terraform 0.11.14からアップグレードする方法

ここからは、すでにTerraform0.11系を利用している方のために、ざっくりですが0.12にアップグレードする方法を記載します。

Terraform アップグレード

Terraform 0.11.14でinitを実行します 2

$ terraform init

Workspaceを切り替えます(Workspace環境を前提にしています。)

$ terraform workspace list
$ terraform workspace select stg

terraform planを実行します。
もし、planを実行し、差分が発生した場合はTerraform 0.12に対応した構文に変更する必要があります。

$ terraform plan

checklistコマンドを実行し、アップグレード可能な状態かを確認します。
問題がなければLooks good!と出力されます。

$ terraform_0.11 0.12checklist
Looks good! We did not detect any problems that ought to be
addressed before upgrading to Terraform v0.12.

This tool is not perfect though, so please check the v0.12 upgrade
guide for additional guidance, and for next steps:
https://www.terraform.io/upgrade-guides/0-12.html

terraformのバージョンを0.12に変更し、initを実行します。

$ terraform init

upgradeコマンドを実行します。
問題がなければUpgrade complete!と出力されます。

$ terraform 0.12upgrade
Upgrade complete!

planを実行し、問題が発生しなければアップグレード完了です。

$ terrafrom plan

まとめ

いかがでしたか?

Terraform 0.12になったことで、構文が変わって戸惑う場面もあるかと思いますが、それ以上に恩恵を授かれることを少しでも感じ取ってもらえたのではないでしょうか。また、これを機にTerraformを触ってみたいぞ!って方が増えたら嬉しいです。

今回は、VPCの話しかできなかったので、次回は更にこの環境を大きくしていき、皆さんのお役に立てる記事を書いていきたいと思います。

ありがとうございました!

参考

Upgrading to Terraform v0.12
Resouces
Terraform 0.12 Preview


  1. 1.Technology Innovation Groupの略で、フューチャーの中でも特にIT技術に特化した部隊です。その中でもDXチームは特にデジタルトランスフォーメーションに関わる仕事を推進していくチームです。
  2. 2.Terraformを0.12にアップグレードする場合は、0.11.14まで上げる必要があります。