AWS再入門ブログリレー2022 Terraform Registry Module (AWS VPC Terraform module) 編

弊社コンサルティング部による『AWS 再入門ブログリレー 2022』 5日目のエントリで、テーマはTerraform Registry Module (AWS VPC Terraform module)です。 Terraform愛が溢れて、長々と書いちゃっていますが、大事なことがたくさん書かれています。 参考にしていただけば幸いです。
2022.02.07

こんにちは!AWS事業本部コンサルティング部のたかくにです。

当エントリは弊社コンサルティング部による『AWS 再入門ブログリレー 2022』の 5日目のエントリです。

このブログリレーの企画は、普段 AWS サービスについて最新のネタ・深い/細かいテーマを主に書き連ねてきたメンバーの手によって、 今一度初心に返って、基本的な部分を見つめ直してみよう、解説してみようというコンセプトが含まれています。

AWSをこれから学ぼう!という方にとっては文字通りの入門記事として、またすでにAWSを活用されている方にとってもAWSサービスの再発見や2022年のサービスアップデートのキャッチアップの場となればと考えておりますので、ぜひ最後までお付合い頂ければ幸いです。

では、さっそくいってみましょう。5日目のテーマは「Terraform Registry Module (AWS VPC Terraform module)編」です。

(AWS再入門といいつつTerraformもよく使うツールなのでご容赦ください。)

今回のブログでは、ハンズオンも実施しますのでぜひ一度やってみてください!

Registry Moduleを使いこなそう!

Terraformを使う上で、「Terraformでコードを1から作るのは大変だからRegistry Moduleを使おう!」と一度は、聞いたことがあるではないかと思います。

私はあります。

しかし、英語のドキュメント、分量も多くて必要な情報がわからない、そもそも使い方わからないとハードルが高いと私は思います。

少なからず、私は上記の壁にぶつかり続けた人間の1人です。

今回は、そんなRegistry Moduleを解説します。

そもそもRegistry Moduleとは

文字通り、Terraform Registryに登録されているモジュールです。

Registry Module一覧は、こちらから検索することで、モジュール一覧を参照できます。

今回は、AWS VPC Terraform moduleについて解説していきます。

モジュールの使い方

はじめに、ローカル上で作成した、モジュールの使い方をご説明します。

モジュールの作成方法

まず、3つのファイルを、同一フォルダ内に作成します。

※1つのファイルに、まとめることもできますが、便宜上分けています。

  • variables.tf:variableブロック(引数)を定義するファイル
  • main.tf:resourceブロックを定義するファイル
  • outputs.tf:outputブロック(戻り値)を定義するファイル

フォルダ階層のイメージは以下の通りです。

今回だと、module_nameフォルダ配下にまとめています。

|-modules # このフォルダも、好みで省略できますが、便宜上まとめています。
| |-module_name # VPCをつくるmoduleなら、「vpc」など好みで命名します。
|   |-variables.tf
|   |-main.tf
|   |-outputs.tf
|
|-main.tf
|-variable.tf
|-terraform.tfvars

IAMユーザー(PowerUserAccess)を作成するモジュールを、定義してみようと思います。

modules/power_user/variables.tf

variable "user_name" {
  type = string
}

modules/power_user/main.tf

resource "aws_iam_user" "main" {
  name = var.user_name
  path = "/"

  tags = {
    "Name" = var.user_name
  }
}

resource "aws_iam_user_policy_attachment" "main" {
  user       = aws_iam_user.main.name
  policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
}

modules/power_user/outputs.tf

output "arn" {
  value = aws_iam_user.main.arn
  description = "The ARN assigned by AWS for this user."
}
output "name" {
  value = aws_iam_user.main.name
  description = "The user's name."
}
output "tags_all" {
  value = aws_iam_user.main.tags_all
  description = "A map of tags assigned to the resource, including those inherited from the provider default_tags configuration block."
}
output "unique_id" {
  value = aws_iam_user.main.unique_id
  description = "The unique ID assigned by AWS."
}

作成したpower_userモジュールのフォルダ階層

|-modules
| |-power_user
|   |-variables.tf
|   |-main.tf
|   |-outputs.tf
|
|-iam.tf
|-variable.tf
|-terraform.tfvars

モジュールを呼び出す方法

文法

module "ローカル名" {
  source = # モジュールの格納フォルダパスを定義
  
  # モジュール内で定義されている引数を渡す
}

power_userモジュールを呼び出す場合

iam.tf

module "power_user" {
  source = "../modules/power_user"
  
  user_name = "terraform-reintroduction"
}

モジュールを呼び出す際は、terraform planterraform apply実行前に、terraform getまたは、terraform initを実行する必要があります。

terraform getterraform initについて

terraform getterraform initでは、モジュールのファイルパスとリソース名のマッピングを行います。

マッピング情報は、.terraform/modules/modules.jsonにあります。

modules.jsonのファイルパス

|-modules
| |-power_user
|   |-variables.tf
|   |-main.tf
|   |-outputs.tf
|
|-iam.tf
|-variable.tf
|-terraform.tfvars
|-.terraform
|  |-modules
|  | |-modules.json
|  |-providers

modules.json

{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"power_user","Source":"../modules/power_user","Dir":"../modules/power_user"}]}

terraform getterraform initのタイミングについて

モジュール使用用途での実行のタイミングは、「新しくローカル名を定義した」または、「source(フォルダ階層)プロパティを変更した」場合に都度行います。

実行タイミングについて

  1. module "A"を定義しました:実行する
  2. module "A"の引数を変更しました:実行しない
  3. module "B"を定義しました:実行する
  4. module "A"を削除しました:実行しない
  5. module "A"を作成しました(sourceプロパティ変更無し):実行しない
  6. module "A"のsourceプロパティを変更しました:実行する
  7. module "A"を削除しました:実行しない
  8. module "A"を定義しました(source変更):実行する

モジュールで作成したリソースの値を参照する方法

モジュールで作成されたリソースの値を、modules/power_userフォルダ外部から参照する場合は、outputの値のみ参照可能です。

例えば、先程のiam.tfで、モジュールから作成したIAMユーザーを、IAMユーザーグループへ所属させる際は、以下のように参照します。

module "power_user" {
  source = "../modules/power_user"
  
  user_name = "terraform-reintroduction"
}

resource "aws_iam_group" "group" {
  name = "group-terraform-reintroduction"
  path = "/"
}

resource "aws_iam_group_membership" "group" {
  name = "group-membership-terraform-reintroduction"
  group = aws_iam_group.group.name

  users = [
    module.power_user.name # ../modules/power_user/outputs.tfを参照。
    # module.power_user.aws_iam_user.main.name のような参照はできない。
  ]
}

Registry Module 実践

さて、前置きがかなり長くなりましたが、ここからが本題です。

今回は、Terraform Registry Module界隈で最も使われているモジュールであろう、AWS VPC Terraform moduleについて、ハンズオンしてみようと思います。

AWS VPC Terraform moduleの使い方

まずは、AWS VPC Terraform moduleを開き、Provision Instructionsを見ます。

Registry Moduleとローカルのモジュールの圧倒的な違いは、versionを指定するか否かです。

Terraformでは、versionの指定が推奨されています。(ここ大事!)

The version argument is recommended for modules from a registry.

Module Blocks

バージョンを指定しない場合の挙動について

versionを指定しない場合、最新のバージョンがローカルの.terraform/modules/に保存されます。

Registry Module(AWS VPC Terraform module)のファイルパス

|-modules
| |-power_user
|   |-variables.tf
|   |-main.tf
|   |-outputs.tf
|
|-iam.tf
|-variable.tf
|-terraform.tfvars
|-.terraform
|  |-modules
|  | |-modules.json
|  | |-vpc
|  |   |- main.tf など
|  |-providers

terraform getterraform initについて」で、「モジュールの情報は、.terraform/modules/modules.jsonに保存される」とご説明しました。

Terraform Registry Moduleも同じく、モジュールの情報は、.terraform/modules/modules.json内に保存されます。

以下は、AWS VPC Terraform moduleを定義して、terraform getを行なった際のmodules.jsonの中身です。

.terraform/modules/modules.json

{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"vpc","Source":"registry.terraform.io/terraform-aws-modules/vpc/aws","Version":"3.11.5","Dir":".terraform/modules/vpc"}]}

versionプロパティを指定しない場合、次の事象が発生すると、偶発的な変更の恐れがあります。

  1. terraform init, apply (モジュール v3.11.5で、マッピング,リソース作成)
  2. モジュール v3.11.6以降がリリースされる
  3. modules.jsonが削除される
  4. 再度terraform init, apply(モジュール v3.11.6でマッピング, リソース作成)
  5. v3.11.5、v3.11.6間で何かしらの変更リスクが発生

バージョンを指定せず、modules.jsonを削除してしまった場合、以前のバージョンからアップデートがあった場合、terraform getterraform initでバージョンアップ(偶発的な変更)をしてしまいます。

そのため、Registry Moduleでは、バージョンアップによる偶発的な変更を防ぐために、versionプロパティを指定することが推奨されています。

心得その1 「version指定は行いましょう」

前述の通り、version指定を行わない場合偶発的なリソースへの変更リスクが生じてしまうため、Registry Moduleを使用する場合は、versionを指定します。

vpc.tf

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.11.5"
  # insert the 23 required variables here
}

心得その2 「とりあえずのterraform plan」

terraform initが終わったら、試しにterraform planをしてみましょう。

リソースは作られないので、とりあえずのterraform planです。

Terraformの原則であるWrite Plan Applyに忠実に従いましょう。

The Core Terraform Workflow

今回の場合、terraform planを行うと、綺麗にエラーで怒られます。

% terraform plan
module.power_user.aws_iam_user.main: Refreshing state... [id=terraform-reintroduction]
module.power_user.aws_iam_user_policy_attachment.main: Refreshing state... [id=terraform-reintroduction-20220206090410142500000001]
╷
│ Error: expected "cidr_block" to contain a network Value with between 16 and 28 significant bits, got: 0
│ 
│   with module.vpc.aws_vpc.this[0],
│   on .terraform/modules/vpc/main.tf line 21, in resource "aws_vpc" "this":
│   21:   cidr_block                       = var.cidr
│ 
╵

エラーログから、variableのcidrが適切な値(CIDR範囲が/16から/28)ではないことがわかります。

心得その3 「Inputsタブは惑わされるため、ReadmeタブのInputsを確認しよう!」

Inputsタブとは、Registry ModuleドキュメントのInputsタブを指します。

Inputsタブには、モジュールを使用するにあたり、引数として指定可能な値が記載されています。

AWS VPC Terraform moduleの場合、InputsはRequired InputsOptional Inputsの2つに分かれています。

Required Inputsでは、以下の説明書きがあります。

These variables must be set in the module block when using this module.

 

DeepL翻訳:これらの変数は、本モジュールを使用する際に、モジュールブロックに設定する必要があります。

Required Inputs

v3.11.5では、以下のInputsが、Required Inputsの対象です。

  • database_subnet_assign_ipv6_address_on_creation
  • database_subnet_group_name
  • default_network_acl_name
  • default_route_table_name
  • default_security_group_name
  • default_vpc_name
  • elasticache_subnet_assign_ipv6_address_on_creation
  • elasticache_subnet_group_name
  • enable_classiclink
  • enable_classiclink_dns_support
  • flow_log_cloudwatch_log_group_kms_key_id
  • flow_log_cloudwatch_log_group_retention_in_days
  • flow_log_log_format
  • intra_subnet_assign_ipv6_address_on_creation
  • outpost_arn
  • outpost_az
  • outpost_subnet_assign_ipv6_address_on_creation
  • private_subnet_assign_ipv6_address_on_creation
  • public_subnet_assign_ipv6_address_on_creation
  • redshift_subnet_assign_ipv6_address_on_creation
  • redshift_subnet_group_name
  • vpc_flow_log_permissions_boundary
  • vpn_gateway_az

「いや、多すぎ!」って私は思います。安心してください。デフォルト値で入っています。

このInputsタブが、惑わされポイントなので注意しましょう。

では、引数のリファレンスは、どこを見ればいいのでしょうか。

答えは、「ReadmeタブのInputs」だと私は思います。

ReadmeタブのInputsとは

ReadmeタブのInputs

実は、ReadMeタブにもInputsの項目があります。

Inputsの名前、説明、データの型、デフォルト値、必須の有無が表形式で記載されています。

心得その2 「とりあえずのterraform plan」で怒られた、cidrを確認してみましょう。

The CIDR block for the VPC. Default value is a valid CIDR, but not acceptable by AWS and should be overridden

 

DeepL翻訳:VPCのCIDRブロックです。デフォルト値は有効なCIDRですが、AWSでは受け入れられませんので、オーバーライドする必要があります。

cidr

前述のInputsタブにも記載されていますが、Required Inputsなど紛らわしい項目があるので個人的には、

  1. ReadmeタブのInputsで確認する
  2. わからない場合は、Inputsタブで確認する

の順番で、リファレンスの参照をおすすめします。

引数cidrを上書きして

vpc.tf

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.11.5"
  # insert the 23 required variables here
  cidr = "10.0.0.0/16"
}

terraform planを実行すると、「VPCが作成されますよ!」と表示されました。

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.vpc.aws_vpc.this[0] will be created
  + resource "aws_vpc" "this" {
      + 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                 = false
      + enable_dns_support                   = true
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = ""
        }
      + tags_all                             = {
          + "Name" = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

心得その4 「モジュールの理解はコードを少しだけ読むとこから」

心得その3 「Inputsタブは惑わされるため、ReadmeタブのInputsを確認しよう!」で実行したterraform planでは、作成されるVPCに対して、Nameタグが付与されていませんでした。

では、Nameタグを付与するには、どうすれば良いのでしょうか。

ReadmeタブのInputsでは、多くのInputsが定義されており本質にたどり着くには時間がかかります。

そこで、モジュールのコードを少しだけ確認します。

terraform initterraform get後、Registry Moduleは.terraform/modules/配下にダウンロードされます。

.terraform/modules/vpc/main.tfを確認すると、作成されるVPCは以下のように定義されています。

.terraform/modules/vpc/main.tf

resource "aws_vpc" "this" {
  count = var.create_vpc ? 1 : 0

  cidr_block                       = var.cidr
  instance_tenancy                 = var.instance_tenancy
  enable_dns_hostnames             = var.enable_dns_hostnames
  enable_dns_support               = var.enable_dns_support
  enable_classiclink               = var.enable_classiclink
  enable_classiclink_dns_support   = var.enable_classiclink_dns_support
  assign_generated_ipv6_cidr_block = var.enable_ipv6

  tags = merge(
    { "Name" = var.name },
    var.tags,
    var.vpc_tags,
  )
}

tagsを確認するとNameタグは、var.nameを参照していることがわかります。

つまり、モジュールを理解するには前後しますが、以下のステップが有効だということがわかります。

  1. ReadmeタブのInputsで確認する+αで実際のコードを少し読みInputを探す
  2. わからない場合は、Inputsタブで確認する

コードを再度、書き換え

vpc.tf

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.11.5"
  # insert the 23 required variables here
  cidr = "10.0.0.0/16"
  name = "terraform-reintroduction"
}

terraform planを実行すると、Nameタグが変わっていることが確認できました。

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.vpc.aws_vpc.this[0] will be created
  + resource "aws_vpc" "this" {
      + 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                 = false
      + enable_dns_support                   = true
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "terraform-reintroduction"
        }
      + tags_all                             = {
          + "Name" = "terraform-reintroduction"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

Internet GatewayとPublicサブネットを作成しよう

VPC作成ときたらサブネットも作成したくなります。

ここでは、Internet Gatewayの作成、VPCへのアタッチ、2つのパブリックサブネットの作成、ルートテーブルの開通を行います。

前回に引き続き、コードを少しだけ読んでみましょう。

以下は、Internet Gatewayが定義されているコード部分になります。

.terraform/modules/vpc/main.tf

resource "aws_internet_gateway" "this" {
  count = var.create_vpc && var.create_igw && length(var.public_subnets) > 0 ? 1 : 0

  vpc_id = local.vpc_id

  tags = merge(
    { "Name" = var.name },
    var.tags,
    var.igw_tags,
  )
}

countを確認すると、create_vpc, create_igw, length(var.public_subnets)の全てがtrueの場合に、Internet Gatewayが作成されるようです。

Inputsを確認していきます。

create_vpc, create_igwはデフォルトでtrueとなっているので、length(var.public_subnets)が1以上の場合に、Internet Gatewayは作成されるとわかります。

また、public_subnetsのデフォルトは[]なので、デフォルトの引数値ではInternet Gatewayは作成されないことがわかります。

では、引数public_subnetsには何を入れれば良いのでしょうか。

Inputsのリファレンスを見ても検討がつきません。

A list of public subnets inside the VPC

 

DeepL翻訳:VPC内のパブリックサブネットのリスト

public_subnets

そこで、再度コード内で、public_subnetsの参照を探します。

.terraform/modules/vpc/main.tf

resource "aws_subnet" "public" {
  count = var.create_vpc && length(var.public_subnets) > 0 && (false == var.one_nat_gateway_per_az || length(var.public_subnets) >= length(var.azs)) ? length(var.public_subnets) : 0

  vpc_id                          = local.vpc_id
  cidr_block                      = element(concat(var.public_subnets, [""]), count.index)
  availability_zone               = length(regexall("^[a-z]{2}-", element(var.azs, count.index))) > 0 ? element(var.azs, count.index) : null
  availability_zone_id            = length(regexall("^[a-z]{2}-", element(var.azs, count.index))) == 0 ? element(var.azs, count.index) : null
  map_public_ip_on_launch         = var.map_public_ip_on_launch
  assign_ipv6_address_on_creation = var.public_subnet_assign_ipv6_address_on_creation == null ? var.assign_ipv6_address_on_creation : var.public_subnet_assign_ipv6_address_on_creation

  ipv6_cidr_block = var.enable_ipv6 && length(var.public_subnet_ipv6_prefixes) > 0 ? cidrsubnet(aws_vpc.this[0].ipv6_cidr_block, 8, var.public_subnet_ipv6_prefixes[count.index]) : null

  tags = merge(
    {
      "Name" = format(
        "${var.name}-${var.public_subnet_suffix}-%s",
        element(var.azs, count.index),
      )
    },
    var.tags,
    var.public_subnet_tags,
  )
}

cidr_blockプロパティにて、引数public_subnetsが使用されているため、public_subnetsにはパブリックサブネットのCIDRが入ることがわかります。

コードを書き換え

main.tf

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.11.5"
  # insert the 23 required variables here
  cidr = "10.0.0.0/16"
  name = "terraform-reintroduction"
  public_subnets = ["10.0.0.0/24", "10.0.1.0/24"]
}

terraform planを実行すると、めっちゃ怒られます。

terraform plan
╷
│ Error: Error in function call
│ 
│   on .terraform/modules/vpc/main.tf line 355, in resource "aws_subnet" "public":
│  355:   availability_zone               = length(regexall("^[a-z]{2}-", element(var.azs, count.index))) > 0 ? element(var.azs, count.index) : null
│     ├────────────────
│     │ count.index is 0
│     │ var.azs is empty list of string
│ 
│ Call to function "element" failed: cannot use element function with an empty list.
╵
╷
│ Error: Error in function call
│ 
│   on .terraform/modules/vpc/main.tf line 355, in resource "aws_subnet" "public":
│  355:   availability_zone               = length(regexall("^[a-z]{2}-", element(var.azs, count.index))) > 0 ? element(var.azs, count.index) : null
│     ├────────────────
│     │ count.index is 1
│     │ var.azs is empty list of string
│ 
│ Call to function "element" failed: cannot use element function with an empty list.
╵
╷
│ Error: Error in function call
│ 
│   on .terraform/modules/vpc/main.tf line 356, in resource "aws_subnet" "public":
│  356:   availability_zone_id            = length(regexall("^[a-z]{2}-", element(var.azs, count.index))) == 0 ? element(var.azs, count.index) : null
│     ├────────────────
│     │ count.index is 0
│     │ var.azs is empty list of string
│ 
│ Call to function "element" failed: cannot use element function with an empty list.
╵
####### 省略 #######

理由としては、引数azsが空のリストだからと確認できます。

A list of availability zones names or ids in the region

 

DeepL翻訳:地域内のアベイラビリティゾーンの名前またはIDのリスト

azs

今回は、Inputsから理解できたのでそのままコードを修正します。

vpc.tf

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.11.5"
  # insert the 23 required variables here
  cidr = "10.0.0.0/16"
  name = "terraform-reintroduction"
  public_subnets = ["10.0.0.0/24", "10.0.1.0/24"]
  azs = ["ap-northeast-1a", "ap-northeast-1c"]
}

terraform planでInternet Gatewayの作成、VPCへのアタッチ、2つのパブリックサブネットの作成、ルートテーブルの開通を作成されることが確認できました。

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.vpc.aws_internet_gateway.this[0] will be created
  + resource "aws_internet_gateway" "this" {
      + arn      = (known after apply)
      + id       = (known after apply)
      + owner_id = (known after apply)
      + tags     = {
          + "Name" = "terraform-reintroduction"
        }
      + tags_all = {
          + "Name" = "terraform-reintroduction"
        }
      + vpc_id   = (known after apply)
    }

  # module.vpc.aws_route.public_internet_gateway[0] will be created
  + resource "aws_route" "public_internet_gateway" {
      + destination_cidr_block = "0.0.0.0/0"
      + gateway_id             = (known after apply)
      + id                     = (known after apply)
      + instance_id            = (known after apply)
      + instance_owner_id      = (known after apply)
      + network_interface_id   = (known after apply)
      + origin                 = (known after apply)
      + route_table_id         = (known after apply)
      + state                  = (known after apply)

      + timeouts {
          + create = "5m"
        }
    }

  # module.vpc.aws_route_table.public[0] will be created
  + resource "aws_route_table" "public" {
      + arn              = (known after apply)
      + id               = (known after apply)
      + owner_id         = (known after apply)
      + propagating_vgws = (known after apply)
      + route            = (known after apply)
      + tags             = {
          + "Name" = "terraform-reintroduction-public"
        }
      + tags_all         = {
          + "Name" = "terraform-reintroduction-public"
        }
      + vpc_id           = (known after apply)
    }

  # module.vpc.aws_route_table_association.public[0] will be created
  + resource "aws_route_table_association" "public" {
      + id             = (known after apply)
      + route_table_id = (known after apply)
      + subnet_id      = (known after apply)
    }

  # module.vpc.aws_route_table_association.public[1] will be created
  + resource "aws_route_table_association" "public" {
      + id             = (known after apply)
      + route_table_id = (known after apply)
      + subnet_id      = (known after apply)
    }

  # module.vpc.aws_subnet.public[0] will be created
  + resource "aws_subnet" "public" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "ap-northeast-1a"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.0.0.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = true
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "terraform-reintroduction-public-ap-northeast-1a"
        }
      + tags_all                                       = {
          + "Name" = "terraform-reintroduction-public-ap-northeast-1a"
        }
      + vpc_id                                         = (known after apply)
    }

  # module.vpc.aws_subnet.public[1] will be created
  + resource "aws_subnet" "public" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "ap-northeast-1c"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.0.1.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = true
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "terraform-reintroduction-public-ap-northeast-1c"
        }
      + tags_all                                       = {
          + "Name" = "terraform-reintroduction-public-ap-northeast-1c"
        }
      + vpc_id                                         = (known after apply)
    }

  # module.vpc.aws_vpc.this[0] will be created
  + resource "aws_vpc" "this" {
      + 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                 = false
      + enable_dns_support                   = true
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "terraform-reintroduction"
        }
      + tags_all                             = {
          + "Name" = "terraform-reintroduction"
        }
    }

Plan: 8 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

心得その5 「命名規則は、ローカルで変更しちゃえ!」

作成されるVPC、サブネットのNameタグが、企業の命名規則に、即してないケースもあるのではないでしょうか。

そのような場合は、ローカルにて保存してNameタグ部分を書き換えてしまいましょう。

まずは、.terraform/modules/vpcをコピーします。

|-modules
| |-power_user
| |  |-variables.tf
| |  |-main.tf
| |  |-outputs.tf
| |-vpc <-----------|
|                   |
|-iam.tf            |
|-vpc.tf            |
|-variable.tf       |
|-terraform.tfvars  |
|-.terraform        |
|  |-modules        |
|  | |-modules.json |
|  | |-vpc ---------|
|  |-providers

次にmodule呼び出すコードを書き換えます。

vpc.tf

module "vpc" {
  source  = "../modules/vpc" # モジュールを読み込むファイルパスを変更
  # version = "3.11.5" # ローカルモジュールでは、version指定しないためコメントアウト
  # insert the 23 required variables here
  cidr = "10.0.0.0/16"
  name = "terraform-reintroduction"
  public_subnets = ["10.0.0.0/24", "10.0.1.0/24"]
  azs = ["ap-northeast-1a", "ap-northeast-1c"]
}

terraform initまたは、terraform getでマッピングを変更します。

% terraform init
Initializing modules...

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v3.74.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

モジュールのマッピング変更が完了しました。

次に、ローカルモジュールのコードを書き換えます。

modules/vpc/main.tf

################################################################################
# VPC
################################################################################

resource "aws_vpc" "this" {
  count = var.create_vpc ? 1 : 0

  cidr_block                       = var.cidr
  instance_tenancy                 = var.instance_tenancy
  enable_dns_hostnames             = var.enable_dns_hostnames
  enable_dns_support               = var.enable_dns_support
  enable_classiclink               = var.enable_classiclink
  enable_classiclink_dns_support   = var.enable_classiclink_dns_support
  assign_generated_ipv6_cidr_block = var.enable_ipv6

  tags = merge(
    { "Name" = "${var.name}-vpc" }, # { "Name" = var.name }
    var.tags,
    var.vpc_tags,
  )
}

### 省略 ###

################################################################################
# Internet Gateway
################################################################################

resource "aws_internet_gateway" "this" {
  count = var.create_vpc && var.create_igw && length(var.public_subnets) > 0 ? 1 : 0

  vpc_id = local.vpc_id

  tags = merge(
    { "Name" = "${var.name}-igw" }, # { "Name" = var.name }
    var.tags,
    var.igw_tags,
  )
}

### 省略 ###
################################################################################
# Publiс routes
################################################################################

resource "aws_route_table" "public" {
  count = var.create_vpc && length(var.public_subnets) > 0 ? 1 : 0

  vpc_id = local.vpc_id

  tags = merge(
    { "Name" = "${var.name}-${var.public_subnet_suffix}-rtb" }, # { "Name" = "${var.name}-${var.public_subnet_suffix}" }
    var.tags,
    var.public_route_table_tags,
  )
}

### 省略
################################################################################
# Public subnet
################################################################################

resource "aws_subnet" "public" {
  count = var.create_vpc && length(var.public_subnets) > 0 && (false == var.one_nat_gateway_per_az || length(var.public_subnets) >= length(var.azs)) ? length(var.public_subnets) : 0

  vpc_id                          = local.vpc_id
  cidr_block                      = element(concat(var.public_subnets, [""]), count.index)
  availability_zone               = length(regexall("^[a-z]{2}-", element(var.azs, count.index))) > 0 ? element(var.azs, count.index) : null
  availability_zone_id            = length(regexall("^[a-z]{2}-", element(var.azs, count.index))) == 0 ? element(var.azs, count.index) : null
  map_public_ip_on_launch         = var.map_public_ip_on_launch
  assign_ipv6_address_on_creation = var.public_subnet_assign_ipv6_address_on_creation == null ? var.assign_ipv6_address_on_creation : var.public_subnet_assign_ipv6_address_on_creation

  ipv6_cidr_block = var.enable_ipv6 && length(var.public_subnet_ipv6_prefixes) > 0 ? cidrsubnet(aws_vpc.this[0].ipv6_cidr_block, 8, var.public_subnet_ipv6_prefixes[count.index]) : null

  tags = merge(
    {
      "Name" = format(
        "%s-subnet-${var.public_subnet_suffix}-%s",
        var.name,
        substr(var.azs[count.index], -2, -1),
      )
    },
    /*
    {
      "Name" = format(
        "${var.name}-${var.public_subnet_suffix}-%s",
        element(var.azs, count.index),
      )
    },
    */
    var.tags,
    var.public_subnet_tags,
  )
}

ローカルで、モジュールを管理することによって、命名規則を自由に変更することができました。

しかし、「ローカルでモジュールを管理するということは、バージョンアップのメンテナンスは自身で行うこと」を意味します。

定期的にモジュールの変更ログを確認する必要があることに注意してください。

モジュール書き換え後のterraform plan実行結果

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.vpc.aws_internet_gateway.this[0] will be created
  + resource "aws_internet_gateway" "this" {
      + arn      = (known after apply)
      + id       = (known after apply)
      + owner_id = (known after apply)
      + tags     = {
          + "Name" = "terraform-reintroduction-igw"
        }
      + tags_all = {
          + "Name" = "terraform-reintroduction-igw"
        }
      + vpc_id   = (known after apply)
    }

  # module.vpc.aws_route.public_internet_gateway[0] will be created
  + resource "aws_route" "public_internet_gateway" {
      + destination_cidr_block = "0.0.0.0/0"
      + gateway_id             = (known after apply)
      + id                     = (known after apply)
      + instance_id            = (known after apply)
      + instance_owner_id      = (known after apply)
      + network_interface_id   = (known after apply)
      + origin                 = (known after apply)
      + route_table_id         = (known after apply)
      + state                  = (known after apply)

      + timeouts {
          + create = "5m"
        }
    }

  # module.vpc.aws_route_table.public[0] will be created
  + resource "aws_route_table" "public" {
      + arn              = (known after apply)
      + id               = (known after apply)
      + owner_id         = (known after apply)
      + propagating_vgws = (known after apply)
      + route            = (known after apply)
      + tags             = {
          + "Name" = "terraform-reintroduction-public-rtb"
        }
      + tags_all         = {
          + "Name" = "terraform-reintroduction-public-rtb"
        }
      + vpc_id           = (known after apply)
    }

  # module.vpc.aws_route_table_association.public[0] will be created
  + resource "aws_route_table_association" "public" {
      + id             = (known after apply)
      + route_table_id = (known after apply)
      + subnet_id      = (known after apply)
    }

  # module.vpc.aws_route_table_association.public[1] will be created
  + resource "aws_route_table_association" "public" {
      + id             = (known after apply)
      + route_table_id = (known after apply)
      + subnet_id      = (known after apply)
    }

  # module.vpc.aws_subnet.public[0] will be created
  + resource "aws_subnet" "public" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "ap-northeast-1a"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.0.0.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = true
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "terraform-reintroduction-subnet-public-1a"
        }
      + tags_all                                       = {
          + "Name" = "terraform-reintroduction-subnet-public-1a"
        }
      + vpc_id                                         = (known after apply)
    }

  # module.vpc.aws_subnet.public[1] will be created
  + resource "aws_subnet" "public" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "ap-northeast-1c"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "10.0.1.0/24"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = true
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "terraform-reintroduction-subnet-public-1c"
        }
      + tags_all                                       = {
          + "Name" = "terraform-reintroduction-subnet-public-1c"
        }
      + vpc_id                                         = (known after apply)
    }

  # module.vpc.aws_vpc.this[0] will be created
  + resource "aws_vpc" "this" {
      + 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                 = false
      + enable_dns_support                   = true
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "terraform-reintroduction-vpc"
        }
      + tags_all                             = {
          + "Name" = "terraform-reintroduction-vpc"
        }
    }

Plan: 8 to add, 0 to change, 0 to destroy.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

最後に

以上、『AWS 再入門ブログリレー 2022』の 5日目のエントリ『Terraform Registry Module (AWS VPC Terraform module)』編でした。

Terraform Registry Module使ってみたいけど使い方わからない人に向けて参考になればとても嬉しいです。

明日、2/8 火曜日は、べこみんさんの「CloudShell編」の予定です!

AWS事業本部コンサルティング部のたかくにでした!