AWS：terraformでEC2インスタンスを立てる

# 概要
EC2のインスタンスをterraformで立てます。
立てて、落として、また立ててもすぐに利用できるようにします。

## terraform
どんなものかについては下記を参照。
https://qiita.com/yamanashi7474/items/d12961780ad296335ed2

### 事前確認
terraformをローカル環境にインストールします。

https://docs.uipath.com/installation-and-upgrade/lang-ja/docs/installing-terraform
上記手順をそのままなぞるとバージョン0.12.3を落とせますが、現在0.14.Xまでバージョンが上がってます。
下記を参考にしてください。

```
$ wget https://releases.hashicorp.com/terraform/0.14.10/terraform_0.14.10_linux_amd64.zip
$ sudo apt-get install unzip
$ unzip terraform_0.14.10_linux_amd64.zip
$ sudo mv terraform /usr/local/bin/
```

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself$ terraform -version
Terraform v0.14.10

Your version of Terraform is out of date! The latest version
is 1.0.7. You can update by downloading from https://www.terraform.io/downloads.html
```
versionの確認が出来れば導入okです。

## 作業ディレクトリの作成
terraformの運用にディレクトリ構成はかなり重要なポイントになります。
適当な運用をすると意図せぬ変更を意図していない環境に当ててしまいかねないので分けておきましょう。
今回は下記構成のmake_ec2_01ディレクトリを作業ディレクトリとします。
このページでは以後は何か記載しない限り常に作業ディレクトリで作業を行っている前提とします。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself$ tree ./
./
├── study_ec2.pem
└── terraform
    └── aws
        └── make_ec2_01

3 directories, 1 file
```

## .tfファイルの作成
.tfファイルは命名規則などの縛りは有りません。
膨大なリソースを一つのファイルに記載しても、細かく切り分けて細分化させても問題ありません。
リソース管理の観点からリソースごとに.tfファイルを分けると使いやすいと思います。

.tfファイルの書き方は非常に簡単です。terraformに何を作るのかを伝える為のデータシートですので、鍵括弧の閉じ方やデータの渡し方にルールはありますが基本的にはAWSの各種リソース(EC2インスタンス/vpc/セキュリティグループ等)単位で書きます。
`resource`で始まるブロックが一つの塊です。

```
resource　"AWSリソース名" "任意の名前" {
  設定1                 = "設定値"
  設定2                 = 設定値
  設定3 = [
    "aaa","bbb"
  ]
  ブロック {
    設定4 = 設定値
  }
  tags = {
    Name = "test-tag"
  }
}
```
`AWSリソース名`というのは、AWSで作成できる様々なものを指す固有のワードです。後述してますがvpcなら`aws_vpc`、ec2インスタンスなら`aws_instance`と決められた固有のコードが入ります。
作りたいもののリソース名は公式サイトで探せば確認できます。google検索をかけるだけでも普通に出てきます。

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

`設定X`は設定項目の事を差します。設定値は然りです。
注意としては設定値の渡し方についてはいくつか種類があります。これも公式サイトで調べると設定項目ごとに渡せる設定値と渡すときのルールが記載されています。

設定値の中には作成したリソースのidなどもあります。terraformでは下記のような書き方をすることでterraformで作成したリソースの情報を渡すことができます。

`AWSリソース名.任意の名前.設定名`
`例：aws_vpc.test-vpc.id`

ブロックは複数の設定をくくる箱のようなものです。ブロックに対して直接設定値を設定することはありませんが、ブロックの中に書く必要のある設定があります。

tagについてはAWSの多くのリソースにつけることができるtagです。上記の例だと`Name`というtagを作ってそれに`test-tag`という文字列を入れています。よくある使い方なので記載しました。

この`resource`を必要な分だけ記載したものが.tfファイルになります。
記載する`resource`の順番や、`resource`内の設定の順番などは気にする必要はありません。とにかく.tfファイルに書いてさえあれば`terraform`コマンドを実行した際に読み取られます。

### 基本的なterraformコマンド (fmt / plan / apply / destroy)
具体的な.tfファイルの作成に入る前に、よく使うerraformコマンド(terraform fmt / terraform plan / terraform apply / terraform destroy)について記載します。

#### terraform fmt
`terraform fmt`は.tfファイルのインデントを整えてくれます。

before

```
resource "aws_vpc" "test-vpc" {
cidr_block = "10.40.0.0/16"
 instance_tenancy = "default"
enable_dns_support = true
enable_dns_hostnames = true
 tags = {
Name = "test-main"
  }
}
```

after

```
resource "aws_vpc" "test-vpc" {
  cidr_block           = "10.40.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "test-main"
  }
}
```

整形されたファイルはプロンプトにファイル名のみ出力されます。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ terraform fmt
vpc.tf
```

#### terraform plan
`terraform plan`はterraformが作業ディレクトリの.tfファイルを読み取り、作成するリソースの一覧を画面に表示します

長いので例をここに記載しませんが、.tfファイルを基に作成されるリソースを出力するだけで何か変更が行われるという事はありません。作成前に確認する目的で実行します。

#### terraform apply
`terraform plan`で作成予定のリソースに問題が無ければ、`terraform apply`でリソースを作成します。
実行するとplanで見たのと同じ作成されるリソースの設定一覧と、最後に下記のように作成するかしないか入力を求められます。
yesを入れれば作成が開始され、それ以外では作成されず終了します。

```
~snip~
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を入力した場合、暫くの時間経過の後に下記の様に作成(added)、変更(changed)、削除(destroyed)をそれぞれいくつ実行したのかという結果が出力されます。

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

.tfファイルのリソースの記載が間違っていた場合、エラーが出た箇所とある程度の原因が出力されますので確認の上.tfファイルを修正してください。

-targetオプションを用いて.tfファイルに記載したリソースの一部のみを作成することも可能です。

https://qiita.com/yamanashi7474/items/d12961780ad296335ed2#plan%E3%82%84apply%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E5%AF%BE%E8%B1%A1%E3%81%A8%E3%81%AA%E3%82%8B%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B

#### terraform destroy
`terraform destroy`は読んで字のごとくリソースを削除します。
注意しなくてはならない点として、`terraform destroy`をそのまま実行した際に削除対象となるのは`terraform apply`で作成した全リソースが対象となります。

全てを削除する意図は無く、特定のリソースのみを削除する際には-targetオプションを用いて対象を限定してください。

https://qiita.com/yamanashi7474/items/d12961780ad296335ed2#plan%E3%82%84apply%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E5%AF%BE%E8%B1%A1%E3%81%A8%E3%81%AA%E3%82%8B%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B

また、destroyは-targetで対象を指定したとしてもその対象に紐づけられて作成されたリソースはまとめて削除されることがあります。実行前に何が消えるのかよく確認した上でyesを入力してください。



## .tfファイルを作成する
それではterraformを使い始める準備が整いましたので、早速AWSのリソースを作成していきます。

### provider.tfの作成
provider.tfはterraformの対象になるクラウド環境の情報が必要です。今回はAWSの情報と、使用するアカウント情報を書き込みます。

```
[yamanashi@ip-172-31-46-224 test01]$ cat provider.tf
provider "aws" {
  version    = "~> 2.0"
  region     = "ap-northeast-1"
  access_key = "AKIXXXXXXXXXXXXXXXX"
  secret_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```
access_keyとsecret_keyは各々で取得する必要があります。
awscliの設定を入れた際に調べたアクセスキーとシークレットアクセスキーに置き換えてください。

https://qiita.com/yamanashi7474/items/f3e4cd136695c8627ef8

### terraformの初期設定 (terraform init)
terraformの初期設定は`terraform init`を実行すれば完了します。
`terraform init`は初回に実行するコマンドでして、これを実行するとprovider.tfを読み取って必要なコンポーネントを自動で取得してくれます。今回はAWSを触ることを宣言してますのでawsを操作するのに必要なコンポーネントを自動で取得してくれます。

一度実行したらそのフォルダの.tfファイルを更新しても再実行する必要はありません。ただしprovider.tfを更新して使用するコンポーネントを追加したりした場合には実行する必要があります。複数回実行しても問題は起きません。

この`terraform init`を作成したprovider.tfが存在するディレクトリで実行してください。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 2.0"...
- Installing hashicorp/aws v2.70.0...
- Installed hashicorp/aws v2.70.0 (self-signed, key ID 34365D9472D7468F)

~snip~

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

`Terraform has been successfully initialized!`が表示されればokです。

これでprovider.tf以外の.tfファイルの作成を開始できます。


### vpc.tf
vpcはAWSの仮想ネットワークを指します。ec2インスタンスを立てるために作成します。
また、AWSではvpcのネットワーク直下にec2インスタンスを置くことはできません。サブネットも必要になりますのでvpc.tfに二つとも記載します。

vpc.tfを作成し、下記のコードを入力してください。

```
resource "aws_vpc" "test-vpc" {
  cidr_block           = "10.40.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "test-main"
  }
}

resource "aws_subnet" "test-subnet-a" {
  vpc_id                  = aws_vpc.test-vpc.id
  cidr_block              = "10.40.0.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true
  tags = {
    Name = "test-subnet-a"
  }
}
```

### igw.tf
vpcをインターネットに接続するためにインターネットゲートウェイが必要です。

igw.tfを作成し、下記のコードを入力してください。

```
resource "aws_internet_gateway" "test-igw" {
  vpc_id = aws_vpc.test-vpc.id

  tags = {
    Name = "main-igw"
  }
}
```
vpc_idには先ほど作成したvpcのidを入れる必要があります。
前述しましたが、上記の書き方をすると作成した`test-vpc`のidを渡すことができます。

### rt.tf
外向けの通信が発生した際に、それをインターネットゲートウェイに向けるためのルートテーブルが必要です。

rt.tfを作成し、下記のコードを入力してください。

```
resource "aws_route_table" "test-routetable" {
  vpc_id = aws_vpc.test-vpc.id
  tags = {
    Name = "test-routetable"
  }
}

resource "aws_route" "route-to-igw" {
  route_table_id         = aws_route_table.test-routetable.id
  gateway_id             = aws_internet_gateway.test-igw.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route_table_association" "rt_association" {
  subnet_id      = aws_subnet.test-subnet-a.id
  route_table_id = aws_route_table.test-routetable.id
}
```

これ、terraformのめんどくさいポイントなんですが`aws_route_table`というルートテーブルを作成した後に`aws_route`でルートを作成してます。さらに`aws_route_table_association`はルートテーブルとサブネットを紐づけるリソースです。
つまり`aws_route`や`aws_route_table_association`はイメージしやすい実体を持っていません。

これが`aws_route_table`
![04.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1206561/42012066-f494-86d1-0668-12d56675cfce.png)

これが`aws_route`
![05.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1206561/506d8e61-e6f6-d762-e511-93762d1c98d9.png)

これが`aws_route_table_association`
![06.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1206561/524517a9-f3b4-bf1a-fb94-4349555eda8e.png)

こういうリソースの書き方を求められるものは他にもあります。そして多くの場合、調べ辛いです。

### sg.tf
セキュリティグループは通信のルールそのものです。中に入る通信(inbound)と外に出る通信(outbound)の二つがありますがそれぞれに何の通信を許可するか記載したルールそのものです。

今回はec2インスタンスに使うセキュリティグループを作成します。

sg.tfを作成し、下記のコードを入力してください。

```
resource "aws_security_group" "test-sg" {
  name        = "test-sg"
  vpc_id      = aws_vpc.test-vpc.id
  description = "test sg"
}

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

resource "aws_security_group_rule" "outbound_all" {
  type      = "egress"
  from_port = 0
  to_port   = 0
  protocol  = "-1"
  cidr_blocks = [
    "0.0.0.0/0"
  ]
  security_group_id = aws_security_group.test-sg.id
}
```
`test-sg`というセキュリティグループを作成し、それに1つのinboundと1つのoutboundのルールを作成しています。
外部からsshで接続するためのルール：aws_security_group_rule.inbound_ssh
内部から外部に接続するためのルール：aws_security_group_rule.outbound_all

かなりガバガバなセキュリティグループですが、一旦これで進めます。

### ec2.tf
ec2の主役、インスタンスを作成します。外部から接続するためのElasticIPアドレスを取得して作成したインスタンスに紐付けもします。

ec2.tfを作成し、下記のコードを入力してください。

```
resource "aws_instance" "terraform-ec2-01" {
  ami                         = "ami-02892a4ea9bfa2192"
  availability_zone           = "ap-northeast-1a"
  instance_type               = "t2.micro"
  key_name                    = "study_ec2"
  subnet_id                   = aws_subnet.test-subnet-a.id
  vpc_security_group_ids      = [aws_security_group.test-sg.id]
  associate_public_ip_address = false
  private_ip                  = "10.40.0.10"
  root_block_device {
    volume_type           = "gp2"
    volume_size           = 8
    delete_on_termination = true
  }
  tags = {
    "Name" = "terraform-ec2-01"
  }
  lifecycle {
    ignore_changes = all
  }
}

resource "aws_eip" "ec2-eip" {
  instance = aws_instance.terraform-ec2-01.id
  vpc      = true
}
```
`aws_eip`はAWSで取得できるElasticIPです。無いとは思いますが取得だけして使用しないと支払いが発生します。
`key_name`で接続に使用するキーペアを指定しています。これは下記ページで作成したキーペアを指定していますが任意の物に変えてもらって結構です。事前に作っておく必要がありますので、作成していない方は次に進む前にキーペアを作成してください。

https://qiita.com/yamanashi7474/items/979084e0532f5633c010#%E3%82%AD%E3%83%BC%E3%83%9A%E3%82%A2%E3%82%92%E4%BD%9C%E3%82%8B

## リソースを作成する
.tfファイルの作成が完了しましたので、terraformで作っていきます。
現時点で6つの.tfファイルが存在しているはずです。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ tree
.
├── ec2.tf
├── igw.tf
├── provider.tf
├── rt.tf
├── sg.tf
└── vpc.tf

0 directories, 6 files
```

まず`terraform plan`を実行してください。
リソース名や設定名に何か誤記があればここで弾かれます。適宜修正してください。

問題が無ければ`terraform apply`を実行しましょう。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ terraform apply
~snip~
Plan: 11 to add, 0 to change, 0 to destroy.


Warning: Version constraints inside provider configuration blocks are deprecated

  on provider.tf line 2, in provider "aws":
   2:   version    = "~> 2.0"

Terraform 0.13 and earlier allowed provider version constraints inside the
provider configuration block, but that is now deprecated and will be removed
in a future version of Terraform. To silence this warning, move the provider
version constraint into the required_providers block.

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

~snip~

Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
```
11個のリソースを作成して完了です。

### 作成したインスタンスに接続する
少しブラウザでの作業を含みます。

AWSマネジメントコンソール -> EC2 -> インスタンスの順に移動し、作成したインスタンスを選択して右上の`接続`を押してください。
![07.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1206561/ba4d37cf-50f3-e637-1da1-d9dca2898de6.png)

`SSHクライアント`タブを選択し、下部のssh接続コマンドをコピーしてください。
![08.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1206561/15d71875-07b0-88ba-7fd6-4b3d6bcb83c0.png)

コピーしたコマンドをキーペアを格納したディレクトリで実行すれば作成したEC2にログインできます。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself$ ls
study_ec2.pem  terraform
yamanashi@DESKTOP-9KCV2CQ:~/studymyself$ ssh -i "study_ec2.pem" ec2-user@ec2-54-150-79-122.ap-northeast-1.compute.amazonaws.com
The authenticity of host 'ec2-54-150-79-122.ap-northeast-1.compute.amazonaws.com (54.150.79.122)' can't be established.
ECDSA key fingerprint is SHA256:9u4mfukp5J4XzjmVhrE8JcLY9GiELAVbG+OByyHJ4vA.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'ec2-54-150-79-122.ap-northeast-1.compute.amazonaws.com,54.150.79.122' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
12 package(s) needed for security, out of 42 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-10-40-0-10 ~]$
[ec2-user@ip-10-40-0-10 ~]$
```

ログインできました。

## 再作成してもう一度ログインする
ログイン出来たら、今度はこのインスタンスをterraformで削除して再度作成して見ましょう。
やり方は作業ディレクトリで`terraform destroy`を実行し、全てを削除し終わったら再度`terraform apply`を実行します。割り振られるDNSは変わるのでsshコマンドはもう一度取得するか適宜修正してください。

このようにterraformでは基盤の削除も作成もワンタッチでできるようにする事で、問題が起きた際のリカバリーを迅速に行うことができます。


規模は小さいですが、同じことを提供するシステムの基盤でやれるようにするのがterraformを用いた運用の目標の一つだと思います。