はじめに
Terraformとは、「Infrastructure as Code」を行うツールで、例えば、AWSリソースをコードベースで作成できたりします。「Infrastructure as Code」を行うツールは、たくさんありますが、Terraformでは「モジュール」という機能があります。
このモジュールと言う機能は、ソースコードをテンプレート化して、パラメータを開発用と、本番用で分けることでソースコードを再利用しながら2環境つくれるような機能です。(ざっくり言うと)
ただ、そのモジュールの構成について正解は無くネット上でも色んな構成であふれています。
なので、今回は、以下のAWSのCloudFormationのBlackBeltやGCPの記事を参考に、モジュールを使いテンプレート分割する(作成単位をアプリケーションやログとかの運用で分ける感じ)アーキテクチャでリソースを作成し、以下をポイントとして本記事を記載しました。
・モジュールの動き方とmain.tfやvariables.tfの連携の仕方
・テンプレート分割した時の動き方
・terraform_remote_stateの動き方
構成
作成した構成は以下になります。
そして。Terraform実行の各ファイル構成は以下のようになります。
ルートモジュールを分割しているので、以下のように実行順序をつけて、3回applyすることでリソースが作られます。
① /environments/dev/Network でterraform apply
② /environments/dev/Security でterraform apply
③ /environments/dev/Application でterraform apply
├── environments
│ ├── dev
│ │ ├── Application
│ │ │ ├── main.tf
│ │ │ ├── provider.tf
│ │ │ └── variables.tf
│ │ ├── Network
│ │ │ ├── main.tf
│ │ │ ├── outputs.tf
│ │ │ ├── provider.tf
│ │ │ └── variables.tf
│ │ └── Security
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ └── variables.tf
│ └── prod(割愛)
└── modules
├── Application
│ ├── ALB
│ │ ├── main.tf
│ │ └── variables.tf
│ ├── EC2
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── main.tf
│ └── variables.tf
├── Network
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── Route
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── VPC
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── Security
├── main.tf
├── outputs.tf
├── SecurityGroup
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── variables.tf
これらのファイルをすべて説明するとあまりにも長くなりすぎるので、本記事は以下の部分を抜粋して解説します。抜粋した基準は、terraform_remote_stateが実行されるまでです。また、ファイルの横にある番号は実行順序となっております。
├── environments
│ ├── dev
│ │ ├── Network
│ │ │ ├── variables.tf・・・1
│ │ │ ├── main.tf・・・2
│ │ │ ├── outputs.tf・・・12
│ │ │ └── provider.tf・・・13(説明上そうさせてください。)
│ │ └── Security
│ │ ├── variables.tf・・・14
│ │ └── main.tf・・・15
└── modules
├── Network
│ ├── variables.tf・・・3
│ ├── main.tf・・・4、8
│ ├── outputs.tf・・・11
│ ├── Route
│ │ ├── variables.tf・・・9
│ │ └── main.tf・・・10
│ └── VPC
│ ├── variables.tf・・・5
│ ├── main.tf・・・6
│ └── outputs.tf・・・7
└── Security
└── variables.tf・・・16
├── main.tf・・・17
├── SecurityGroup
├── variables.tf・・・18
└── main.tf・・・19
実行順での解説
上記で振った番号順で、ファイルの中身を記載しながら解説します。
1. environments/dev/Network/variables.tf
このファイルから始まります。variablesと記載のある通り変数の定義ファイルです。terraform的な構文ですが、vpc_cidr_block = "10.0.0.0/16"のような定義を行ってます。また、vpc_nameをdev-vpcやprd-vpcに分けることで、同じテンプレートのモジュールを再利用可能になります。
variable vpc_cidr_block {
default = "10.0.0.0/16"
}
variable vpc_name {
default = "myvpc"
}
variable public-1a-cidr {
default = "10.0.1.0/24"
}
variable public-1a-name {
default = "my-public-1a"
}
variable public-1c-cidr {
default = "10.0.2.0/24"
}
variable public-1c-name {
default = "my-public-1c"
}
variable private-1a-cidr {
default = "10.0.3.0/24"
}
variable private-1a-name {
default = "vpc-private-1a"
}
variable private-1c-cidr {
default = "10.0.4.0/24"
}
variable private-1c-name {
default = "vpc-private-1c"
}
variable igw-name {
default = "my-igw"
}
variable eipa-name {
default = "eip-1a"
}
variable ngwa-name {
default = "natge-1a"
}
variable eipc-name {
default = "eip-1c"
}
variable ngwc-name {
default = "natge-1c"
}
2. environments/dev/Network/main.tf
source = "../../../modules/Network/" のようにモジュールの実行フォルダを指定することで、モジュールが実行されます。また、1. で指定した、environments/dev/Network/variables.tfのパラメータを受け取っています。 vpc_cidr_block = var.vpc_cidr_block という記載は、vpc_cidr_blockは、1から持ってきた変数(var)のvpc_cidr_block("10.0.0.0/16")となります。
#----------------------------------------
# Networkモジュールの実行
#----------------------------------------
module "Network" {
source = "../../../modules/Network/"
vpc_cidr_block = var.vpc_cidr_block
vpc_name = var.vpc_name
public-1a-cidr = var.public-1a-cidr
public-1a-name = var.public-1a-name
public-1c-cidr = var.public-1c-cidr
public-1c-name = var.public-1c-name
private-1a-cidr = var.private-1a-cidr
private-1a-name = var.private-1a-name
private-1c-cidr = var.private-1c-cidr
private-1c-name = var.private-1c-name
igw-name = var.igw-name
eipa-name = var.eipa-name
ngwa-name = var.ngwa-name
eipc-name = var.eipa-name
ngwc-name = var.ngwc-name
}
3. modules/Network/variables.tf
次項の4.modules/Network/main.tf で使う変数の定義です。2番で渡されたのと同じ変数を定義します。
variable "vpc_cidr_block" {}
variable "vpc_name" {}
variable "public-1a-cidr" {}
variable "public-1a-name" {}
variable "public-1c-cidr" {}
variable "public-1c-name" {}
variable "private-1a-cidr" {}
variable "private-1a-name" {}
variable "private-1c-cidr" {}
variable "private-1c-name" {}
variable "igw-name" {}
variable "eipa-name" {}
variable "ngwa-name" {}
variable "eipc-name" {}
variable "ngwc-name" {}
4. modules/Network/main.tf
source = "./VPC" で、Networkモジュールの中からVPCの子モジュールの実行を命令します。また、例のごとく、vpc_cidr_block = var.vpc_cidr_blockのように変数を設定します。
ルートモジュール→Networkモジュール→VPCモジュールのように実行するときは、毎回毎回変数の受け渡しが必要です。
また、module "vpc"とmodule "route" の実行順序は、CloudFormationなどと同様にTerraform側で、自動的に実行順序を計算してくれます。
module "vpc" {
source = "./VPC"
vpc_cidr_block = var.vpc_cidr_block
vpc_name = var.vpc_name
public-1a-cidr = var.public-1a-cidr
public-1a-name = var.public-1a-name
public-1c-cidr = var.public-1c-cidr
public-1c-name = var.public-1c-name
private-1a-cidr = var.private-1a-cidr
private-1a-name = var.private-1a-name
private-1c-cidr = var.private-1c-cidr
private-1c-name = var.private-1c-name
}
module "route" {
source = "./Route"
igw-name = var.igw-name
eipa-name = var.eipa-name
ngwa-name = var.ngwa-name
eipc-name = var.eipa-name
ngwc-name = var.ngwc-name
vpc_id = module.vpc.vpc_id
aws_subnet_public_1a = module.vpc.aws_subnet_public_1a
aws_subnet_public_1c = module.vpc.aws_subnet_public_1c
aws_subnet_private_1a = module.vpc.aws_subnet_private_1a
aws_subnet_private_1c = module.vpc.aws_subnet_private_1c
}
5. modules/Network/VPC/variables.tf
6番で使うmodules/Network/VPC/main.tfの変数の定義です。4番から渡されたmodule "vpc" の中にあるものだけで、module "route"の中にあるものの記載は不要です。
variable "vpc_cidr_block" {}
variable "vpc_name" {}
variable "public-1a-cidr" {}
variable "public-1a-name" {}
variable "public-1c-cidr" {}
variable "public-1c-name" {}
variable "private-1a-cidr" {}
variable "private-1a-name" {}
variable "private-1c-cidr" {}
variable "private-1c-name" {}
6. modules/Network/VPC/main.tf
いよいよリソースの作成になります。1で定義した、
variable vpc_cidr_block {
default = "10.0.0.0/16"
}
などが、色々なファイルやを経由してきて、変数セットされリソース(ここではVPCなど)が作成されます。構文的には、普通のterraform実行と変わらないです。
#############################################################
#VPCの作成
#############################################################
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr_block
tags = {
Name = var.vpc_name
}
}
#############################################################
#サブネットの作成
#############################################################
resource "aws_subnet" "public_1a" {
vpc_id = "${aws_vpc.vpc.id}"
availability_zone = "ap-northeast-1a"
cidr_block = var.public-1a-cidr
tags = {
Name = var.public-1a-name
}
}
resource "aws_subnet" "public_1c" {
vpc_id = "${aws_vpc.vpc.id}"
availability_zone = "ap-northeast-1c"
cidr_block = var.public-1c-cidr
tags = {
Name =var.public-1c-name
}
}
resource "aws_subnet" "private_1a" {
vpc_id = "${aws_vpc.vpc.id}"
availability_zone = "ap-northeast-1a"
cidr_block = var.private-1a-cidr
tags = {
Name = var.private-1a-name
}
}
resource "aws_subnet" "private_1c" {
vpc_id = "${aws_vpc.vpc.id}"
availability_zone = "ap-northeast-1c"
cidr_block = var.private-1c-cidr
tags = {
Name = var.private-1c-name
}
}
7. modules/Network/VPC/outputs.tf
ここから、この記事のポイントになってきます。このoutput.tf を使うことにより、別のモジュールでVPCモジュールで作成したリソースを拾えるようになります。
output "vpc_id" {
value = aws_vpc.vpc.id
}
の箇所を抜粋して構文を説明すると、VPCのIDをvpc_idという名前でoutputを行い、その値は、value = aws_vpc.vpc.idである。というもので、6で作成した、resource "aws_vpc" "vpc" の各リソースの作成の箇所で"(ダブルクォーテーション)で分割されている範囲を.(ドット)でつないで、最後に .id を付けることでそのリソースのIDを拾っています。
resource "aws_vpc" "vpc" → aws_vpc.vpc.id みたいな書き方。
output "vpc_id" {
value = aws_vpc.vpc.id
}
output "aws_subnet_public_1a" {
value = aws_subnet.public_1a.id
}
output "aws_subnet_public_1c" {
value = aws_subnet.public_1c.id
}
output "aws_subnet_private_1a" {
value = aws_subnet.private_1a.id
}
output "aws_subnet_private_1c" {
value = aws_subnet.private_1c.id
}
8. modules/Network/main.tf
2回目の登場のmodules/Network/main.tfです。2回目は、module "route" {が実行されます。
vpc_id = module .vpc.vpc_idで、モジュールvpcの7. modules/Network/VPC/outputs.tfでoutputしたvpc_idが設定されます。
その他のigw-name = var.igw-nameは、先ほどのoutputなどとは無関係ないので、1. environments/dev/Network/variables.tf で渡ってきたものです。
module "vpc" {
source = "./VPC"
vpc_cidr_block = var.vpc_cidr_block
vpc_name = var.vpc_name
public-1a-cidr = var.public-1a-cidr
public-1a-name = var.public-1a-name
public-1c-cidr = var.public-1c-cidr
public-1c-name = var.public-1c-name
private-1a-cidr = var.private-1a-cidr
private-1a-name = var.private-1a-name
private-1c-cidr = var.private-1c-cidr
private-1c-name = var.private-1c-name
}
module "route" {
source = "./Route"
igw-name = var.igw-name
eipa-name = var.eipa-name
ngwa-name = var.ngwa-name
eipc-name = var.eipa-name
ngwc-name = var.ngwc-name
vpc_id = module.vpc.vpc_id
aws_subnet_public_1a = module.vpc.aws_subnet_public_1a
aws_subnet_public_1c = module.vpc.aws_subnet_public_1c
aws_subnet_private_1a = module.vpc.aws_subnet_private_1a
aws_subnet_private_1c = module.vpc.aws_subnet_private_1c
}
9. modules/Network/Route/variables.tf
10番で使うmodules/Network/Route/main.tf用の変数の定義です。8番から渡されたmodule "route" {~} 内の変数は、すべて記載する必要があります。
variable "igw-name" {}
variable "eipa-name" {}
variable "ngwa-name" {}
variable "eipc-name" {}
variable "ngwc-name" {}
variable "vpc_id" {}
variable "aws_subnet_public_1a" {}
variable "aws_subnet_public_1c" {}
variable "aws_subnet_private_1a" {}
variable "aws_subnet_private_1c" {}
10. modules/Network/Route/main.tf
InternetGatewayなどのRoute系のリソース作成です。こちらの実行については、6. modules/Network/VPC/main.tfと動きは同じなので、説明は割愛します。
#############################################################
#インターネットゲートウェイの作成
#############################################################
resource "aws_internet_gateway" "igw" {
vpc_id = var.vpc_id
tags = {
Name = var.igw-name
}
}
#############################################################
#EIPの作成とそのEIPを割り当てるNAT Gatewayの作成
#############################################################
resource "aws_eip" "eip_1a" {
vpc = true
tags = {
Name = var.eipa-name
}
}
resource "aws_nat_gateway" "nat_1a" {
subnet_id = var.aws_subnet_public_1a
allocation_id = "${aws_eip.eip_1a.id}"
tags = {
Name = var.ngwa-name
}
}
resource "aws_eip" "eip_1c" {
vpc = true
tags = {
Name = var.eipa-name
}
}
resource "aws_nat_gateway" "nat_1c" {
subnet_id = var.aws_subnet_public_1c
allocation_id = "${aws_eip.eip_1c.id}"
tags = {
Name = var.ngwc-name
}
}
#############################################################
#パブリックサブネット向けのルートテーブルの作成
#############################################################
resource "aws_route_table" "public" {
vpc_id = var.vpc_id
tags = {
Name = "route-table-public"
}
}
resource "aws_route" "public" {
destination_cidr_block = "0.0.0.0/0"
route_table_id = "${aws_route_table.public.id}"
gateway_id = "${aws_internet_gateway.igw.id}"
}
resource "aws_route_table_association" "public_1a" {
subnet_id = var.aws_subnet_public_1a
route_table_id = "${aws_route_table.public.id}"
}
resource "aws_route_table_association" "public_1c" {
subnet_id = var.aws_subnet_public_1c
route_table_id = "${aws_route_table.public.id}"
}
resource "aws_route_table" "private_1a" {
vpc_id = var.vpc_id
tags = {
Name = "route-table-private-1a"
}
}
resource "aws_route_table" "private_1c" {
vpc_id = var.vpc_id
tags = {
Name = "route-table-private-1c"
}
}
#############################################################
#プライベートサブネット向けのルートテーブルの作成とNAT Gatewayの紐づけ
#############################################################
resource "aws_route" "private_1a" {
destination_cidr_block = "0.0.0.0/0"
route_table_id = "${aws_route_table.private_1a.id}"
nat_gateway_id = "${aws_nat_gateway.nat_1a.id}"
}
resource "aws_route" "private_1c" {
destination_cidr_block = "0.0.0.0/0"
route_table_id = "${aws_route_table.private_1c.id}"
nat_gateway_id = "${aws_nat_gateway.nat_1c.id}"
}
resource "aws_route_table_association" "private_1a" {
subnet_id = var.aws_subnet_private_1a
route_table_id = "${aws_route_table.private_1a.id}"
}
resource "aws_route_table_association" "private_1c" {
subnet_id = var.aws_subnet_private_1c
route_table_id = "${aws_route_table.private_1c.id}"
}
11. modules/Network/outputs.tf
- modules/Network/VPC/outputs.tfでアウトプットしてきた変数をルートモジュール側に渡すためにさらにoutputします。
output "vpc_id" {
value = module.vpc.vpc_id
}
の箇所を抜粋して構文を説明すると、vpc_idという名前でoutputし、その値は、value = module.vpc.vpc_idである。というものです。module.vpc.vpc_idは、VPCモジュールで定義した(でアウトプットした)vpc_idというものです。
output "vpc_id" {
value = module.vpc.vpc_id
}
output "aws_subnet_public_1a" {
value = module.vpc.aws_subnet_public_1a
}
output "aws_subnet_public_1c" {
value = module.vpc.aws_subnet_public_1c
}
output "aws_subnet_private_1a" {
value = module.vpc.aws_subnet_private_1a
}
output "aws_subnet_private_1c" {
value = module.vpc.aws_subnet_private_1c
}
12. environments/dev/Network/outputs.tf
outputs.tfが続いてますね。11. odules/Network/outputs.tfでアウトプットしてきた変数を他のルートモジュールでも使えるように、さらにoutputします。
output "vpc_id" {
value = module.Network.vpc_id
}
の箇所を抜粋して構文を説明すると、vpc_idという名前でoutputする。その値は、value = module.vpc.vpc_idである。というものです。Networkモジュールで定義した(でアウトプットした)vpc_idというものです。
output "vpc_id" {
value = module.Network.vpc_id
}
output "aws_subnet_public_1a" {
value = module.Network.aws_subnet_public_1a
}
output "aws_subnet_public_1c" {
value = module.Network.aws_subnet_public_1c
}
output "aws_subnet_private_1a" {
value = module.Network.aws_subnet_private_1a
}
output "aws_subnet_private_1c" {
value = module.Network.aws_subnet_private_1c
}
13. environments/dev/Network/provider.tf
基本的に、おまじない的なファイルで、私もあまり意味を考えず記載しているのですが、backend s3のところはポイントで、指定したバケット(ここではmybucket)に、NetworkModule.tfstateを置きます。キャプチャのようにバケットに配置されます。このtfstateファイルにoutputしてきたidなどが格納されています。
以下のような流れで渡ってきたことになります。
・7. modules/Network/VPC/outputs.tf
↓
・11. modules/Network/outputs.tf
↓
・12. environments/dev/Network/outputs.tf
↓
・13. environments/dev/Network/provider.tf
↓
・S3に、vpc_idなどの情報が記載されたNetworkModule.tfstateの配置
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.51.0"
}
}
backend s3 {
bucket = "mybucket"
region = "ap-northeast-1"
key = "NetworkModule.tfstate"
}
}
provider aws {
region = "ap-northeast-1"
}
ここまででで、① /environments/dev/Networkの terraform applyは終了です。
実行が正常終了した場合、以下のようにOutputsのプロンプトが返ってきます。
そして続いて、② /environments/dev/Security でterraform apply
を実行していきます。
14. environments/dev/Security/variables.tf
Security リソースとして定義したい、変数定義です。1のNetwork/variables.tfと動きは同じなので、説明は割愛します。
variable security_group_name {
default = "mysg"
}
variable security_group_description {
default = "mysg"
}
15. environments/dev/Security/main.tf
基本的には、Network/main.tfと同じ動きなのですが、①のNetworkモジュールで作成した、VPCのIDなどを拾いたいため、それらの情報がアウトプットされた、mybucketのNetworkModule.tfstateを定義します。今振り返ると、このnetwork_remote_state_bucketとnetwork_remote_state_keyの定義は14番でやったほうが良かったですね。すいません。。。
#----------------------------------------
# Securityモジュールの実行
#----------------------------------------
module "Security" {
source = "../../../modules/Security/"
security_group_name = var.security_group_name
security_group_description = var.security_group_description
network_remote_state_bucket = "mybucket"
network_remote_state_key = "NetworkModule.tfstate"
}
16. modules/Security/variables.tf
modules/Network/variables.tf と同じ動きなので説明は割愛します。
#############################################################
#Security作成リソースに渡すパラメータ
#############################################################
variable "security_group_name" {}
variable "security_group_description" {}
variable "network_remote_state_bucket" {}
variable "network_remote_state_key" {}
17. modules/Security/main.tf
modules/Network/main.tf と同じ動きなので説明は割愛します。
module "securitygroup" {
source = "./SecurityGroup"
security_group_name = var.security_group_name
security_group_description = var.security_group_description
network_remote_state_bucket = var.network_remote_state_bucket
network_remote_state_key = var.network_remote_state_key
}
18. modules/Security/SecurityGroup/variables.tf
modules/Network/SecurityGroup/variables.tfと同じ動きなので説明は割愛します。
#############################################################
#SecurityNetwork作成リソースに渡すパラメータ
#############################################################
variable "security_group_name" {}
variable "security_group_description" {}
variable "network_remote_state_bucket" {}
variable "network_remote_state_key" {}
19. modules/Security/SecurityGroup/main.tf
基本的には、リソースの作成なのですが、ポイントとしてterraform_remote_stateと言う機能を使うことで、別モジュールで実行された値の取り出しを行っています。
1 data "terraform_remote_state" "network" { の箇所でNetworkModule.tfstateを参照
2.data.terraform_remote_state.network.outputs.vpc_id の箇所で、1で参照した、NetworkModule.tfstateから、Networkモジュールでoutputsされた、vpc_idを取り出す
のような流れで値の取り出しを行っています。
#############################################################
#セキュリティグループの作成
#############################################################
resource "aws_security_group" "security_group" {
name = var.security_group_name
description = var.security_group_description
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
#############################################################
#ネットワークモジュールのoutputデータの取り出し
#############################################################
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = var.network_remote_state_bucket
key = var.network_remote_state_key
region = "ap-northeast-1"
}
}
■動作確認
実行結果の動作確認ですが、この構成では、EC2がプライベートサブネット上の存在のため、ログインはSSMのセッションマネージャーで行い、Apacheをyumインストールすることで、NAT Gateway経由で外部yumリポジトリにアクセスできていることを確認し、その後、ELBのDNSからApacheの画面が表示できることでオンライン経路を確認しております。
おわりに
最後まで読んでいただきありがとうございました。Terraformはいろいろな構成が考えられて楽しいと思います。「ルートモジュールを分割するか1つにするか」、「そもそもモジュールを使うのが正しいのか」、「分割する場合は、どのレイヤーにするのか」、だとか色々。
今回も頭を楽しく使いながら記事を書けたので、今後も、「Infrastructure as Code」に関する記事を書いていきたいと思います。