0から始めるTerraform

# はじめに

今回はTerraformを触ったことがない学生がある程度できるようになるまでの道のりを記事に起こします。
分かりやすく解説していくので、ぜひご覧ください。

#### 投稿主の前提条件
- 大学院生
- 応用情報技術者取得
- アルバイトでゲーム会社のwebアプリエンジニアとして勤務(2~3年)
- aws経験はボチボチ
- 圧倒的Mac派
- 書ける言語
    - Python
    - Go
    - Java
    - Javascript
    - Vue.js
    - Google App Script

# Terraformとは？

Terraform（テラフォーム）は、HashiCorp社によって開発された、オープンソースのインフラストラクチャー・コード化ツールです。このツールにより、クラウドリソースやデータセンターのリソースを、コードによって管理・構築することができます。

## 特徴

### 1. クラウドプロバイダーに依存しない

Terraformは、AWS、Azure、Google Cloud Platformなど、多くのクラウドプロバイダーと連携することができます。

```hcl
provider "aws" {
region = "us-west-2"
}
```

### 2. 宣言的なコード

Terraformは、宣言的なコードによりインフラストラクチャーを定義します。何をしたいのかを書くだけで、その通りにリソースが構築されます。

```hcl
resource "aws_instance" "example" {
ami = "ami-123456"
instance_type = "t2.micro"
}
```

このコードは、Terraformを使用してAWS上に仮想マシンを作成するための宣言的な定義です。
　具体的には以下の設定が含まれています。

- `resource "aws_instance" "example"`: AWS上にインスタンスを作成するリソースの定義で、このリソースには名前として"example"が付けられています。
- `ami = "ami-123456"`: 使用するAmazon Machine Image（AMI）のIDを指定しています。このIDは、インスタンス作成時に使用するOSやソフトウェアがプリインストールされたイメージを表します。
- `instance_type = "t2.micro"`: インスタンスのサイズを指定しています。この例では、"t2.micro"タイプのインスタンスが作成されます。

このコードをTerraformで実行すると、指定されたAMIとインスタンスタイプを使用して、AWS上に新しい仮想マシンが作成されます。

詳しい構文は次の章で紹介します。

### 3. コードによるバージョン管理

インフラのコード化により、バージョン管理が可能となり、変更履歴を容易に追跡できます。

## インフラストラクチャーのライフサイクル

Terraformの操作は主に以下のステップから構成されます。

1. **初期化**：`terraform init`
2. **プラン作成**：`terraform plan`
3. **適用**：`terraform apply`
4. **破棄**：`terraform destroy`

# Terraformの基本構文

## terraformブロック

`terraform` ブロックは、Terraformの設定に使用されます。バックエンドの設定、プロバイダーのバージョンの制約など、Terraformの動作に関連する設定を行います。

### プロバイダーのバージョン制約

`required_providers` ブロックを使用して、プロバイダーのバージョンを指定することができます。

```hcl
terraform {
  required_version = ">=1.3"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 2.0"
    }
  }
}
```

この設定により、AWSプロバイダーのバージョン2.0以上、3.0未満を使用するように指定しています。




### バックエンドの設定

バックエンドは、Terraformの状態を保存する場所を定義します。以下はS3バケットをバックエンドとして使用する例です。

```hcl
terraform {
  backend "s3" {
    bucket = "my-tf-state-bucket"
    key    = "terraform.tfstate"
    region = "us-west-2"
  }
}
```

## 変数

変数は、コード内で何度も使用する値を一元管理するために使用されます。変数を使用することで、コードの再利用性とメンテナンス性が向上します。

```hcl
variable "region" {
  description = "AWSのリージョン"
  default     = "us-west-2"
}
```

## リソース

リソースブロックは、クラウドインフラストラクチャー内の物理的なコンポーネントを定義します。以下はAWSのS3バケットを作成する例です。

```hcl
resource "aws_s3_bucket" "bucket" {
  bucket = "my-tf-test-bucket"
  acl    = "private"
}
```

## データソース

データソースブロックは、既存のクラウドインフラストラクチャーのコンポーネントにアクセスするために使用されます。

```hcl
data "aws_ami" "example" {
  most_recent = true
  owners      = ["self"]
}
```

## 出力

出力ブロックは、Terraformが構築または変更したインフラストラクチャーに関する情報を出力するために使用されます。

```hcl
output "bucket_name" {
  value = aws_s3_bucket.bucket.id
}
```

## Standard Module Structure
基本的にはhashicorpのTerraformのstandard moduleに沿ってフォルダ構成をすると良いみたいです。
以下参照

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


以下は、最小限のスタンダードモジュールで構成されたものです。
```
$ tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
```


以下は、全てのオプション要素が含まれているモジュール構成です。

```
$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│   ├── nestedA/
│   │   ├── README.md
│   │   ├── variables.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   ├── nestedB/
│   ├── .../
├── examples/
│   ├── exampleA/
│   │   ├── main.tf
│   ├── exampleB/
│   ├── .../

```



# セットアップ編 (Mac)

Mac上でTerraformをセットアップするための手順を以下に説明します。

## tfenvのインストール

`tfenv`はTerraformのバージョン管理ツールで、プロジェクトごとに異なるバージョンのTerraformを使用することができます。Mac上でのインストール方法は以下の通りです。

:::note info
**tfenvの使用は必須ではありませんが、使用した方がバージョンの管理が楽になります。**
:::



### Homebrewを使用したインストール

Homebrewを使用して、`tfenv`をインストールします。

```bash:ターミナル
$ brew install tfenv
```

### インストールの確認

以下のコマンドを実行して、`tfenv`が正しくインストールされたことを確認します。

```bash:ターミナル
$ tfenv --version
```

### Terraformのバージョンのインストールと選択

`tfenv`を使用して、特定のバージョンのTerraformをインストールし、使用するバージョンを選択することができます。

```bash:ターミナル
$ tfenv install 1.3.9
$ tfenv use 1.3.9
```

これで、プロジェクトで使用するTerraformのバージョンを0.12.0に設定しました。

### 使用できるバージョンの確認
```bash:ターミナル
$ tfenv list
* 1.3.9 (set by /Users/local/.tfenv/version)
  1.3.8
```


`tfenv list-remote`でインストール可能なバージョン一覧が確認できます。



## Terraformのインストール

Homebrewを使用して、Terraformをインストールします。

```bash:ターミナル
$ brew tap hashicorp/tap
$ brew install hashicorp/tap/terraform
```

## インストールの確認

以下のコマンドを実行して、Terraformが正しくインストールされたことを確認します。

```bash:ターミナル
$ terraform -version
```

このコマンドは、インストールされたTerraformのバージョンを表示します。

## 初期化

プロジェクトディレクトリに移動し、Terraformを初期化します。

```bash:ターミナル
$ cd your_project_directory
$ terraform init
```

このコマンドは、プロジェクトに必要なプロバイダーをダウンロードし、Terraformを初期化します。



# 使ってみる ~基本編~

## initをする



```bash:ターミナル
$ mkdir terraform_test
$ cd terraform_test
```



terraform initをする前に、
initの為にtfstateを保存するS3バケットを作る必要があります。




今回は`terraform-paupau-tfstate`という名前でバケットを作成してみます。



![スクリーンショット 2023-08-14 17.05.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/4500fb27-42b5-8588-ef81-394ef1442b52.png)

![スクリーンショット 2023-08-14 2.03.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/d8acbdb3-c415-de83-b8eb-13d7a626b688.png)

次に、S3の情報を`backend.tf`に落とし込んでいきます。
terraformの公式ドキュメントが充実しているので、テンプレートを引っ張ってきます。

かなりドキュメントが充実しているので、初学者にはとても嬉しいですね！


https://developer.hashicorp.com/terraform/language/settings/backends/s3

テンプレを自分の環境に置き換えて、以下のようなものを作りました。


```terraform:main.tf
  provider "aws"{
    region = "ap-northeast-1"
  }
```


```terraform:backend.tf
terraform {
    required_version = "1.5.5"
  backend "s3" {
    bucket = "terraform-paupau-tfstate"
    key    = "terraform.tfstate"
    region = "ap-northeast-1"
  }
}
```


:::note info
**VScodeの拡張機能にterraformの機能があり、補完が効いて楽なので積極的に使いましょう！**
:::


次に`terraform init`で初期化をしようとすると...


```bash:ターミナル

$ terraform init

Initializing the backend...
Terraform encountered problems during initialisation, including problems
with the configuration, described below.

The Terraform configuration must be valid before initialization so that
Terraform can determine which modules and providers need to be installed.
╷
│ Error: error configuring S3 Backend: error validating provider credentials: error calling sts:GetCallerIdentity: InvalidClientTokenId: The security token included in the request is invalid.
│       status code: 403, request id: xxxxxx

```

あれ、認証できない...?

そうです。AWS CLIの設定が出来ていませんでした。
configとcredentialsの設定をします。




```bash:ターミナル
$ cat ~/.aws/credentials                                      
[default]
aws_access_key_id = hogehoge
aws_secret_access_key = hogehoge

$ cat ~/.aws/config                      
[default]
region = ap-northeast-1
output = json

```

これでバッチリです！

```bash:ターミナル
.terraform % terraform init -reconfigure

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v5.12.0...
- Installed hashicorp/aws v5.12.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

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.
```

**initが成功しました！**

## VPCとサブネットを作ってみる

こちらもterraformのドキュメントから引っ張ってきました。
VPCのドキュメントは[こちら](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc)
サブネットのドキュメントは[こちら](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet)


vpc.tfを作ってみます。


```terraform:vpc.tf
resource "aws_vpc" "main" {
  cidr_block = "192.168.1.0/24"

    tags = {
    Name = "Main-vpc"
  }
}

resource "aws_subnet" "main_1a" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "192.168.1.0/25"
  availability_zone = "ap-northeast-1a"
}
```


次に`terraform validate`で構文が正しいか、確認してみます。
Terraformの設定ファイルが正しい構文と構造を持っているかをチェックするために使用されます。

```bash:ターミナル
$ terraform validate
Success! The configuration is valid.
```
順調です。



次に`terraform plan`で確認してみます。
tfstateと現在のコードの内容の差分を見て、状態を教えてくれます。
(長いので少し省略してます)

```bash:ターミナル
% terraform plan

・
・
・
Plan: 2 to add, 0 to change, 0 to destroy.
```

２個作成予定が確認できました。

余談ですが`terraform fmt`でフォーマットを綺麗にしてくれます。

ようやく、`terraform apply`してみます。

```bash:ターミナル
$ 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_subnet.main_1a will be created
  + resource "aws_subnet" "main_1a" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "ap-northeast-1a"
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "192.168.1.0/25"
      + 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                        = false
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags_all                                       = (known after apply)
      + vpc_id                                         = (known after apply)
    }

  # aws_vpc.main will be created
  + resource "aws_vpc" "main" {
      + arn                                  = (known after apply)
      + cidr_block                           = "192.168.1.0/24"
      + 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" = "Main-vpc"
        }
      + tags_all                             = {
          + "Name" = "Main-vpc"
        }
    }

Plan: 2 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_vpc.main: Creating...
╷
│ Error: creating EC2 VPC: operation error EC2: CreateVpc, https response error StatusCode: 403, RequestID: 71b56b4f-b2c5-4b51-9138-a859a5707eb3, api error UnauthorizedOperation: You are not authorized to perform this operation. Encoded authorization failure message: HFvjs_g1WA1DvLX7IkOqRVkEP6BRUVXdIgl2l2Z6TBm69UahT9QAnCvHbZSDVviMDyucCaXAsOpuz2vls0JTMEiuLMM6CGhluUggZp8VdlaMmmHfDfH
```

あれ、うまく行ってないようです。詳細内容がエンコードされていますね...

`aws sts decode-authorization-message --encoded-message "encode_message"`
を使ってデコードしてみましょう。

デコードするには、IAMのポリシーに`DecodeAuthorizationMessage`を追加する必要があります。
IAM > ポリシー > ポリシーを作成 > JSON まで行って、

```json:ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:DecodeAuthorizationMessage",
            "Resource": "*"
        }
    ]
}
```

と入力し、ポリシーを作成します。
![スクリーンショット 2023-08-15 3.16.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/a9bb36d3-8d89-5834-ec69-f0780ed3f460.png)

次に今作ったポリシーをアタッチします。


![スクリーンショット 2023-08-15 3.17.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/d130c6a4-03f9-cead-3784-4160ae918463.png)

![スクリーンショット 2023-08-15 3.18.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/1d83538c-0fb2-1211-af00-9e4e2fd955a7.png)


無事アタッチできたようです。

先ほどのをデコードし、エラーを解析してみると、
どうやら、vpcやサブネット周りのポリシーが必要だったみたいです。
以下、ポリシーのJSONです。


```json:ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateVpc",
                "ec2:DescribeVpcs",
                "ec2:DeleteVpc",
                "ec2:ModifyVpcAttribute",
                "ec2:CreateTags",
                "ec2:DeleteTags",
                "ec2:DescribeTags",
                "ec2:CreateSubnet",
                "ec2:DescribeSubnets",
                "ec2:DeleteSubnet",
                "ec2:DescribeVpcAttribute"
            ],
            "Resource": "*"
        }
    ]
}
```



ポリシーを作成しアタッチしました。

再び`terraform apply`に挑戦します。


```bash:ターミナル

$ terraform 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.main: Destroying... [id=vpc-06e030806]
aws_vpc.main: Destruction complete after 0s
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 1s [id=vpc-0e77b6339378]
aws_subnet.main_1a: Creating...
aws_subnet.main_1a: Creation complete after 1s [id=subnet-000f00088170b]

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


うまく作成できたようです！！
確認してみましょう！
![スクリーンショット 2023-08-15 3.38.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1936739/b959c017-842b-6653-7172-8f141a8da78a.png)

vpcとサブネットが上手くできているようです！


**S3にあるtfstateも更新されていますね^^**





# 使ってみる ~応用編~

## モジュール化・変数化してみる


以下のようなツリーでモジュール化と変数化を実装してみました。
```
$ tree
.
├── backend.tf
├── modules
│   └── vpc_subnet
│       ├── main.tf
│       └── variables.tf
└── use_module.tf
```

backend.tfは引き続き同じソースコードです。


```terraform:main.tf
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr_block

  tags = {
    Name = "Main-vpc"
  }
}

resource "aws_subnet" "main_1a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.subnet_1a_cidr_block
  availability_zone = "ap-northeast-1a"
}

```


```terraform:variables.tf
variable "vpc_cidr_block" {
  type = string
}

variable "subnet_1a_cidr_block" {
  type = string
}
```

```terraform:use_module.tf
module "vpc_subnet1" {
    source = "./modules/vpc_subnet/"

    vpc_cidr_block = "192.168.1.0/24"
    subnet_1a_cidr_block = "192.168.1.0/25"
}

module "vpc_subnet2" {
    source = "./modules/vpc_subnet/"
    
    vpc_cidr_block = "192.168.12.0/24"
    subnet_1a_cidr_block = "192.168.2.0/25"
}

```



## workspaceの使用

workspaceで作業環境を分けてみたいと思います。
`stage`と`master`を作ってみます。


```:ターミナル
$ terraform workspace list
* default

$ terraform workspace new "stage"
Created and switched to workspace "stage"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.


$ terraform workspace new "master"
Created and switched to workspace "master"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

```

その後、使用するところまで、作ってみます。

```terraform:workspace.tf

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr_block

  tags = {
    Name = "${terraform.workspace}-test-vpc" //現在選ばれているworkspaceが入る
  }
}

```




# まとめ

今回は、Terraform未経験の学生が基礎的な内容に挑戦してみました。
誰かの参考になればと思います。
(間違いあれば指摘お願いいたします！)