Terraformを使ったAWS環境構築を試してみる

# はじめに
Terraformを使ったAWS環境構築をやることになったため、
最低限実行環境や使用するファイル等を抑えておくために
とりあえず触ってみたという記事になります。

Terraformに関する私の知識としてIaCツールでサーバや
各社のパブリッククラウドのインフラ環境をコードで
自動構築できるツールだという認識はあるものの実際に使用したことはありません。

# ドキュメント
公式サイトのAWS向けチュートリアルをなぞっていきます。

https://www.terraform.io/

https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli

# 実行環境構築
## 環境
### 接続元端末
- OS:Windows 11
- SSHクライアント:TeraTerm 5.0

### Terraform実行用サーバ
- EC2インスタンス(Linux):AmazonLinux2023
AMI:ami-0dfa284c9d7b2adad
インスタンスサイズ:t2.micro
AWS CLI:2.14.5
(Terraformで構築する環境とは別VPC上のパブリックサブネット上に作成済み)

## Terraformインストール
```:コマンド
# yum-utilsの導入
sudo yum install -y yum-utils
# リポジトリ追加
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
# Terraformインストール
sudo yum -y install terraform
# 確認
terraform -version
```

```:実行結果
[ec2-user@ip-192-168-0-5 ~]$ terraform -version
Terraform v1.6.6
on linux_amd64
```

AWS環境構築のためTerraformの他AWS CLIが必要です。(今回はAL3利用のためデフォルトで導入済み)
AWS CLIの実行ユーザーには権限周りで躓くのを防ぐため、とりあえずPowerUserAccessを付与しています。

# ディレクトリ・ファイル準備

```:コマンド
# チュートリアル用ディレクトリ作成
mkdir learn-terraform-aws-instance
cd learn-terraform-aws-instance
# チュートリアル用ファイル作成
touch main.tf
```
```:確認結果
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ pwd
/home/ec2-user/learn-terraform-aws-instance
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ ll
total 0
-rw-r--r--. 1 ec2-user ec2-user 0 Dec 23 05:08 main.tf
```

作成した`main.tf`に公式ドキュメント記載の内容を貼り付けます。
ドキュメント内のTipsに下記記載があるのでリージョン指定を東京(ap-northeast-1)、
AMI IDを東京リージョンのAL3のAMI IDに変更しておきます。
> The AMI ID used in this configuration is specific 
> to the us-west-2 region. If you would like to use a different region,
> see the Troubleshooting section for guidance.

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

  required_version = ">= 1.2.0"
}

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

resource "aws_instance" "app_server" {
  ami           = "ami-0dfa284c9d7b2adad"
  instance_type = "t2.micro"

  tags = {
    Name = "ExampleAppServerInstance"
  }
}
```

# Terraform作業ディレクトリ初期化
### Terraform作業ディレクトリの準備
リソースファイルが配置されたディレクトリをTerraformに読み込ませます。
```:コマンド
terraform init
```

```:実行結果
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ pwd
/home/ec2-user/learn-terraform-aws-instance

# terraform initの実行
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 4.16"...
- Installing hashicorp/aws v4.67.0...
- Installed hashicorp/aws v4.67.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!

(省略)
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$
```

Terraform作業ディレクトリの初期化が完了すると`terraform init`を実行したディレクトリ配下に
`.terraform`ディレクトリと`.terraform.lock.hcl`ファイルが生成されます。

```
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ ls -al
total 8
drwxr-xr-x. 3 ec2-user ec2-user   66 Dec 23 05:28 .
drwx------. 6 ec2-user ec2-user  158 Dec 23 05:19 ..
drwxr-xr-x. 3 ec2-user ec2-user   23 Dec 23 05:28 .terraform
-rw-r--r--. 1 ec2-user ec2-user 1407 Dec 23 05:28 .terraform.lock.hcl
-rw-r--r--. 1 ec2-user ec2-user  367 Dec 23 05:19 main.tf
```

### チェック
記載したファイルのフォーマットと構文チェックを実施します。
```:コマンド
# フォーマットチェック
terraform fmt
# 構文チェック
terraform validate
```

```:実行結果
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ terraform fmt
main.tf
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ terraform validate
Success! The configuration is valid.
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$
```

# Terraformの実行
## Terraformの実行(1回目)
実際に記載したコードをもとに環境を作ってもらいます。
```:コマンド
# Terraformの実行
terraform apply
```

```:実行結果
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ 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:

(変更内容が記載される)

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
```
最終確認として`Enter a value:`でyse/noを入力する必要があります。

```:実行結果
aws_instance.app_server: Creating...

 Error: creating EC2 Instance: VPCIdNotSpecified: No default VPC for this user. GroupName is only supported for EC2-Classic and default VPC.
       status code: 400, request id: 7f3344e0-4f94-4326-a6b4-e23188d58076

   with aws_instance.app_server,
   on main.tf line 16, in resource "aws_instance" "app_server":
   16: resource "aws_instance" "app_server" {


[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$
```
`main.tf`作成した段階で「これVPC指定してないけどいけるの？」と思っていましたが、案の定エラーになりました。
公式ドキュメントでは、オレゴンリージョン(us-west-2)にデフォルトVPC一式が存在することが前提となっているため、リソースとしてはEC2だけ作成すれば自動でオレゴンのデフォルトVPCに配置される流れでした。

オレゴンのデフォルトVPC以外の既存VPCにデプロイする場合は
`main.tf`内にSGとサブネットのIDを入れれば良いという記載があるのでそれに従います。

https://developer.hashicorp.com/terraform/tutorials/aws-get-started/aws-build#troubleshooting

```変更後main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }

  required_version = ">= 1.2.0"
}

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

resource "aws_instance" "app_server" {
  ami           = "ami-0dfa284c9d7b2adad"
  instance_type = "t2.micro"
  vpc_security_group_ids = ["<SG ID>"]
  subnet_id = "<subnet ID>"

  tags = {
    Name = "ExampleAppServerInstance"
  }
}
```
## Terraformの実行(2回目)
修正完了後、再度構文チェックと実行します。

```:実行結果
# 変更後の構文チェック
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ terraform validate
Success! The configuration is valid.
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$
# 実行
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ terraform apply

(省略)

aws_instance.app_server: Creating...
aws_instance.app_server: Still creating... [10s elapsed]
aws_instance.app_server: Still creating... [20s elapsed]
aws_instance.app_server: Still creating... [30s elapsed]
aws_instance.app_server: Creation complete after 32s [id=i-xxxxxxxxxxxxxxxx]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$

```
Apply complete!と表示され、マネジメントコンソール上からも指定したサブネット上に
EC2インスタンスが起動していることが確認できました。

![TF_EC2_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/e44885d1-726b-82be-491a-6725c5a9cf6d.png)

# Terraformで構築したリソースの削除
Terraformで作成したEC2インスタンスを削除します。

```:コマンド
terraform destroy
```

```:実行結果
[ec2-user@ip-192-168-0-5 learn-terraform-aws-instance]$ terraform destroy
aws_instance.app_server: Refreshing state... [id=i-xxxxxx]

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

Terraform will perform the following actions:

(変更内容が記載される)

Destroy complete! Resources: 1 destroyed.
```

![TF_EC2_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/80c09acf-c286-3517-8f45-0363e8da5c0e.png)

正常に削除されました。

変数の利用などまだ公式チュートリアルの内容はまだありますが、
Terraformでの構築の流れを理解することができました。

# VPC周りを含めた環境の構築
公式チュートリアルの中では既存VPC上にEC2インスタンスをデプロイしましたが、
VPCも含めたEC2インスタンスの新規構築をやってみます。

## 構成
以下のような形でリソース種別にファイル分割します。
```:ディレクトリ構成
TF-WORK
|-- ec2.tf
|-- main.tf
`-- network.tf
```

完成図としては以下の通り

![TerraformVPC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/349a476b-b6be-ad47-809b-4f4fc08d67e5.png)

SGは作成せずVPCデフォルトのものをアタッチさせます。

providerのリファレンスを参考に各種パラメータを記載していきます。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs

## main.tf

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

  required_version = ">= 1.2.0"
}

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


## network.tf

``` network.tf
# ======VPC======
resource "aws_vpc" "tf-vpc-01" {
  cidr_block       = "192.168.0.0/16"
  instance_tenancy = "default"
  enable_dns_hostnames = "true"

  tags = {
    Name = "TF-VPC-01"
    Env = "TF-WORK"
  }
}

# ======Subnet======
resource "aws_subnet""tf-vpc-01-pub-01-a" {
  vpc_id     = aws_vpc.tf-vpc-01.id
  cidr_block = "192.168.0.0/24"
  availability_zone = "ap-northeast-1a"
  map_public_ip_on_launch = "true"

  tags = {
    Name = "TF-VPC-01-Pub-01-a"
    Env = "TF-WORK"
  }
}

resource "aws_subnet" "tf-vpc-01-pri-01-a" {
  vpc_id     = aws_vpc.tf-vpc-01.id
  cidr_block = "192.168.1.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "TF-VPC-01-Pri-01-a"
    Env = "TF-WORK"
  }
}

# ======IGW======
resource "aws_internet_gateway" "tf-vpc-01-igw-01" {
  vpc_id = aws_vpc.tf-vpc-01.id

  tags = {
    Name = "TF-VPC-01-IGW-01"
    Env = "TF-WORK"
  }
}

# ======RouteTable======
resource "aws_route_table" "tf-vpc-01-rtb-pub-01" {
  vpc_id = aws_vpc.tf-vpc-01.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.tf-vpc-01-igw-01.id
  }

  tags = {
    Name = "TF-VPC-01-RTB-Pub-01"
    Env = "TF-WORK"
  }
}

resource "aws_route_table" "tf-vpc-01-rtb-pri-01" {
  vpc_id = aws_vpc.tf-vpc-01.id

  tags = {
    Name = "TF-VPC-01-RTB-Pri-01"
    Env = "TF-WORK"
  }
}

resource "aws_route_table_association" "tf-vpc-01-rtb-at-pub" {
  subnet_id = aws_subnet.tf-vpc-01-pub-01-a.id
  route_table_id = aws_route_table.tf-vpc-01-rtb-pub-01.id
}

resource "aws_route_table_association" "tf-vpc-01-rtb-at-pri" {
  subnet_id = aws_subnet.tf-vpc-01-pri-01-a.id
  route_table_id = aws_route_table.tf-vpc-01-rtb-pri-01.id
}
```

## ec2.tf

``` ec2.tf
resource "aws_instance" "tf-ec2-01" {
  ami           = "ami-0dfa284c9d7b2adad"
  instance_type = "t2.micro"
  subnet_id = aws_subnet.tf-vpc-01-pub-01-a.id
  key_name = "TF-AWS-KEY"

  tags = {
    Name = "TF-EC2-01"
    Env = "TF-WORK"
  }
}
```

## 実行
```:実行結果
# 構文チェック
[ec2-user@ip-192-168-0-5 TF-WORK]$ terraform validate
Success! The configuration is valid.
[ec2-user@ip-192-168-0-5 TF-WORK]$
# 実行プラン確認
[ec2-user@ip-192-168-0-5 TF-WORK]$terraform plan
(省略)
Plan: 9 to add, 0 to change, 0 to destroy.
[ec2-user@ip-192-168-0-5 TF-WORK]$
# 適用
[ec2-user@ip-192-168-0-5 TF-WORK]$terraform apply
(省略)
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
[ec2-user@ip-192-168-0-5 TF-WORK]$
```

![TF_VPC_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/f75e3d92-de5d-017e-83dd-58042d856c5c.png)

![TF_EC2_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/561546/91a85a9e-2ed4-d66a-2fbe-cee67ae1ab29.png)

問題なくデプロイされたことが確認できました。

# まとめ
Terraform知識ゼロ状態でTerraformを使ったAWS環境構築を試してみました。
実行環境の構築も簡単にでき、Terraformファイルの記述自体や指定するパラメータ等も
個人的にはCloudFormationより感覚的に書けると思いました。
リソースの依存関係も自動で分析されて良い感じに作ってくれるのが素晴らしい。
変数のマッピング等の機能もいろいろあるようなのでまた触って理解していきます。