【コピペでOK！】IaC初心者のためのTerraformでEC2インスタンス立ち上げ入門（AWS）

# はじめに
こんにちは
普段「[ねずみさん家。のインフラエンジニア道場」](https://www.youtube.com/@NezumiInfraDojo)というチャンネルで
~~しばき回されている~~鍛えられている、キタと申します。

今回は先日[Youtubeライブ](https://youtu.be/QvGnHeNn32I)で配信した内容を元に、Terraform入門を記事にしました。

今まで IaC に対してすごく便利なものなのは理解していたのですが、リソースを不本意に大量作成・削除してしまわないか等の恐怖心を抱いてしまっており、触ることを避けてしまっていました。

今回は同じ事を思っている方に少しでも理解してもらえるように、簡単にまとめたので是非参考になれば幸いです。

## これは何
Terraform初心者のための構築記事です。
ここではYoutubeライブで使用したコードの紹介と解説を行っていきます。

### 環境
* OS：Windows 11
* WSL2：AlmaLinux 9
* AWSアカウント：作成済みを想定しています

### この記事の対象の方
* IaCが怖い方
* Terraformを初めて利用する方
* AWSの環境をコードで管理したい方
* インフラの自動化に興味がある方

### インストールが必要なツール
* unzip 
* aws-cli
* git
* tfenv (Terraform)

各ツールのインストール・セットアップ方法は以下の通りです。

#### unzip
```
// Almalinuxの場合
$ sudo dnf install unzip

// Ubuntuの場合
$ sudo apt install unzip
```
gitからダウンロードしたファイルを展開するのに`unzip`が必要なのでインストールします

#### aws-cli
インストール

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html
 
セットアップ(認証情報の設定)
※Short-term credentialsで行っています。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-quickstart.html


aws-cliはAWSのサービスをコマンドラインから操作するためのツールで、
これを通じてterraformを実行するためにインストール・セットアップします。


#### git
```
// Almalinuxの場合
$ sudo dnf install git

// Ubuntuの場合
$ sudo apt install git
```
gitは、`tfenv`をインストールするために使用します。

#### tfenv
インストール
Manualの1から順番に行っていけば、インストールが出来ます。

https://github.com/tfutils/tfenv/blob/master/README.md

tfenvはTerraformのバージョン管理ツールで、Terraformのバージョンをインストールして切り替えることが出来ます。
```
// Terraform version 1.6.1をインストール
$ tfenv install 1.6.1

// 利用するバージョンの指定
$ tfenv use 1.6.1
```
1.6.1をインストールして、バージョン指定して利用します。
※`-bash: tfenv: command not found`と出てきた場合は以下コマンドを行ってみてください。
```
$ source ~/.bashrc
```

### 環境構築前確認
```
// AWS CLIのバージョン確認
$ aws --version                                            
aws-cli/2.22.4 Python/3.12.6 Linux/5.15.167.4-microsoft-standard-WSL2 exe/x86_64.almalinux.9

// terraformのバージョン確認
$ terraform -v                                                          
Terraform v1.6.1
on linux_amd64

// gitのバージョン確認
$ git -v
git version 2.43.5

// tfenvのバージョン確認
$ tfenv -v
tfenv 3.0.0-49-g39d8c27
```
ツールのインストールとセットアップが完了したら、バージョン確認を行います。


## そもそも Terraform is 何
Terraformを知らない方も居られるかもなので簡単に説明しますね。
既にご存じの方は読み飛ばしていただいてOKです。

Terraformは、HashiCorp社が手掛ける、マルチプロバイダーに対応したIaCツールの一種で、インフラの構成をソースコードとして管理することが出来ます。

サーバ内部の設定なども管理したい場合は、Ansibleなどを併用していくと良いみたいです。

※IaC=Infrastructure as Code
「インフラをコードで管理して自動化しちゃおうぜ！」のこと

#### HCLについて簡単に解説
Terraformのコードは、HCL(HashiCorp Configuration Language)という言語を利用して記述していきます。構造は非常にシンプルで、初心者でも分かりやすい言語となっています。

また、インフラ構成の定義は、`.tf`という拡張子のファイルに、各インフラのリソースの定義をコードで記述することによって行います。

```HCL:例
resource "リソースの種別" "リソース名" {
    項目1 = 項目のオプション
    項目2 = 項目のオプション
    項目3 = 項目のオプション
}
```
リソースの種別は`aws_instance`や`aws_vpc`など
リソース名は任意の文字列を記述することが可能です。

使用できるリソースの種類↓

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

## 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/040e22da-8588-e30a-17a3-c7ae34617f44.png)


## コードの実装
コードは全てGitHubにあげていますので、こちらを好きにご利用ください。

https://github.com/nezumisannn/yl_terraform

ユーザのホームディレクトリ配下で以下コマンドを実行すればクローンで持ってこれます。
```
$ cd /home/user/
$ git clone https://github.com/nezumisannn/yl_terraform.git
```

ディレクトリ構成は以下の通りです
```
└── terraform-youtube-live
    ├── .gitignore (ファイル除外用)
    ├── .terraform.lock.hcl（init時に作成されるファイル)
    ├── ec2.tf
    ├── provider.tf
    ├── terraform.tfvars
    ├── variables.tf
    └── vpc.tf
```

### provider.tf
```HCL:provider.tf
provider "aws" {
  region = var.region
}
```
プロバイダーの指定とリージョンの変数の設定を行っています。
プロバイダーはここで指定しているaws以外にもAzureやGoogleCloud(旧GCP)などを選択することが出来ます。


### terraform.tfvars
```HCL:terraform.tfvars
region  = "ap-northeast-1"
project = "yt-live-terraform"
```
terraform.tfvarsは変数の代入を行っています。
今回であれば、`region`には東京リージョン(ap-northeast-1)を定義しており、`project`にはタグ等に使いまわせるよう、`yt-live-terraform`を入れています。


### variables.tf
```HCL:variables.tf
variable "region" {
  type = string
}

variable "project" {
  type = string
}
```
上のterraform.tfvarsでは変数の代入を行いましたが、ここでは変数の定義を行っています。
type(型)を`string`で指定しており、ここではシンプルな文字列型にしています。

**primitive type**
|  型名  |   概要  |
|--------|--------|
| string | 文字列型|
| number | 数字型  |
|  bool  | 論理型  |

primitive type以外にも型が存在していますので気になる方は以下を参照下さい。

https://developer.hashicorp.com/terraform/language/expressions/type-constraints

ここまで、一旦まとめると以下の通りです。
```
    ├── provider.tf      (プロバイダーの指定）
    ├── terraform.tfvars（変数に対する値の代入）
    ├── variables.tf     (変数の定義)
```
ではリソースの部分の説明に移っていきます。

### vpc.tf
```HCL:vpc.tf
resource "aws_vpc" "yt_live" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "${var.project}-vpc"
  }
}

resource "aws_subnet" "yk_live_public_1a" {
  vpc_id                  = aws_vpc.yt_live.id
  cidr_block              = "10.0.0.0/24"
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project}-public-1a"
  }
}

resource "aws_subnet" "yk_live_public_1c" {
  vpc_id                  = aws_vpc.yt_live.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.region}c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project}-public-1c"
  }
}

resource "aws_internet_gateway" "yt_live" {
  vpc_id = aws_vpc.yt_live.id

  tags = {
    Name = "${var.project}-igw"
  }
}

resource "aws_route_table" "yt_live_public" {
  vpc_id = aws_vpc.yt_live.id

  tags = {
    Name = "${var.project}-public-rt"
  }
}

resource "aws_route" "yt_live_public" {
  route_table_id         = aws_route_table.yt_live_public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.yt_live.id
}

resource "aws_route_table_association" "yt_live_public_1a" {
  subnet_id      = aws_subnet.yk_live_public_1a.id
  route_table_id = aws_route_table.yt_live_public.id
}

resource "aws_route_table_association" "yt_live_public_1c" {
  subnet_id      = aws_subnet.yk_live_public_1c.id
  route_table_id = aws_route_table.yt_live_public.id
}
```
ここではVPC等のネットワークに関するリソースの実装をしています。
上から順に説明していきます。
```HCL
resource "aws_vpc" "yt_live" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "${var.project}-vpc"
  }
}
```
VPCを`10.0.0.0/16`で作っています。
```HCL
resource "aws_subnet" "yk_live_public_1a" {
  vpc_id                  = aws_vpc.yt_live.id
  cidr_block              = "10.0.0.0/24"
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project}-public-1a"
  }
}

resource "aws_subnet" "yk_live_public_1c" {
  vpc_id                  = aws_vpc.yt_live.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.region}c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project}-public-1c"
  }
}
```
パブリックサブネット1aを`10.0.0.0/24`で1cを`10.0.1.0/24`で分けて作っています。
```HCL
resource "aws_internet_gateway" "yt_live" {
  vpc_id = aws_vpc.yt_live.id

  tags = {
    Name = "${var.project}-igw"
  }
}
```
インターネットゲートウェイ作成しています。
```HCL
resource "aws_route_table" "yt_live_public" {
  vpc_id = aws_vpc.yt_live.id

  tags = {
    Name = "${var.project}-public-rt"
  }
}

resource "aws_route" "yt_live_public" {
  route_table_id         = aws_route_table.yt_live_public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.yt_live.id
}

resource "aws_route_table_association" "yt_live_public_1a" {
  subnet_id      = aws_subnet.yk_live_public_1a.id
  route_table_id = aws_route_table.yt_live_public.id
}

resource "aws_route_table_association" "yt_live_public_1c" {
  subnet_id      = aws_subnet.yk_live_public_1c.id
  route_table_id = aws_route_table.yt_live_public.id
}
```
パブリックルートテーブルの作成を行った後に、ルートの宛先を0.0.0.0/0にしており、
パブリックサブネットの1aと1cの関連付けを行っています。

注意点としては、今回はパブリックサブネットのみ作成していますが
プライベートサブネットも作成しておくと、後からリソースを追加するときも楽だと思います。


### ec2.tf
```HCL:ec2.tf
locals {
  public_key_file  = "./.key/${var.project}-key.id_rsa.pub"
  private_key_file = "./.key/${var.project}-key.id_rsa"
}

resource "tls_private_key" "keygen" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "local_file" "private_key_pem" {
  filename = local.private_key_file
  content  = tls_private_key.keygen.private_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ${local.private_key_file}"
  }
}

resource "local_file" "public_key_openssh" {
  filename = local.public_key_file
  content  = tls_private_key.keygen.public_key_openssh
  provisioner "local-exec" {
    command = "chmod 600 ${local.public_key_file}"
  }
}

resource "aws_key_pair" "key_pair" {
  key_name   = "${var.project}-key"
  public_key = tls_private_key.keygen.public_key_openssh
}

resource "aws_security_group" "allow_ssh" {
  name        = "${var.project}-allow-ssh"
  description = "Allow SSH inbound traffic"
  vpc_id      = aws_vpc.yt_live.id
}

resource "aws_security_group_rule" "allow_ssh" {
  security_group_id = aws_security_group.allow_ssh.id
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}

resource "aws_instance" "instance" {
  ami                    = "ami-03f584e50b2d32776"
  instance_type          = "t3.micro"
  key_name               = aws_key_pair.key_pair.key_name
  subnet_id              = aws_subnet.yt_live_public_1a.id
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  tags = {
    Name = "${var.project}-instance"
  }
}
```
続けてこちらではEC2インスタンス周りの実装を行っています。
こちらも一番上から説明すると、
```HCL
locals {
  public_key_file  = "./.key/${var.project}-key.id_rsa.pub"
  private_key_file = "./.key/${var.project}-key.id_rsa"
}
```
ここで公開鍵と秘密鍵のファイルパスとファイル名を定義しています。


```HCL
resource "tls_private_key" "keygen" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "local_file" "private_key_pem" {
  filename = local.private_key_file
  content  = tls_private_key.keygen.private_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ${local.private_key_file}"
  }
}

resource "local_file" "public_key_openssh" {
  filename = local.public_key_file
  content  = tls_private_key.keygen.public_key_openssh
  provisioner "local-exec" {
    command = "chmod 600 ${local.public_key_file}"
  }
}

resource "aws_key_pair" "key_pair" {
  key_name   = "${var.project}-key"
  public_key = tls_private_key.keygen.public_key_openssh
}

```
その後RSAを使用して4096ビットの秘密鍵を生成し、ローカルファイル(秘密鍵・公開鍵)の作成を行っています。
作成と同時に接続時にエラーが出てこないようにパーミッションを600にしています。

その後、AWS側へローカルと同様のキーペアを作成し、パブリックキーはローカルファイルの公開鍵をインポートしています。
```HCL
resource "aws_security_group" "allow_ssh" {
  name        = "${var.project}-allow-ssh"
  description = "Allow SSH inbound traffic"
  vpc_id      = aws_vpc.yt_live.id
}

resource "aws_security_group_rule" "allow_ssh" {
  security_group_id = aws_security_group.allow_ssh.id
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}
```
インスタンスに紐づけるためのセキュリティグループを作成後、
インバウンドルールで22番ポートを`0.0.0.0/0`(全開放)にしています。

注意点として、ここでは演習用のためセキュリティグループにてSSH(22番ポート)を`0.0.0.0/0`で全開放をしてしまっていますが、通常であれば接続元のグローバルIPのみ許可することをセキュリティの観点から強く推奨します。

```HCL
resource "aws_instance" "instance" {
  ami                    = "ami-03f584e50b2d32776"
  instance_type          = "t3.micro"
  key_name               = aws_key_pair.key_pair.key_name
  subnet_id              = aws_subnet.yt_live_public_1a.id
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  tags = {
    Name = "${var.project}-instance"
  }
}
```
最後にインスタンスを作成していきます。
AMIは`Amazon LInux 2023`の最新版を利用し、インスタンスタイプはt3.micro
キーペアは先ほど作成したものを指定し、サブネット、セキュリティグループをそれぞれ指定してインスタンスを作成しています。

## 実行
```
// terraform初期化(プラグインのインストール)
$ terraform init

// terraformのコードフォーマット(整形)する
$ terraform fmt

// 実施内容の確認
$ terraform plan

// 実環境への反映
$ terraform apply
```
ここまで実装が完了したら、↑の通りにコマンドを実行し、実環境への反映を行います。
問題がなければapply実行時「Enter a value」が出るので「Yes」を入力してEnterを押し、以下表示が出ればリソース作成完了です。
```
Apply complete! Resources: 15 added, 0 changed, 0 destroyed.
```
あとはAWSコンソール上からリソースが作成されているか確認しましょう。
しっかり出来ていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/872f7565-1273-f730-e35c-0ad9c2c3ad4d.png)

インスタンスも起動から実行中になっていますので、あとはパブリックIPアドレスを確認してSSHで繋ぎに行けばOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/9309a557-4fce-9e1f-50bc-820ee32f5bc6.png)


### (おまけ) リソースの削除
テスト用で作ったリソースなので、不要な課金を避けるために削除をしていきます。
削除のコマンドは`terraform destroy`ですが、事前に`plan`を実行して削除されるリソースを確認しておきましょう。
```
$ terraform plan -destroy
```
内容に問題がなければ`plan`を外して実行しましょう。

```
$ terraform destroy
```
以下のような出力があれば削除が完了です。
では最後にコンソールから確認してみましょう。
```
Destroy complete! Resources: 15 destroyed.
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/b56ff41a-1f3f-f9bb-5d31-05a8d6493652.png)
VPCはもう消えているので確認できませんが、インスタンスを確認するとちゃんと終了済みになっていますね。

お疲れ様でした。
以上でIaC初心者のためのTerraform入門完了です。
個人的には普段触ってるものが、そのままのリソースの名前で指定できたので思った以上に分かりやすくてビックリしました。

この記事が少しでも皆様の参考になれば幸いです。
ではでは～