作りながら覚えるTerraform入門シリーズの第3回です。
第2回の基本編ではとりあえずVPCを作成してみましたが、今回はサブネットやルートテーブルなど含めて改めてVPC関連のリソースを作成してみましょう。
作りながら覚えるTerraform入門シリーズ
- インストールと初期設定
- 基本編
- VPC編 => 今回はコチラ
- EC2編
- Route53 + ACM編
- ELB編
- RDS編
今回の学習ポイントは以下です。
- 変数の使い方
- リソース間の参照方法
- 関数の使い方
- Stateファイルの管理
VPCの作成
まず、改めてVPCを作成します。
network.tf
を以下のように書き換えます。
################################
# VPC
################################
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.prefix}-vpc"
}
}
DNS関連の以下の2つのパラメータは基本的にどちらとも有効にしておく場面が多いと思います。割りと重要な設定なので、true/falseいずれの場合であっても明示的に記述しておくのが良いと思います。
- DNS解決: enable_dns_support
- DNSホスト名: enable_dns_hostnames
省略した場合、デフォルトではenable_dns_support
はtrue
、enable_dns_hostnames
はfalse
になります。パラメータの意味は、公式ドキュメントの VPC での DNS の使用 を参照しましょう。
instance_tenancy
は省略していますがdefault
が設定されます。
tags
の中にKeyValueの組み合わせをマップで記述することでタグを指定できます。指定しないとコンソール画面での識別に困るリソースはNameタグを指定します。
変数の使い方
プレフィックスのように何度も出現する文字列や、環境ごとに変動する値は変数化しておきましょう。
変数を定義するにはvariable
ブロックを使います。既存のnetwork.tf
ファイルに記述しても動作しますが、別ファイルに切り出してvariables.tf
に記述するのが一般的です。
# Common
variable "prefix" {
description = "Project name given as a prefix"
type = string
default = "cloud02"
}
# VPC
variable "vpc_cidr" {
description = "The CIDR block of the VPC"
type = string
default = "10.0.0.0/16"
}
variable
に続けて変数の名前を定義し、波括弧{}で囲った中に説明(description)、型(type)、デフォルト値などを記述します。説明、型は以降は省きますが、省略せずに記述することが推奨されています。
network.tf
では、定義した変数を以下のように記述して呼び出しています。
cidr_block = var.vpc_cidr
Name = "${var.prefix}-vpc"
var.変数名
で呼び出すことができますが、文字列の中に変数を記述する場合は"${var.変数名}-vpc"
のように${}
で括った形で変数を呼び出して、文字列全体はダブルクオーテーションで囲みます。
variables.tf
でデフォルト値を指定しない場合、planやapply実行時にプロンプトが表示されて入力を求められます。変数が宣言されているのに値がわからない状態、ということですね。
terraform plan
var.prefix
Enter a value:
terraform plan -var="prefix=cloud02"
のように-var
を使って引数で渡す方法もあります。
terraform plan -var="prefix=cloud02" -var="vpc_cidr=10.0.0.0/16"
terraform.tfvars
というファイル名に変数の値を記述すると、自動的に読み込んでくれます。
prefix = "cloud02"
vpc_cidr = "10.0.0.0/16"
また、環境変数で指定することもできます。TF_VAR_<変数名>=<値>
の形式で宣言します。
export TF_VAR_prefix=cloud02
export TF_VAR_vpc_cidr=10.0.0.0/16
整理すると、変数のデフォルト値を書いていない場合、
- コマンド実行時にプロンプトで入力する
- コマンド実行時に
-var
オプションで指定する -
terraform.tfvars
ファイルで指定する - 環境変数で指定する
ということになります。同時に指定された場合の 優先順位 は、
-var
オプション -> terraform.tfvars
-> 環境変数 となり、どれも指定されていなければプロンプトで入力となります。
初めは variables.tf
と terraform.tfvars
の使い分けがわかりにくいかもしれませんが、
-
variables.tf
には変数の宣言を書く -
terraform.tfvars
には変数の値を書く
というように理解しています。
variables.tf
のデフォルト値で全ての変数値を指定するとterraform.tfvars
は不要になりますが、開発/検証/本番など環境ごとに変動する値を定義したり、パスワードなど機密情報を書いて.gitignore
でバージョン管理対象外とする場合にterraform.tfvars
は役立ちます。
なお、terraform.tfvars
以外の名前で定義する場合、例えば、dev.tfvars
というように開発環境向けの変数を記述したファイルを利用する場合は-var-file
で指定すると読み込むことができます。
terraform plan -var-file="dev.tfvars"
変数の基本的な使い方は以上です。
ローカル変数を宣言するための locals ブロックもありますが、基本的な使い方は同じです。
variables.tf
と terraform.tfvars
は以下のようにして続きを進めていきます。
# Common
variable "prefix" {}
# VPC
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
prefix = "cloud02"
この段階でいったんapplyしてVPCの作成を確認しても構いませんし、そのまま続きのコードを記述して最後にapplyしてもOKです。
インターネットゲートウェイの作成
次はインターネットゲートウェイを作成します。
network.tf
に以下のコードを追記します。
################################
# Internet Gateway
################################
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.prefix}-igw"
}
}
vpc_id = aws_vpc.vpc.id
では、VPCのIDを参照しています。
今回のIGW→VPCのように他のリソースと関連付ける場合には、<リソースタイプ>.<リソースの識別名>.<属性>
という形で関連リソースを指定することができます。
属性は今回の例ではid
です。
リファレンスの「Attributes Reference」を参照するとどういった属性があるかわかりますが、たいていの場合はid
やarn
といったリソースを一意に特定するためのものが利用されます。
サブネットの作成
続いてサブネットを作成します。
network.tf
に以下のコードを追記します。
################################
# Subnet
################################
resource "aws_subnet" "public_subnet_1a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1a"
cidr_block = cidrsubnet(var.vpc_cidr, 8, 11) # 10.0.11.0/24
map_public_ip_on_launch = true
tags = {
"Name" = "${var.prefix}-public-subnet-1a"
}
}
resource "aws_subnet" "public_subnet_1c" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1c"
cidr_block = cidrsubnet(var.vpc_cidr, 8, 12) # 10.0.12.0/24
map_public_ip_on_launch = true
tags = {
"Name" = "${var.prefix}-public-subnet-1c"
}
}
resource "aws_subnet" "private_subnet_1a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1a"
cidr_block = cidrsubnet(var.vpc_cidr, 8, 21) # 10.0.21.0/24
map_public_ip_on_launch = false
tags = {
"Name" = "${var.prefix}-private-subnet-1a"
}
}
resource "aws_subnet" "private_subnet_1c" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1c"
cidr_block = cidrsubnet(var.vpc_cidr, 8, 22) # 10.0.22.0/24
map_public_ip_on_launch = false
tags = {
"Name" = "${var.prefix}-private-subnet-1c"
}
}
システム構成 を見て頂くとわかりやすいと思いますが、パブリックサブネットとプライベートサブネットをAZ-1aとAZ-1cにそれぞれ作成しています(計4つ)
パブリックサブネットはmap_public_ip_on_launch
をtrue
にしてパブリックIPアドレスの自動割当を有効にしています。また、cidr_block
は cidrsubnet という関数を使ってVPCのCIDRである「10.0.0.0/16」を元に算出しています。
構文はcidrsubnet(prefix, newbits, netnum)
です。
一見難しそうに見えるかもしれませんが、それぞれの関数部分をterraform console
で確認するとわかりやすいです。
terraform console
> cidrsubnet("10.0.0.0/16", 8, 11)
"10.0.11.0/24"
> cidrsubnet("10.0.0.0/16", 8, 12)
"10.0.12.0/24"
> cidrsubnet("10.0.0.0/16", 8, 21)
"10.0.21.0/24"
> cidrsubnet("10.0.0.0/16", 8, 22)
"10.0.22.0/24"
Terraformには他にも様々な組み込み関数がありますが、動作確認をするにはterraform console
が便利です。
ルートテーブルの作成
最後にルートテーブルを作成してサブネットに関連付けます。
network.tf
に以下のコードを追記します。
################################
# Public Route Table
################################
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.prefix}-public-route"
}
}
resource "aws_route" "to_internet" {
route_table_id = aws_route_table.public_rt.id
gateway_id = aws_internet_gateway.igw.id
destination_cidr_block = "0.0.0.0/0"
}
resource "aws_route_table_association" "public_rt_1a" {
subnet_id = aws_subnet.public_subnet_1a.id
route_table_id = aws_route_table.public_rt.id
}
resource "aws_route_table_association" "public_rt_1c" {
subnet_id = aws_subnet.public_subnet_1c.id
route_table_id = aws_route_table.public_rt.id
}
################################
# Private Route Table
################################
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.prefix}-private-route"
}
}
resource "aws_route_table_association" "private_rt_1a" {
subnet_id = aws_subnet.private_subnet_1a.id
route_table_id = aws_route_table.private_rt.id
}
resource "aws_route_table_association" "private_rt_1c" {
subnet_id = aws_subnet.private_subnet_1c.id
route_table_id = aws_route_table.private_rt.id
}
aws_route_table
で空のルートテーブルを作成
aws_route
でルートを追加、
aws_route_table_association
でサブネットに関連付け
という構成になっています。
パブリックサブネット向けのルートにはインターネット宛のルートとしてインターネットゲートウェイと関連付けています。プライベートサブネットには今回は特にルートを追加しませんが、NATGWが存在する場合などは同じようにルートを追加すればOKです。
terraform apply
を実行して、VPC、インターネットゲートウェイ、サブネット、ルートテーブルが作成、関連付けされていることを確認しましょう。
StateファイルをS3へ移行
さて、Terraformの実行計画 で解説したようにTerraformでは差分を簡単に知ることができて便利ですが、これはこれから実行されるコードと terraform.tfstate
というファイルを比較することで実現しています。
terraform.tfstate
はapplyやdestroyするたびに自動的に作成・更新されるファイルで、その名のとおり、Terraformによって管理されるインフラの状態が書かれています。中身はJSON形式です。
例えばterraform.tfstate
を誤って削除してしまった場合、全てのリソースが新規扱いになるためapply
を実行するとさきほど作成したVPC関連のリソースが再び一式作成されてしまいます。つまり、インフラの状態を維持・管理していく上で、非常に重要なファイルということです。
チーム開発では複数人で共有しながら進める必要があるため、terraform.tfstate
をS3などのストレージで管理するのが一般的です。学習の範囲ではterraform.tfstate
がローカルに保存されていても問題ありませんが、この記事ではterraform.tfstate
をS3に移行してみたいと思います。
terraform.tfstate
が保存される場所などの定義をTerraformでは「バックエンド」と呼び、backend
ブロックに記述します。provider.tf
にbackend
ブロックを追加しましょう。S3バケットの名前は読み替えてください。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
backend "s3" {
bucket = "cloud02-tf-remote-state-bucket"
key = "ap-northeast-1/dev/terraform.tfstate"
region = "ap-northeast-1"
}
}
provider "aws" {
region = "ap-northeast-1"
}
backend "s3"
と記述することで保存先をS3とすることを宣言しています。
他にもbackend "remote"
と書けばTerraform Cloudを利用することになります。
bucket
key
region
にはそれぞれバケット名、tfstateのパス(ファイル名)、リージョンを指定します。
Backend Configuration に書かれている通り、backend
ブロックでは変数が使えないので注意しましょう。
A backend block cannot refer to named values (like input variables, locals, or data source attributes).
バックエンドで指定するS3バケットは予めコンソール画面から作成しています。
S3バケット自体もTerraformで作成することもできますが、その場合は同じ管理下に含めず、別途作成するのが良さそうです。
書き換えできたらterraform plan
を実行してみます。
terraform plan
Error: Backend initialization required, please run "terraform init"
│
│ Reason: Initial configuration of the requested backend "s3"
│
│ The "backend" is the interface that Terraform uses to store state,
│ perform operations, etc. If this message is showing up, it means that the
│ Terraform configuration you're using is using a custom configuration for
│ the Terraform backend.
│
│ Changes to backend configurations require reinitialization. This allows
│ Terraform to set up the new configuration, copy existing state, etc. Please run
│ "terraform init" with either the "-reconfigure" or "-migrate-state" flags to
│ use the current configuration.
│
│ If the change reason above is incorrect, please verify your configuration
│ hasn't changed and try again. At this point, no changes to your existing
│ configuration or state have been made.
そうすると、バックエンドの構成が変更されたのでterraform init
を実行するよう促されます。
terraform init
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new "s3"
backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
terraform init
を実行すると、ローカルのtfstateをS3にコピーするか聞かれますので「yes」を入力して進めます。そうすると、ローカルのterraform.tfstate
はクリアされ、S3バケット上にファイルが作成されます。
この状態で改めてterraform plan
を実行してみます。
terraform plan
─────────────────────────────────────────────────────────────────────────────────────────────────
No changes. Your infrastructure matches the configuration.
Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan:
terraform apply -refresh-only
No changes
と表示されているので、S3のterraform.tfstate
を参照して差分チェックできていることがわかります。これ以降、変更を加えるたびにS3にあるterraform.tfstate
が更新されることになります。
S3に保存することで複数人で共有できるようになりましたが、厳密には同時に変更が加えられないよう更新中にロックしておく仕組みが必要です。DynamoDB State Locking にあるようにDynamoDBを使えば簡単に実装できますが、まだ試せていないので今回は省略させて頂きます。
今回は以上です。次回はEC2編ということで、Webサーバを2台構築してみたいと思います!