【ハンズオン】初めてでもOK！Terraformでサクッと簡単にEC2インスタンスを作ってみよう

どうも、キタです
昨今はAIがあるので、コーディングもかなり楽になってきましたね。

今回は Terraform を利用して、EC2インスタンスを簡単に作ってみましょう。
1時間程度でサクッと試せるハンズオンです。
※IaCやTerraformに関する説明は省略しています。

## この記事がオススメの方
・AWSでインフラ構築をしたことがある方（VPC,EC2等）
・IaC に興味があり、Terraform を初めて触る方

## この記事の目標
・Terraform を使ってAWS上に簡単なリソースを作成・変更・削除できるようになる

## 作成リソース
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/7d5966b3-7224-469c-8ab6-aa5be2a6be32.png)

よくある最低限の構成です。
★VPC - 10.0.0/16
★パブリックサブネット×2 - (1a：10.0.0/24)(1c：10.0.1/24)
★IGW,RT,SG
★EC2 - インスタンスタイプ：t3.micro

## セットアップ
各ツールのインストール・セットアップ方法は以下の通りです。
※既にセットアップが済んでいる方は飛ばしてOKです
・unzip
・git
・aws-cli
・tfenv

### unzip,git
```bash:
// Almalinuxの場合
$ sudo dnf install -y unzip git

// Ubuntuの場合
$ sudo apt install -y unzip git
```

### aws-cli
aws-cliは以下マニュアル通りにインストールします
```bash:
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install
```
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html

クレディシャル(認証情報の設定)は適切な権限を付与したIAMユーザーを作成し、
Short-term credentialsで行っています。
```bash:
$ aws configure
AWS Access Key ID [None]: ＜IAMユーザのアクセスキー＞
AWS Secret Access Key [None]: ＜IAMユーザのシークレットアクセスキー＞
Default region name [None]: 何も入力せずEnter
Default output format [None]: 何も入力せずEnter
```
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html
### tfenv
tfenv は Terraform のバージョン管理ツールです。
複数環境で異なるバージョンを使用する場合に、簡単にバージョン切り替えが出来るのでとてもオススメです。

Manualの1から順番に行いインストールします。
基本的にはコマンド通りに git clone してPATHを通してあげればOKです

https://github.com/tfutils/tfenv/blob/master/README.md
```bash:
$ git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv
$ echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile
$ source .bash_profile
```

今回はバージョン 1.11.3 をインストールして
インストールリストの確認をします。
```bash:
$ tfenv install 1.11.3
$ tfenv list
* 1.11.3 (set by /home/user/.tfenv/version)
```
インストールを確認したので、useで使用していきます。
```bash:
$ tfenv use 1.11.3
Switching default version to v1.11.3
Default version (when not overridden by .terraform-version or TFENV_TERRAFORM_VERSION) is now: 1.11.3
```
スイッチできたら現在のTerraformのバージョン確認をします。
```bash:
$ terraform version
Terraform v1.11.3
on linux_amd64
```
バージョン確認も問題なさそうですね。
では事前準備を行っていきます。

## 事前準備
テスト用のディレクトリを作成して移動します
```bash:
$ mkdir terraform-test-project
$ cd terraform-test-project
```
githubからコードをcloneする
```bash:
$ git clone https://github.com/shinkitada/education-terrafom.git
$ cd education-terrafom
$ mv terraform.tfvars.example terraform.tfvars
```
以下リポジトリにコードを上げています。

https://github.com/shinkitada/education-terrafom.git

tfvarsの「name」を自分の名前に変更して保存します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/fe19b549-2c64-4a5c-952f-7f3a9aa95317.png)

## リソース作成
Ｔerraform コマンドの実行への一連の流れについては以下の通りです。
今回は作成から削除まで、一連の流れで実行していきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/9a7e32aa-c092-40a9-80bb-7cca1a94ec97.png)


・initでTerraformの初期化、必要なプロバイダー情報やプラグインをインストールします
```bash:
$ terrafom init
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/5f9d8861-20d6-4310-938f-65f049a1fcf8.png)

init を実行すると以下lockファイルとキャッシュディレクトリが自動作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/c9c13434-976d-4477-a37b-ade398c6fc2f.png)

・fmt / validate でフォーマットの整形と構文を検証します
```bash:
$ terraform fmt
$ terraform validate
```
あえてec2.tfがfmtで整えられるようにしているので整形されるはずです。
ではplanをしてみましょう。
```bash:
$ terraform plan
```
エラーがなければapplyで実行します。
```bash:
$  terraform apply
```
アクションの実行確認が出るので「yes」と入力してEnterします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/44be4c29-e48a-4277-9b2a-ca51f02557f2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/b547cc49-91cc-4f35-8431-9a889b3a9946.png)
「Apply complete! 」と出ればOKです

ちなみに、apply を実行すると「terraform.tfstate」が自動作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/b8296333-1dd5-4d71-8efe-e99001198c5a.png)
tfstate には、実行で作成されたリソース内容が全て書き込まれています。
つまり、以降 Terraform は tfstate ファイルを参照して、リソースの差分確認（追加リソースの作成や削除）を行います。

複数人で特定の環境でTerraformを実行する場合は __tfstate ファイルを S3 で管理する__ のが望ましいですが、今回は割愛します。

では、作成したインスタンスのパブリックIPを確認してみましょう
```bash:
$ terraform state show aws_instance.my_instance | grep public_ip
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/cfb4dac5-6a41-4c1f-b3b4-5999febae4ad.png)
これをコピーしてSSHコマンドで接続してみます。
```bash:
$ ssh -i "./key/<自分の名前>-key.id_rsa" ec2-user@<コピーしたIPアドレス>
例：$ ssh -i "./key/kitada-key.id_rsa" ec2-user@203.0.113.0
```
接続できればOKです。
ではexitで作成したEC2から抜けて、削除までしてしまいましょう。
```bash:
$ exit
```
削除前のドライランを実行します
```bash:
$  terraform plan -destroy
```
エラーが出ないことを確認して削除します。
```bash:
$  terraform destroy
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/0f846929-8e3d-435a-8a1f-f228e76ed17a.png)
アクションの実行確認が出てくるので「yes」と入力してEnter
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935946/2a4ff68a-03ce-46a0-ba58-9118ccd74723.png)
destroyedになれば成功です！
お疲れ様でした。

## コード解説
では最後にコード解説をして終わろうと思います。
それぞれのファイル構成は以下の通りです。

```bash:
.
├── ec2.tf          （EC2インスタンスと関連リソースの定義）
├── provider.tf     （プロバイダーの設定）
├── terraform.tfvars（変数の値）
├── variables.tf    （変数の定義）
└── vpc.tf          （VPCとネットワーク関連リソースの定義）
```

### ・ec2.tf （EC2インスタンスと関連リソースの定義）
```hcl:
#httpプロバイダーを使用してmyipのURLを取得する
data "http" "my_ip" {
  url = "https://api.ipify.org?format=json"
}

#ローカル変数
locals {
  public_key_file  = "./.key/${var.name}-key.id_rsa.pub"
  private_key_file = "./.key/${var.name}-key.id_rsa"
  my_ip            = "${jsondecode(data.http.my_ip.response_body)["ip"]}/32"
}

#秘密鍵の作成
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_pem" {
  filename = local.public_key_file
  content  = tls_private_key.keygen.public_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ${local.public_key_file}"
  }
}

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

# SSH用のセキュリティグループを作成
resource "aws_security_group" "allow_ssh" {
  name        = "${var.name}-allow-ssh"
  description = "Allow SSH inbound traffic"
  vpc_id      = aws_vpc.my-vpc.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [local.my_ip]
  }
}

variable "ami_id" {
  description = "The AMI ID for the instance"
  type        = string
  default     = "ami-03ec4a957caaadb88"
}

#インスタンス作成
resource "aws_instance" "my_instance" {
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  key_name               = aws_key_pair.key_pair.key_name
  subnet_id            = aws_subnet.my_subnet_1a.id
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  tags = {
    Name = "terraform-edu-${var.name}-instance"
  }
}

```
このファイルでは、AWS上にLinuxの仮想サーバー（EC2インスタンス）を立てるための設定です。
SSHで安全に接続できるように接続元の現在のIPアドレスからのアクセスのみを許可する設定を入れています。


```hcl:
#httpプロバイダーを使用してmyipのURLを取得する
data "http" "my_ip" {
  url = "https://api.ipify.org?format=json"
}

#ローカル変数
locals {
  public_key_file  = "./.key/${var.name}-key.id_rsa.pub"
  private_key_file = "./.key/${var.name}-key.id_rsa"
  my_ip            = "${jsondecode(data.http.my_ip.response_body)["ip"]}/32"
}
```
このコードでは[ローカル変数(locals)](https://developer.hashicorp.com/terraform/language/values/locals)というものを利用しています。
これは、外部のHTTPのURLから情報を取得するための設定です。「my_ip」という名前でこの設定を後から参照します。

` url = "https://api.ipify.org?format=json":`  ここで指定されたURLにアクセスすると、接続元の現在のインターネット接続のIPアドレスがJSON形式で返ってきます。
これを利用してSGで許可設定をする際に、マイIPで許可設定を入れるというようなことを実現しています。

それぞれローカル(ファイル内のみ)変数を利用して
`public_key_file`と`private_key_file`ではEC2への接続に必要な公開鍵と秘密鍵のファイルの保存場所を定義しています。

`my_ip`ではJSONデータの中から「ip」というキーの値（あなたのIPアドレス）を取り出しています。
`/32`では特定のIPアドレスのみを許可するCIDR表記にしています。

```hcl:
resource "tls_private_key" "keygen" {
  algorithm = "RSA"
  rsa_bits  = 4096
}
```
これは、サーバーへの安全な接続に必要な秘密鍵と公開鍵のペアをTerraformに作成させる設定です。
「keygen」という名前でこの鍵ペアを参照できます。

認証方式は RSA にして 4096bit で作成しています。


```hcl
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_pem" {
  filename = local.public_key_file
  content  = tls_private_key.keygen.public_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ${local.public_key_file}"
  }
}
```
これはローカル(実行元のコントロールサーバー)内にファイルを作成するための設定です。
その権限を600にするコマンドを実行しています。

```hcl
resource "aws_key_pair" "key_pair" {
  key_name   = "${var.name}-key"
  public_key = tls_private_key.keygen.public_key_openssh
}
```
これは、AWSの「キーペア」という仕組みに公開鍵を登録するための設定です。
EC2インスタンスにSSH接続する際に使われます。
`public_key = tls_private_key.keygen.public_key_openssh`で先ほど作成した公開鍵の内容をAWSに登録します。

```hcl
resource "aws_security_group" "allow_ssh" {
  name        = "${var.name}-allow-ssh"
  description = "Allow SSH inbound traffic"
  vpc_id      = aws_vpc.my-vpc.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [local.my_ip]
  }
}
```
ここではAWSのSGの設定をしています。
22番ポートを指定して許可元のIPは先ほどローカル変数で参照したmy_ipからのみを許可する設定にしています。

```hcl
variable "ami_id" {
  description = "The AMI ID for the instance"
  type        = string
  default     = "ami-03ec4a957caaadb88"
}


resource "aws_instance" "my_instance" {
  ami                     = var.ami_id
  instance_type           = "t3.micro"
  key_name                = aws_key_pair.key_pair.key_name
  subnet_id               = aws_subnet.my_subnet_1a.id
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  tags = {
    Name = "terraform-edu-${var.name}-instance"
  }
}
```
これが、実際にAWS上に仮想サーバー（EC2インスタンス）を作成する設定です。
amiはそれぞれIDが割り当てられており、今回はAlmalinux9のAMI IDを指定しています。
インスタンスタイプはt3.microで秘密鍵やSGを先ほど設定したものにしています。

### ・provider.tf （プロバイダーの設定）
```hcl:
#AWSプロバイダーの設定
provider "aws" {
  region = var.region
  default_tags {
    tags = {
      environment = var.environment
    }
  }
}

#httpプロバイダーの設定
provider "http" {}
```
ここではプロバイダーの設定を行っています。
今回はAWSで、リージョンは変数からap-northeast-1(東京リージョン)を指定しています。
また、[default_tags](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/default_tags
)を使用して、リソースを作成する際に自動的にタグを付与しています。

httpプロバイダーの設定では、TerraformがHTTPエンドポイントとやり取りすることを宣言しておりTerraformの構成内でHTTPリクエストを送信ンしたりレスポンスを処理するために入れています。
設定ブロック内ではリクエストヘッダーやタイムアウトなどの設定のオプションを入れることが出来ますが、空にしており、デフォルトで動作するようにしています。




### ・terraform.tfvars （変数の値）
```hcl:
#変数一覧
region      = "ap-northeast-1"
name        = "hogehoge"
environment = "prd"
```
ここではそれぞれに参照している変数の値を参照しています。


### ・variables.tf （変数の定義）
```hcl:
#リージョン
variable "region" {
  type        = string
  description = "メインリージョン"
  default     = "ap-northeast-1"
}

#名前
variable "name" {
  type        = string
  description = "自分の名前"
  default     = "hoge"
}

#環境名
variable "environment" {
  type        = string
  description = "環境名"
  default     = "dev"
}
```


### ・vpc.tf （VPCとネットワーク関連リソースの定義）
```hcl:
resource "aws_vpc" "my-vpc" {
  cidr_block = "10.0.0.0/16"

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

resource "aws_subnet" "my_subnet_1a" {
  vpc_id                  = aws_vpc.my-vpc.id
  cidr_block              = "10.0.0.0/24"
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true

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


resource "aws_subnet" "my_subnet_1c" {
  vpc_id                  = aws_vpc.my-vpc.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.region}c"
  map_public_ip_on_launch = true

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

resource "aws_internet_gateway" "my_igw" {
  vpc_id = aws_vpc.my-vpc.id

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

resource "aws_route_table" "my_public_rt" {
  vpc_id = aws_vpc.my-vpc.id

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

resource "aws_route" "my_route" {
  route_table_id         = aws_route_table.my_public_rt.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.my_igw.id
}

resource "aws_route_table_association" "my_route_public_1a" {
  subnet_id      = aws_subnet.my_subnet_1a.id
  route_table_id = aws_route_table.my_public_rt.id
}

resource "aws_route_table_association" "my_route_public_1c" {
  subnet_id      = aws_subnet.my_subnet_1c.id
  route_table_id = aws_route_table.my_public_rt.id
}
```
ここではVPCとサブネット、igw(インターネットゲートウェイ),rt(ルートテーブル)の設定をしています。
```hcl
resource "aws_vpc" "my-vpc" {
  cidr_block = "10.0.0.0/16"

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

resource "aws_subnet" "my_subnet_1a" {
  vpc_id                  = aws_vpc.my-vpc.id
  cidr_block              = "10.0.0.0/24"
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true

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


resource "aws_subnet" "my_subnet_1c" {
  vpc_id                  = aws_vpc.my-vpc.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.region}c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.name}-public-1c"
  }
}
```
VPCは 10.0.0/16 にしていて
サブネットは1a（10.0.0.0/24） と1c（10.0.0.1/24）で作成しています。

```hcl
resource "aws_internet_gateway" "my_igw" {
  vpc_id = aws_vpc.my-vpc.id

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

resource "aws_route_table" "my_public_rt" {
  vpc_id = aws_vpc.my-vpc.id

  tags = {
    Name = "${var.name}-public-rt"
  }
}
```
こちらはigwとrtを作成しています。
```hcl
resource "aws_route" "my_route" {
  route_table_id         = aws_route_table.my_public_rt.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.my_igw.id
}

resource "aws_route_table_association" "my_route_public_1a" {
  subnet_id      = aws_subnet.my_subnet_1a.id
  route_table_id = aws_route_table.my_public_rt.id
}

resource "aws_route_table_association" "my_route_public_1c" {
  subnet_id      = aws_subnet.my_subnet_1c.id
  route_table_id = aws_route_table.my_public_rt.id
}
```
このコードは、AWSのVPC（仮想プライベートクラウド）におけるインターネットへの経路設定を行っています。
パブリックルートテーブル (my_public_rt) に対して、「すべての送信先 (0.0.0.0/0) へのトラフィックをインターネットゲートウェイ (igw) に送る」というルールを追加しています。

またサブネットの my_subnet_1a と my_subnet_1c をパブリックルートテーブル (rt) に関連付け、このサブネット内のリソースがインターネットにアクセスできるようにしています。

ということでお疲れ様でした！
前に解説した内容とほぼ同様でしたがもしご興味があれば参考にしていただければ幸いです！
ではでは～