[Terraform]Moduleを作ると環境毎のデプロイが便利

2021.01.31

はじめに

システムを構築するにあたって開発、検証、本番環境をそれぞれ用意することが多いですが、Terraformで作成する場合にModuleを使った環境毎のデプロイが便利だったので紹介します。

Terraform Moduleとは

複数のresourceブロックで構成されたファイル群(.tf)をテンプレート化したものです。Moduleブロックで呼び出す時に変数を渡すだけで一貫性ある構成を作成できます。

Modules Overview - Configuration Language - Terraform by HashiCorp

例えば、開発と本番環境それぞれ異なるネットワークのVPCとサブネットを作成する場合、以下のようなModuleを作成します。

  • module/vpc/mainf.tf
resource "aws_vpc" "vpc" {
  cidr_block           = var.cidr_vpc
  instance_tenancy     = "default"
  enable_dns_hostnames = true
}

resource "aws_subnet" "public1" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = var.cidr_public1
  availability_zone = var.az1
}

resource "aws_subnet" "public2" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = var.cidr_public2
  availability_zone = var.az2
}
  • module/vpc/variables.tf


variable "cidr_vpc" {}
variable "cidr_public1" {}
variable "cidr_public2" {}
variable "az1" {}
variable "az2" {}

環境毎のtfファイルを作成し、変数を定義します。

  • environment/dev/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.24.1"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

module "network" {
  source   = "../../module_aws/vpc"
  cidr_vpc = "10.255.0.0/16"
  cidr_public1 = "10.255.1.0/24"
  cidr_public2 = "10.255.2.0/24"
  az1 = "ap-northeast-1a"
  az2 = "ap-northeast-1c"
}
  • environment/prd/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.24.1"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

module "network" {
  source   = "../../module_aws/vpc"
  cidr_vpc = "10.0.0.0/16"
  cidr_public1 = "10.0.1.0/24"
  cidr_public2 = "10.0.2.0/24"
  az1 = "ap-northeast-1a"
  az2 = "ap-northeast-1c"
}

moduleを指定したルートモジュールterraform applyすると以下のようなVPCリソースがデプロイされます。

抽象化したModuleを目指す

Moduleは、なるべく抽象化することがより良いModuleと言われています。先程のModuleから環境毎に異なるSubnetリソース数のデプロイする場合を考えてみます。

本番環境では3AZで冗長化したい場合、今のModuleだと対応できません。別のModuleを用意する、もしくはenvironment/prd/main.tfに追加AZのresourceブロックを記述する方法などもありますが、せっかくのModuleを使った一貫性が損なわれます。 そんな悩みもTerraformを使えば、組み込み関数やMeta引数など様々な機能を利用し、柔軟なリソースのデプロイに対応できるコードが記述できます。

抽象化したModuleを作ってみる

先程のModuleでは、単純に変数を定義して渡しているだけなので、より抽象化したModuleを作ってみます。以下のようなSubnetをPublic,Private,Secureレイヤで分けたネットワーク構成でデプロイできるようにします。

作成した構成ファイルが以下です。

  • module_aws/vpc/main.tf
data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "vpc" {
  cidr_block           = var.cidr_vpc
  instance_tenancy     = "default"
  enable_dns_hostnames = true
  tags = {
    Name = "${var.system}-${var.env}-vpc"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "${var.system}-${var.env}-igw"
  }
}

resource "aws_subnet" "public" {
  count             = length(var.cidr_public)
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(var.cidr_public, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = {
    Name = "${var.system}-${var.env}-public-${count.index + 1}"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.cidr_private)
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(var.cidr_private, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = {
    Name = "${var.system}-${var.env}-private-${count.index + 1}"
  }
}

resource "aws_subnet" "secure" {
  count             = length(var.cidr_secure)
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(var.cidr_secure, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = {
    Name = "${var.system}-${var.env}-secure-${count.index + 1}"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "${var.system}-${var.env}-public-rt"
  }
}
resource "aws_route_table_association" "public" {
  count          = length(var.cidr_public)
  subnet_id      = element(aws_subnet.public.*.id, count.index)
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "${var.system}-${var.env}-private-rt"
  }
}
resource "aws_route_table_association" "private" {
  count          = length(var.cidr_private)
  subnet_id      = element(aws_subnet.private.*.id, count.index)
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table" "secure" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "${var.system}-${var.env}-secure-rt"
  }
}
resource "aws_route_table_association" "secure" {
  count          = length(var.cidr_secure)
  subnet_id      = element(aws_subnet.secure.*.id, count.index)
  route_table_id = aws_route_table.secure.id
}

resource "aws_network_acl" "main" {
  vpc_id = aws_vpc.vpc.id
  subnet_ids = compact(
    flatten([
      aws_subnet.public.*.id,
      aws_subnet.private.*.id,
      aws_subnet.secure.*.id
    ])
  )
  egress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
  ingress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
  tags = {
    Name = "${var.system}-${var.env}-nacl"
  }
}
  • module_aws/vpc/variables.tf
variable "system" {}
variable "env" {}
variable "cidr_vpc" {}
variable "cidr_public" {}
variable "cidr_private" {}
variable "cidr_secure" {}
  • environment/dev/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.24.1"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

module "network" {
  source   = "../../module_aws/vpc"
  system   = "test"
  env      = "dev"
  cidr_vpc = "10.255.0.0/16"
  cidr_public = [
    "10.255.1.0/24",
    "10.255.2.0/24"
  ]
  cidr_private = [
    "10.255.101.0/24",
    "10.255.102.0/24"
  ]
  cidr_secure = [
    "10.255.201.0/24",
    "10.255.202.0/24"
  ]
}
  • environment/prd/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.24.1"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

module "network" {
  source   = "../../module_aws/vpc"
  system   = "test"
  env      = "prd"
  cidr_vpc = "10.1.0.0/16"
  cidr_public = [
    "10.1.1.0/24",
    "10.1.2.0/24",
    "10.1.3.0/24"
  ]
  cidr_private = [
    "10.1.101.0/24",
    "10.1.102.0/24",
    "10.1.103.0/24"
  ]
  cidr_secure = [
    "10.1.201.0/24",
    "10.1.202.0/24",
    "10.1.203.0/24"
  ]
}

module_aws/vpc/main.tfのdataブロックでaws_availability_zonesを作成します。providerで指定したAWSリージョンのAZがlistされます。リストから呼び出す時には、availability_zone = data.aws_availability_zones.available.names[0]と記述して参照できます。

サブネット(aws_subnet)は3つのラベル(public,private,secure)に分けてresourceを作成します。サブネットを動的に作成できるようにcountを使います。それぞれの変数(cidr_public,cidr_praivate,cidr_secure}に格納されている要素(ここではCIDRブロック)数をlength関数で計算してcountに渡しています。cidr_blockでは、count.indexとelement関数を使って変数の要素を順番に参照してサブネットリソースを作成できるようになります。

また、各リソースを判別するタグをつけます。count.indexは「0」から始まるインデックス番号なので1を加算していく方法でタグ付けします。

resource "aws_subnet" "public" {
  count             = length(var.cidr_public) ←変数「cidr_public,cidr_private,cidr_secure」の要素を計算
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = element(var.cidr_public, count.index) ←変数に格納されている要素を順番に参照
  availability_zone = data.aws_availability_zones.available.names[count.index] ←[0]から順番に参照
  tags = {
    Name = "${var.system}-${var.env}-public-${count.index + 1}" ←count.indexに1を加算
  }
}

ルートテーブル(aws_route_table)もそれぞれのレイヤで異なるルート設定にする為、3つのラベル(public,private,secure)に分けて作成します。サブネットの関連付け(aws_route_table_association)では、ここでもcountを使って関連付けるリソース数を計算します。ワイルドカードでaws_subnet.public.*.idとすることでpublicラベルの全てのresource属性が参照されます。

resource "aws_route_table_association" "public" {
  count          = length(var.cidr_public) ←変数「cidr_public,cidr_private,cidr_secure」の要素を計算
  subnet_id      = element(aws_subnet.public.*.id, count.index) ←publicラベルのaws_subnetのid属性を順番に参照
  route_table_id = aws_route_table.public.id
}

ネットワークACL(aws_network_acl)は、 VPC作成とともに自動で作成されますが、Terraformで作成したリソースとして管理したいので新規に作成します。ネットワークACLにサブネットを関連付けますが複数のサブネット(public,private,secureラベルのaws_subnet)の属性を参照するには、flatten関数で各resourceの属性をフラットにしてcompact関数で新たなリストとして渡します。

resource "aws_network_acl" "main" {
  vpc_id = aws_vpc.vpc.id
  subnet_ids = compact( ←flattenのリストから新たなリストで渡す
    flatten([ ←ワイルドカードで指定したリソース属性をフラットにする
      aws_subnet.public.*.id,
      aws_subnet.private.*.id,
      aws_subnet.secure.*.id
    ])
  )
〜

ここはかなりハマってしまいました。count使うとネットワークACLが複数作成されるのでNG、以下のような構文で各リソースをワイルドカードで指定するとaws_subnet.private is tuple with 2 elementsInappropriate value for attribute "subnet_ids": element 0: string required.と出力されてNGでした。(subnet_idsはlist型のみ)

resource "aws_network_acl" "main" {
  vpc_id = aws_vpc.vpc.id
  subnet_ids = [
    aws_subnet.public.*.id,
    aws_subnet.private.*.id,
    aws_subnet.secure.*.id
  ]

これで抽象化したModuleの完成です。後はmoduleを呼び出すmain.tfを環境毎に作成してterraform applyすれば変数に応じてリソースがデプロイされます。

おわりに

抽象化したModuleを作成しておくと、環境毎に変数を定義するだけで一貫性ある構成がデプロイできます。個人的にはModuleの抽象化を目的とせず、柔軟性が求められるようなリソースだけ抽象化することが実装まで時間短縮、構成ファイルの可読性が良くなると感じました。