はじめに こんにちはー TIG DXチーム 1 のゆるふわエンジニアの前原です。
前回のはじめてのTerraform 0.12 ~環境構築~ に続き実践編です。実際の構築を通して、最近バージョンアップしたTerraform 0.12の構文がこんな感じで変わったよー的な話を伝えていければと思っています。
では、Terraformを用いてAWSのリソースを作成していきましょう。
構築する環境構成図 下図の環境(VPC, VPC Endpoint, NAT Gatewayなど)をTerraformで構築していきます。
VPCは、Staging(stg)とProduction(prd)に2つのVPCを構成します。AZ(Availability Zone)は3つのゾーンを利用し、サブネットはパブリックとプライベートに分けて構成します。
Endpointとして、S3をセットします。
パブリックプライベートにNAT Gatewayを構築します。コストを抑えるために1台とします。
最終的に以下のディレクトリ構成になります。前回の記事でお伝えしたように、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を実行します。 が、実行する前に構文や設定に問題がないかを確認するためのコマンドを実行します。
構文に問題ないかをvalidate
コマンドで確認します。 問題なければSuccess
と出力されます。
$ terraform validate Success! The configuration is valid.
次にterraform fmt
というインデントなどのスタイルを揃えるコマンドを実行します。
それでは、設定に問題ないかを確認します。
$ 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: + 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.
最後にapply
を実行し、Apply complete!
と出力されたら完了です。
$ terraform apply 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.
ちょっとした解説(1) ここでは上記で説明できていない部分について解説します。
Workspace vpc.tf
で記述されていた${terraform.workspace}
についてですが、これはWorkspaceの環境名を割り当てるための変数です。 そのため、今回の実行結果を見るとタグに指定した値にstg
と入っていることがわかります。
+ tags = { + "Env" = "stg" + "Name" = "stg-example" + "Project" = "example" } }
また、cidr_block
には、10.0.0.0/24
のネットワークアドレスが入っていることがわかります。
+ cidr_block = "10.0.0.0/16"
これは、Workspaceによって値が変わる部分のため、以下のように2つ定義をしています。 そのため、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
ちょっとした解説(2) リソースの参照 以下は、先ほど作成した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とそれ以前での変更点をまとめていきます。
面倒だったブロックが不要になったよ ブロックやダブルクォーテーション("${}"
)で囲う必要がなくなりました。 ただし、変数同士を繋いで表現する場合(NameやEnv)は、囲う必要があります(tagの部分)
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} " } }
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 validate Success! The configuration is valid.
Planなどの実行結果がわかりやすくなった 劇的な変化はないですが、見やすくなりました。
$ 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 plan + create Terraform will perform the following actions: + 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
が使えるようになりました! これは個人的には革新的で今まで抱えていた問題を解決する1つの武器となると思っています。
サブネットを作成するときに以下のように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
0.12.6
からfor_each
をマップ形式でアクセスできるようになりました。 以下のようにfor_each
で定義し、each.key
とeach.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
のように1つのサブネットに作成したい場合などに役立ちます。
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の書き方が変わったので変更点を以下に記載します。
instance_type = "${lookup(local.ec2_config[terraform.workspace], "instance_type")} "
instance_type = local.ec2_config[terraform.workspace]["instance_type" ]
ここからは、すでにTerraform0.11系を利用している方のために、ざっくりですが0.12にアップグレードする方法を記載します。
Terraform 0.11.14でinitを実行します 2 。
Workspaceを切り替えます(Workspace環境を前提にしています)。
terraform workspace list terraform workspace select stg
terraform plan
を実行します。 もし、planを実行し、差分が発生した場合はTerraform 0.12に対応した構文に変更する必要があります。
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を実行します。
upgradeコマンド
を実行します。 問題がなければUpgrade complete!
と出力されます。
$ terraform 0.12upgrade Upgrade complete!
plan
を実行し、問題が発生しなければアップグレード完了です。
まとめ いかがでしたか?
Terraform 0.12になったことで、構文が変わって戸惑う場面もあるかと思いますが、それ以上に恩恵を授かれることを少しでも感じ取ってもらえたのではないでしょうか。また、これを機にTerraformを触ってみたいぞ! って方が増えたら嬉しいです。
今回は、VPCの話しかできなかったので、次回は更にこの環境を大きくしていき、皆さんのお役に立てる記事を書いていきたいと思います。
ありがとうございました!
参考 Upgrading to Terraform v0.12 Resouces Terraform 0.12 Preview