LoginSignup
2
1

More than 1 year has passed since last update.

作りながら覚えるTerraform入門(3) - VPC編

Last updated at Posted at 2021-06-07

作りながら覚えるTerraform入門シリーズの第3回です。
第2回の基本編ではとりあえずVPCを作成してみましたが、今回はサブネットやルートテーブルなど含めて改めてVPC関連のリソースを作成してみましょう。

作りながら覚えるTerraform入門シリーズ
  1. インストールと初期設定
  2. 基本編
  3. VPC編 => 今回はコチラ
  4. EC2編
  5. Route53 + ACM編
  6. ELB編
  7. RDS編

今回の学習ポイントは以下です。

  • 変数の使い方
  • リソース間の参照方法
  • 関数の使い方
  • Stateファイルの管理

VPCの作成

まず、改めてVPCを作成します。
network.tfを以下のように書き換えます。

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_supporttrueenable_dns_hostnamesfalseになります。パラメータの意味は、公式ドキュメントの VPC での DNS の使用 を参照しましょう。

instance_tenancyは省略していますがdefaultが設定されます。
tagsの中にKeyValueの組み合わせをマップで記述することでタグを指定できます。指定しないとコンソール画面での識別に困るリソースはNameタグを指定します。

変数の使い方

プレフィックスのように何度も出現する文字列や、環境ごとに変動する値は変数化しておきましょう。
変数を定義するにはvariableブロックを使います。既存のnetwork.tfファイルに記述しても動作しますが、別ファイルに切り出してvariables.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というファイル名に変数の値を記述すると、自動的に読み込んでくれます。

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.tfterraform.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.tfterraform.tfvarsは以下のようにして続きを進めていきます。

variables.tf
# Common
variable "prefix" {}

# VPC
variable "vpc_cidr" {
  default = "10.0.0.0/16"
}
terraform.tfvars
prefix = "cloud02"

この段階でいったんapplyしてVPCの作成を確認しても構いませんし、そのまま続きのコードを記述して最後にapplyしてもOKです。

インターネットゲートウェイの作成

次はインターネットゲートウェイを作成します。
network.tfに以下のコードを追記します。

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」を参照するとどういった属性があるかわかりますが、たいていの場合はidarnといったリソースを一意に特定するためのものが利用されます。

サブネットの作成

続いてサブネットを作成します。
network.tfに以下のコードを追記します。

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_launchtrueにしてパブリックIPアドレスの自動割当を有効にしています。また、cidr_blockcidrsubnet という関数を使って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に以下のコードを追記します。

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.tfbackendブロックを追加しましょう。S3バケットの名前は読み替えてください。

provider.tf
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バケット上にファイルが作成されます。

3FFE3306-E4DB-4F2A-8C87-907511FAEF87.png

この状態で改めて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台構築してみたいと思います!

参考リンク

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1