【Terraform/AWS】【入門編3】フォルダ構成と各ファイルの役割について

# はじめに
本記事は、AWS初心者でIaCに挑戦してみたい方向けに導入～コード実行までをまとめた内容となります。
今回は、tfファイルの構成についてお話しします。

- 記事構成
<cite><font size="1">[①【Terraform/AWS】【入門編1】環境準備](https://qiita.com/rittan_girla/items/eefc45c1f9dd0a1761fa/)</font></cite>
<cite><font size="1">[②【Terraform/AWS】【入門編2】tfファイル・Terraformコマンドについて](https://qiita.com/rittan_girla/items/7dbd9a9da842ccbf8508/)</font></cite>
<cite><B><font size="1">[③【Terraform/AWS】【入門編3】tfファイル構成について](https://qiita.com/rittan_girla/items/37458a11c14dacce9ca0/)</B></font></cite><B>　★本記事</B>
<cite><font size="1">[④【Terraform/AWS】【入門編4】モジュールについて](https://qiita.com/rittan_girla/items/81077f1493609eb7a6ca/)</font></cite>

# 前回の復習
簡単なコードを使って、terraformコマンドの一連の操作を行いました。
前回使ったコードは、下記のように全ての情報を一つのファイル(main.tf)に記述していました。
しかし、この状態では構築するリソースが増えた時に分かりづらくなります。
また、変数を使用していないため、amiやinstance_type、Tags等のインスタンス設定値を変更する場合、コード修正を行っていかなければならないため、管理が複雑になってしまいます。
そのため、各処理に応じたファイルに分割していくことが推奨されます。


```main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "ap-northeast-1"
}

# Create an EC2 Instance
resource "aws_instance" "example" {
  ami           = "ami-0b5c74e235ed808b9"
  instance_type = "t2.micro"

  tags = {
    Name = "TEST1"
  }
}
```

# 1. Terraformファイル構成

Terraformのフォルダ・ファイル構成のベストプラクティスは下記に記載があります。
詳細な内容については、下記を参考にしてください。

https://developer.hashicorp.com/terraform/language/modules/develop/structure


## 1-2. 最小限のファイル構成

最小限の推奨モジュール構成です。
前回例に出したコードレベルであればこの構成で十分ですが、実際の構築は多くのリソースを組み合わせていくため、あまり使用しない構成になります。

>```
>.
>├── README.md
>├── main.tf
>├── variables.tf
>├── outputs.tf
>```
※引用元：https://developer.hashicorp.com/terraform/language/modules/develop/structure

## 1-3. モジュール構成

モジュール構成の例です。
例えば、nestedAにEC2、nestedBにVPCといったようにリソースタイプ毎にモジュールを分けた階層型の構造です。
一般的にはこちらの構成で構築することが多いと思います。

- ルートモジュール（親モジュール）：terraformを実行するモジュール
- 子モジュール：下記でいうmodules/配下にあるモジュール(nestedAやnestedB)

>```
>.
>├── README.md
>├── main.tf
>├── variables.tf
>├── outputs.tf
>├── ...
>├── modules/
>│   ├── nestedA/
>│   │   ├── README.md
>│   │   ├── variables.tf
>│   │   ├── main.tf
>│   │   ├── outputs.tf
>│   ├── nestedB/
>│   ├── .../
>├── examples/
>│   ├── exampleA/
>│   │   ├── main.tf
>│   ├── exampleB/
>│   ├── .../
>```

※引用元：https://developer.hashicorp.com/terraform/language/modules/develop/structure

# 2. Terraformファイル

「1. Terraformファイル構成」で紹介したファイルの用途と前回の復習に記載している「main.tf」を最小限のファイル構成に当てはめて修正した内容を記載していきます。

## 2-1. README.md
モジュールの内容や構成についてを記載するファイルです。
Markdownで記載します。
※特に処理に影響する設定ではないため、今回は割愛します。

>There should be a description of the module and what it should be used for.

※引用元：https://developer.hashicorp.com/terraform/language/modules/develop/structure

## 2-2. variables.tf
変数を定義するファイルです。

```variables.tf

variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-1"
}

variable "ec2_instance_type" {
  description = "AWS EC2 instance type."
  type        = string
  default     = "t2.micro"
}

variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."
  default     = "ami-0b5c74e235ed808b9"
}

variable "resource_tags" {
  type        = string
  description = "AWS EC2 instance tags."
  default     = {
     Name = "TEST1"
  }
}

```

### 余談

terraform.tfvarsを使って変数を管理する方法もあります。
variables.tfでは変数名と型を定義して、terraform.tfvarsに値を定義します。

```variables.tf

variable "aws_region" {
  description = "AWS region"
  type        = string
}

variable "ec2_instance_type" {
  description = "AWS EC2 instance type."
  type        = string
}

variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."
}

variable "resource_tags" {
  type        = string
  description = "AWS EC2 instance tags."
}

```

```terraform.tfvars
aws_region = "ap-northeast-1
ec2_instance_type = "t2.micro"
image_id = "ami-0b5c74e235ed808b9"
resource_tags = {
  Name = "TEST1"
  }
```

※参考：https://developer.hashicorp.com/terraform/tutorials/cli/variables

## 2-3. main.tf
メインの処理を記載します。
変数値はvar.変数名で取得することができます。

```main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = var.aws_region
}

# Create an EC2 Instance
resource "aws_instance" "example" {
  ami           = var.image_id
  instance_type = var.ec2_instance_type

  tags = var.resource_tags
}
```

## 2-4.output.tf

一番イメージが湧きにくいですが、簡単にいうと戻り値を取得するための設定ファイルです。
用途は下記の三つがあります。

>- A child module can use outputs to expose a subset of its resource attributes to a parent module.
>- A root module can use outputs to print certain values in the CLI output after running terraform apply.
>- When using remote state, root module outputs can be accessed by other configurations via a terraform_remote_state data source.

引用元：https://developer.hashicorp.com/terraform/language/values/outputs#accessing-child-module-outputs

分かりにくいのでざっくり和訳します。
1. 子モジュールの出力を親モジュールに公開できる
2. terraform apply実行後にCLI上に値を出力できる
3. remote stateを使う場合、terraform_remote_stateデータを用いて、他の構成からルートモジュールの出力にアクセスできる

### 2-4-1. 子モジュールの出力を親モジュールに公開できる
例えば、networkモジュールのVPC IDをsecuritygroupモジュールで使用したい時、子モジュール同士は値の参照ができません。そのため、network/outputs.tfにVPC IDを出力して、親モジュールに渡します。親モジュールからsecuritygroupモジュールに値を受け渡すことでVPC IDを連携できます。

#### 2-4-1-1. モジュール例
```
.
├── main.tf
├── modules/
│   ├── network/
│   │   ├── main.tf
│   │   ├── outputs.tf
│   ├── securitygroup/
│   │   ├── variables.tf
│   │   ├── main.tf
```
#### 2-4-1-2. 処理の流れのまとめ
- networkモジュールでVPCを構築
- 構築したVPCのIDを出力
- 親モジュールからnetworkモジュールで出力された値を取得
- 取得した値をsecuritygroupのvpc_idで使用

#### 2-4-1-3. ルートモジュール
子モジュールであるnetworkとsecuritygroupを呼び出しています。
注目する部分は、network_vpc_idと記載されている行です。
securitygroupモジュールに対して、networkモジュールの値を使用しています。

```main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "ap-northeast-1"
}

module "network" {
  source = "./modules/network"
}

module "securitygroup" {
  source         = "./modules/securitygroup"
  network_vpc_id = module.network.vpc_id
}
```

#### 4. networkモジュール

- VPCを構築モジュール
  ```modules/network/main.tf
  resource "aws_vpc" "example" {
      cidr_block = "10.0.0.0/16"
      tags = {
          Name = "example-vpc"
        }
  }
  ```
　
 - 出力モジュール
 構築したVPCのIDを出力させるモジュール
   ```modules/network/output.tf
    output "vpc_id" {
      value = aws_vpc.example.id
   }
   ```
#### 2-4-1-5. securitygroupモジュール

- ルートモジュールで参照したnetworkモジュールの出力値用の変数定義

  ```modules/securitygroup/variables.tf
  variable "network_vpc_id" {
      type = string
      description = "AWS VPC ID"
  }
  ```
　
- セキュリティグループを構築するモジュール
vpc_idで直接networkモジュールの値を拾えないため、ルートモジュールから出力値を拾っています。
  ```modules/securitygroup/main.tf
  resource "aws_security_group" "example" {
      name = "example-securitygroup"
      vpc_id = var.network_vpc_id
  
      tags = {
         Name = "example-securitygroup" 
      }
  }
  ```

### 2-4-2. terraform apply実行後にCLI上に値を出力できる

文字通り、terraform applyを実行後の画面に、outputs.tfで定義した内容が表示されます。

#### 2-4-2-1. モジュール例
例えば、下記二つのファイルを使って、terraform applyを実行してみます。

```main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "ap-northeast-1"
}

# Create an EC2 Instance
resource "aws_instance" "example" {
  ami           = "ami-07c589821f2b353aa"
  instance_type = "t2.micro"

  tags = {
    Name = "TEST1"
  }
}
```
```output.tf
output "instance_id" {
  value = aws_instance.example.id
}
```
#### 2-4-2-2. 実行結果

「Outputs：」の以降で、output.tfで指示した通りにインスタンスIDが表示されています。

```powershell
PS F:\work\terraform\test1> terraform apply

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

・・・（途中省略）・・・

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

instance_id = "i-******************"
```

### 2-4-3. remote stateを使う場合、terraform_remote_stateデータを用いて、他の構成からルートモジュールの出力にアクセスできる

前回terraform.tfstateは更新履歴を保存しているファイルだということを説明しました。
terraform.tfstateは変更時に競合する場合があるため、一般的には共有フォルダ（AWSであればS3）で管理します。
例えば、outputs.tfを含んだterraformモジュールを実行した場合、terraform.tfstateにoutputs.tfで出力した情報が書き込まれます。
他のプロジェクトのモジュールで、その出力した内容を利用したい場合、terraform.tfstateを参照させることで値を再利用することができます。

下記の例で、terraform_remote_stateの処理について記載します。
なお、リモート共有フォルダの準備ができなかったため、ローカルでリモートフォルダに見立てた構成としています。

#### 2-4-3-1. モジュール例

projectAとprojectBは独立したモジュールとします。
また、backendは共有フォルダと見立てたローカルフォルダです。

```
├── projectA/
│   ├── main.tf
│   ├── outputs.tf
│
├── projectB/
│   ├── main.tf
│
├── backend/　　　　　　　　　  ※リモートフォルダに見立てたフォルダ
│   ├── terraform.tfstate　　　 ※projectAを実行すると作成される
```

#### 2-4-3-2. projectA内ファイル

- main.tf
backendをローカルとして、backend配下のterraform.tfstateを参照するようにしています。
処理としては、VPCを構築する内容です。

```projectA/main.tf
terraform {
  backend "local" {
    path = "../backend/terraform.tfstate"
  }

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "example-vpc"
  }
}
```

- outputs.tf
構築したVPC IDを出力させています。

```projectA/outputs.tf
output "vpc_id" {
  value = aws_vpc.example.id
}
```

#### 2-4-3-3. projectB内ファイル

- main.tf

```projectB/main.tf
data "terraform_remote_state" "remote" {
  backend = "local"

  config = {
    path = "../backend/terraform.tfstate"
  }
}

resource "aws_security_group" "example" {
  name   = "example-securitygroup"
  vpc_id = data.terraform_remote_state.remote.outputs.vpc_id

  tags = {
    Name = "example-securitygroup"
  }
}
```

参考：https://developer.hashicorp.com/terraform/language/state/remote-state-data

#### 2-4-3-4. projectA実行

Outputs:行から、outputs.tfでVPC IDを出力していることが分かります。

```
PS F:\work\terraform\projectA> terraform apply  

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:

  # aws_vpc.example will be created
  + resource "aws_vpc" "example" {
      + arn                                  = (known after apply)
      + 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_dns_hostnames                 = (known after apply)
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + 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" = "example-vpc"
        }
      + tags_all                             = {
          + "Name" = "example-vpc"
        }
    }

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

Changes to Outputs:
  + vpc_id = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_vpc.example: Creating...
aws_vpc.example: Creation complete after 1s [id=vpc-99999999999999999]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

vpc_id = "vpc-99999999999999999"
```

#### 2-4-3-5. projectB実行

「data.terraform_remote_state.backend: Reading...」より、backend/terraform.tfstateからデータを取得していることが分かります。
vpc_idには先ほど出力させた「vpc-99999999999999999」が入力されています。

```
PS F:\work\terraform\test_tfstate_2> terraform apply
data.terraform_remote_state.backend: Reading...
data.terraform_remote_state.backend: Read complete after 0s

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:

  # aws_security_group.example will be created
  + resource "aws_security_group" "example" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = "example-securitygroup"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags                   = {
          + "Name" = "example-securitygroup"
        }
      + tags_all               = {
          + "Name" = "example-securitygroup"
        }
      + vpc_id                 = "vpc-99999999999999999"
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_security_group.example: Creating...
aws_security_group.example: Creation complete after 1s [id=sg-99999999999999999]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

#### 2-4-3-6. 実行後terraform.tfstateファイルについて

projectAとprojectBでterraform.tfstateが作成される場所が異なります。
.tfstateの作成場所は、terraform{}のbackend{}で指定できます。
※今回はprogectA/main.tfの冒頭で定義しています。

- projectA実行後
下記ファイルがbackendフォルダ内に作成されます。
上から6行目にprojectA/outputs.tfで指定した内容が書き込まれています。

```backend/terraform.tfstate
{
  "version": 4,
  "terraform_version": "1.7.3",
  "serial": 6,
  "lineage": "************",
  "outputs": {
    "vpc_id": {
      "value": "vpc-************",
      "type": "string"
    }
  },
  "resources": [
    {
      "mode": "managed",
      "type": "aws_vpc",
      "name": "example",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "arn": "arn:aws:ec2:ap-northeast-1:************:vpc/vpc-************",
            "assign_generated_ipv6_cidr_block": false,
            "cidr_block": "10.0.0.0/16",
            "default_network_acl_id": "acl-************",
            "default_route_table_id": "rtb-************",
            "default_security_group_id": "sg-************",
            "dhcp_options_id": "dopt-************",
            "enable_dns_hostnames": false,
            "enable_dns_support": true,
            "enable_network_address_usage_metrics": false,
            "id": "vpc-************",
            "instance_tenancy": "default",
            "ipv4_ipam_pool_id": null,
            "ipv4_netmask_length": null,
            "ipv6_association_id": "",
            "ipv6_cidr_block": "",
            "ipv6_cidr_block_network_border_group": "",
            "ipv6_ipam_pool_id": "",
            "ipv6_netmask_length": 0,
            "main_route_table_id": "rtb-************",
            "owner_id": "************",
            "tags": {
              "Name": "example-vpc"
            },
            "tags_all": {
              "Name": "example-vpc"
            }
          },
          "sensitive_attributes": [],
          "private": "************"
        }
      ]
    }
  ],
  "check_results": null
}
```

- projectB実行後
先ほどのbackend/terraform.tfstateとは別にprojectB内でterraform.tfstateが作成されます。
9行目の"mode": "data",から38行目の}までがbackend/terraform.tfstateを参照して、VPC IDを取得している処理となります。

```projectB/terraform.tfstate
{
  "version": 4,
  "terraform_version": "1.7.3",
  "serial": 6,
  "lineage": "************",
  "outputs": {},
  "resources": [
    {
      "mode": "data",
      "type": "terraform_remote_state",
      "name": "backend",
      "provider": "provider[\"terraform.io/builtin/terraform\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "backend": "local",
            "config": {
              "value": {
                "path": "../backend/terraform.tfstate"
              },
              "type": [
                "object",
                {
                  "path": "string"
                }
              ]
            },
            "defaults": null,
            "outputs": {
              "value": {
                "vpc_id": "vpc-************"
              },
              "type": [
                "object",
                {
                  "vpc_id": "string"
                }
              ]
            },
            "workspace": null
          },
          "sensitive_attributes": []
        }
      ]
    },
    {
      "mode": "managed",
      "type": "aws_security_group",
      "name": "example",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "arn": "arn:aws:ec2:ap-northeast-1:************:security-group/sg-************",
            "description": "Managed by Terraform",
            "egress": [],
            "id": "sg-************",
            "ingress": [],
            "name": "example-securitygroup",
            "name_prefix": "",
            "owner_id": "************",
            "revoke_rules_on_delete": false,
            "tags": {
              "Name": "example-securitygroup"
            },
            "tags_all": {
              "Name": "example-securitygroup"
            },
            "timeouts": null,
            "vpc_id": "vpc-************"
          },
          "sensitive_attributes": [],
          "private": "************",
          "dependencies": [
            "data.terraform_remote_state.backend"
          ]
        }
      ]
    }
  ],
  "check_results": null
}
```

# おわりに
今回はTerraformのフォルダ構成と各ファイルの役割についてご紹介しました。
次回はmain.tfの書き方について、もう少し深堀していきたいと思います。

# 参考
https://developer.hashicorp.com/terraform/language/modules
https://developer.hashicorp.com/terraform/language/values/variables
https://developer.hashicorp.com/terraform/tutorials/cli/outputs
https://developer.hashicorp.com/terraform/language/values/outputs#output-values
https://developer.hashicorp.com/terraform/language/state/remote
https://developer.hashicorp.com/terraform/language/state/remote-state-data
https://developer.hashicorp.com/terraform/language/settings/backends/local