[Terraform] AWSのEC2インスタンスを、コードで一発構築する！

今回はTerraformをご紹介します:clap:

Terraformは、HashiCorpさんが開発しているオープンソースソフトウェアです。
（ビジネス向けの有料版もあります。）

AWSやGCPなどのクラウド上にサーバーなどを構築する作業を、コードで書いて実行するだけで出来るようになります。
最初は大変かもですが、一回慣れれば↓のようなメリットがあります。

* AWSコンソールをいちいちポチポチしなくても環境構築ができるようになる。
* コピーしてちょっといじれば他のケースにも流用できる。
* Gitなどのソース管理ソフトが使えるので、
    * 更新履歴が取れる。
    * 万が一のとき前のバージョンにすぐ戻せる。
    * 複数人で開発作業ができる<br />etc...

なおHCLという言語で書く必要がありますが、全然難しくないです。

今回はこのTerraformでAWSのEC2インスタンスを構築してみましょう。

# おことわり

記事中に登場する固有ID値などは全部ダミーです。
ご自分の環境における値に読み替える、などしてくださいね。

# 必要なもの

## Terraformバージョン 0.12.XX。

インストール方法は本記事でご紹介しますのでご安心を。
ちなみに本記事は↓のバージョンで執筆しました。

```shell
$ terraform --version
Terraform v0.12.26
+ provider.aws v2.68.0
```

## AWS関連

### AWSアカウント。

もちろんご用意下さい。
本記事くらいの操作なら無料枠で全然可能です。
（ただし、AWS会員登録にはクレジットカードが必須です。）

### AWS初期設定。
おすすめ記事 > [AWSアカウントを取得したら速攻でやっておくべき初期設定まとめ](https://qiita.com/tmknom/items/303db2d1d928db720888)

### AWSの基本的な機能（VPC,サブネット,IAM,EC2,AMI,セキュリティグループ）の把握。

おすすめ記事 > [0から始めるAWS入門：概要](https://qiita.com/hiroshik1985/items/6433d5de97ac55fedfde)

### プログラムからEC2インスタンス作成する権限を持ったIAMユーザー。

および、そのIAMユーザーのアクセスキーとシークレットキーを、お手元にご用意ください。

#### 具体的には

* プログラムからAWSを操作することを許可されている。
* 必要なポリシー（できれば最低限必要なものだけに絞る）を付与している。<br />
    * こちらを参考にさせて頂きました > [IAMポリシーによるEC2インスタンス操作制限](https://qiita.com/tyoshitake/items/82ce3d6077f93c8207b4)<br />（ちょっと試すだけなら管理者権限で実施しても良いですが、後始末は重々ご注意を。）

#### ポリシー設定サンプル

ポリシーはAWSコンソールでポチポチする他にJSON形式で設定できますが、↓このポリシー設定で本記事の作業ができることを確認済みです。
パーフェクトとまでは行かないですが、かなり権限を絞ってみました。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:TerminateInstances",
                "ec2:StartInstances",
                "ec2:StopInstances"
            ],
            "Resource": "arn:aws:ec2:*:*:instance/*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ec2:RevokeSecurityGroupIngress",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupEgress",
                "ec2:DeleteSecurityGroup"
            ],
            "Resource": "arn:aws:ec2:*:*:security-group/*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "ec2:RunInstances",
            "Resource": [
                "arn:aws:ec2:*::image/ami-*",
                "arn:aws:ec2:*:*:subnet/*",
                "arn:aws:ec2:*:*:key-pair/*",
                "arn:aws:ec2:*:*:instance/*",
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:security-group/*",
                "arn:aws:ec2:*:*:network-interface/*"
            ]
        },
        {
            "Sid": "VisualEditor3",
            "Effect": "Allow",
            "Action": [
                "ec2:Describe*",
                "iam:PassRole",
                "iam:Get*",
                "iam:List*",
                "ec2:GetConsole*",
                "ec2:ImportKeyPair",
                "ec2:CreateKeyPair",
                "ec2:CreateSecurityGroup",
                "ec2:DeleteKeyPair"
            ],
            "Resource": "*"
        }
    ]
}
```


## 操作端末

* Mac
* Linux

この記事では、上記OSのコマンドインターフェースで、`tfenv`というツールを一段かませてTerraformを使う方法をご紹介します。

※ もし純粋にTerraform本体だけを使うなら、こちらがダウンロードページ。
> https://www.terraform.io/downloads.html

こちらの方法では、ページに載っているOS端末なら大体動くと思います。

### Terraformの話をしていたのにいきなり `tfenv` とか出てきたぞ！？

`tfenv`は、Terraformの複数バージョンを管理して、好きなバージョンのTerraformを呼び出すようにワンクッションかませられるツールです。
> https://github.com/tfutils/tfenv

「Terraformをバージョンアップしたら、同じコードなのに動かなくなっちゃった…」
こんな事が発生したら、アワアワしちゃいますよね。
そういった問題を回避できるツールなのです。
便利なのでtfenvを使う前提で進めてまいりましょう。

# tfenvインストール編

## Macの場合

### brewコマンド（Homebrew）インストール

Homebrewは、コマンド操作でMacにソフトウェアをインストールするのに便利なツールで、とても有名です。
この記事の手順を実施するのにも必要なので、もし未導入でしたら↓こちらの記事をご紹介するのでインストールをすませておきましょう。
> [Homebrewのインストール](https://qiita.com/zaburo/items/29fe23c1ceb6056109fd)

### tfenvインストールの前に

もしすでにTerraform単体でインストールしてしまっていたら、以下のコマンドを実行しておきましょう。

### tfenvインストール

```MacですでにTerraformが入ってしまっている場合に事前に行う手順.
# もしすでにTerraformが入っていて tfenv install がコケたら、これをする。
brew unlink terraform 
```

では改めまして...
Macに tfenv をインストールするには、以下のコマンドを打てばOKです。

```Macでtfenvをインストールするコマンド.
brew install tfenv
```

## Linuxの場合

Linuxの場合は、githubから tfenv 一式をまんま取得してしまいましょう。

その中の/binディレクトリに、ビルド済のtfenvコマンドファイルがあるので、tfenvのbinディレクトリにPATHを通してどこからでもtfenvコマンドを打てるようにしてあげれば、インストール完了です。

```Linuxでtfenvをインストールする一連のコマンド.
git clone https://github.com/tfutils/tfenv ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
```

# tfenvを使ってTerraformをインストール

## まずは、Terraformのバージョン一覧を取得してみよう

以下のコマンドで出来ます。

```Terraformのバージョン一覧を見るコマンド.
tfenv list-remote
```

## Terraformインストール

せっかくバージョン一覧を調べたところですが、latest (**「正式版で一番新しいやつを頼む」**) という指定でインストールしてしまいましょう。

```tfenvでTerraform最新版をインストールするコマンド.
tfenv install latest
```

## 使用するTerraformバージョンを指定しておく

インストールしただけではダメです、これも忘れずに。
複数バージョンのTerraformが扱えるtfenvならではですね。

```Terraformバージョンを指定するコマンド.
tfenv use latest
```

# 実践編

## 作業ディレクトリを作る

Terraformでは基本、１プロジェクト１ディレクトリという風になっています。
ので、ディレクトリを作りましょう。

```
mkdir hogehoge
```

そして、ディレクトリの中に入っておきましょう。

```
cd hogehoge
```

## AWSアクセス情報ファイルを作る

まずはTerraformにAWSアクセス情報を与えてあげないと何も出来ません。
というわけでやっていきましょう。

viなどのテキストエディタで `terraform.tfvars` というファイル名で編集を開始します。（この名前にしておけば、自動的に読み込んでくれるのです。）

```
vi terraform.tfvars
```

中身はというと、

```terraform.tfvars
access_key_sample = "AKAOFUOAE92AFHGHESY" //←ご自分のAWSアクセスキーに書き換えてね
secret_key_sample = "RHaZsoghSOGH93ShlgihhrilirsrurhEzg+" //←ご自分のAWSシークレットキーに書き換えてね
region_sample = "ap-northeast-1" //←日本国外でご利用なら、お好みで書き換えてね
```

こんな感じ。（ちなみに↑のキーはデタラメのダミーです。）

### 余談： HCL言語でのコメントの書き方

↓こういう書き方をすると、Terraformは無視してくれます。

```Terraformのコメント記法.
# #の後に何を書いても無効

// //の後に何を書いても無効

/*
／＊（半角）と ＊／（半角）のあいだに何を書いても無効（複数行もOK）
／＊（半角）と ＊／（半角）のあいだに何を書いても無効（複数行もOK）
／＊（半角）と ＊／（半角）のあいだに何を書いても無効（複数行もOK）
*/
```

メモを書きとめておくのに利用しましょう。

## SSH接続用鍵ファイルを作成しておく

EC2インスタンスが起動したあとSSH接続するのに必要な鍵ファイルを作成しておきましょう。これがないと、せっかくEC2インスタンスを作っても肝心の接続ができなくなってしまいます。

ご承知の方もいらっしゃると思いますが、念のためコマンド例を↓に書いておきます。

```SSH鍵ファイル作成コマンド例.
ssh-keygen -t rsa -b 4096 -C "foo@example.com" -f aws_key_pair_example
```

実行すると、英語で質問が２回ほど来ますが、今回は例なので、何も入力せずリターンキーを押せばOKです。そうすると以下２つのファイルができあがります。

* aws_key_pair_example  ←秘密鍵ファイル。
* aws_key_pair_example.pub  ←公開鍵ファイル。

※ AWSコンソールの「キーペア」画面で作成もできますが、今回はコマンドで作成したものを使う方法で説明していきます。

## ソースコード本体を書く

それではいよいよTerraformのコードを書いていきます。

テキストエディタなどで `main.tf` というファイル名でコードを書いていきましょう。**（ファイル名の拡張子を `.tf` にしておけば、Terraformが実行対象として認識してくれるのです。）**

```
vi main.tf
```

コードの中身は↓こちら。
<font color="red">**２ヶ所ほど、ご自分の環境に合わせて書き換えて頂く必要があります。**
**`//★`という目印をつけてあるので、コード内を検索してみて下さい。**</font>

```main.tf

//terraform.tfvars に書いたアクセスキーなどを格納する変数を宣言しています。
variable access_key_sample {}
variable secret_key_sample {}
variable region_sample {}

/*
この宣言により、AWS操作を実行する際のアクセスキーなどを
自動的に参照してくれるようになります。
*/
provider "aws" {
  access_key = var.access_key_sample
  secret_key = var.secret_key_sample
  region     = var.region_sample
}

/*
 Amazon公式AMIのAmazon Linux2の最新版を取得するための条件を宣言しています。
*/
data "aws_ami" "al2_latest" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "architecture"
    values = ["x86_64"]
  }
  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  filter {
    name   = "block-device-mapping.volume-type"
    values = ["gp2"]
  }
  filter {
    name   = "state"
    values = ["available"]
  }
}

/*
  SSH鍵ファイルの"公開鍵"の方を指定します。
  秘密鍵の方を指定してしまわないようご注意！
*/
resource "aws_key_pair" "example" {
  key_name   = "example"
  public_key = file("aws_key_pair_example.pub")
}

//新たにセキュリティグループを作成するための宣言です。
resource "aws_security_group" "example" {
  name = "example"
  vpc_id = "vpc-hoge2222222" //★ご自分のVPCのIDを入力してください。
}

/*
  上で宣言した "example"セキュリティグループにインバウンドルールを追加します。
  ポート:22でSSH接続をどこのIPからでも許可するよう設定します。
  （本来はIP制限をかけた方がセキュリティ上良いですが、今回は例なので良しとしましょう。）
  これがないと、SSH接続がAWSのファイアウォールにブロックされてしまいます。
*/
resource "aws_security_group_rule" "example_sg_inbound" {
  type = "ingress" //インバウンドルールである。
  from_port = "22" //接続元（=皆さんの端末）から発信する際のポート番号。
  to_port = "22" //接続先（=EC2インスタンス側）で待ち受けるポート番号。
  protocol = "tcp" //TCPプロトコル通信である。
  cidr_blocks = ["0.0.0.0/0"] //どこのIPからでも接続を許可する。
  security_group_id = aws_security_group.example.id
}

//ここから、実際にEC2インスタンスを構築する設定が書かれています。
resource "aws_instance" "example" {

  /* 【必須】
   ベースにしたいAMIのIDを指定します。
   ここでは、上の方で宣言しておいた「最新のAmazon Linux2」という設定を動的に指定しています。
  */
  ami = data.aws_ami.al2_latest.image_id

  /* 【必須】
   インスタンスタイプを指定します。
   今回は、一番安くて低スペックな t2.micro にしましょう。
  */
  instance_type = "t2.micro"

  /*
    EC2インスタンスに適用するセキュリティグループをIDで指定します。
    カンマ区切りで複数指定もできます。
    今回は、上の方で宣言した "example"セキュリティグループ のIDを指定しておきます。
  */
  security_groups = [ aws_security_group.example.id ]

  /*
    構築先のネットワークのサブネットをお好みで指定できます。
    省略するとデフォルトサブネットが適用されます。
    ただしセキュリティグループの作成もTerraformで行う場合は、指定しないとエラーになります。
  */
  subnet_id = "subnet-hoge2222222" //★ご自分のVPCに紐づくサブネットのIDを入力してください。

  //上の方で作ったキーペア設定のIDを動的に指定しています。
  key_name = aws_key_pair.example.id

  /*
   プライベートIPをお好みで指定することもできます。
   （サブネットルールに従わないIPや、すでに使われているIPを指定したらエラー）
  */
//  private_ip = "172.31.0.1"

  //パブリックIPもお好みで指定可能。（こちらも、ルールに合わないIPを指定したらエラー）
//  public_ip = "xxx.xxx.xxx.xxx"

  //ここでは、ルートディスクの容量やタイプを指定できます。
  root_block_device {
    volume_size           = "20"   //容量：20GiB
    volume_type           = "gp2"  //タイプ：gp2
    delete_on_termination = "true" //EC2インスタンスを破棄するとき、このディスクも一緒に破棄する：YES

    //...今回書いた以外にも、色々オプションがあります。
  }
}
```

## 実行

### ディレクトリを初期化する

Terraformの都合上、最初だけちょっとした準備が必要なので、↓のコマンドを実行してあげましょう。
（Terraform作業ディレクトリの最上位階層にて１回だけ実行すればOKです。）

```
terraform init
```

### 実行計画を確認する

次に↓のコマンドを実行してみてください。

```
terraform plan
```

↓こんな感じで色々表示されます。

```shell
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.aws_ami.al2_latest: Refreshing state...

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.example will be created
  + resource "aws_instance" "example" {
      + ami                          = "ami-hogehogehoge22222"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
...
...
...
```

「もし実行したら、こうなるよ」という情報を教えてくれます。
今回はあまり細かく指定していないので、大体の項目が `(known after apply)`（構築後に分かるよ）となっています。

### 実際に実行する

今度こそ実行です。
↓のコマンドを打ってください。

```
terraform apply
```

そうすると↓のように「本当に実行していい？」と聞かれるので `yes` と打ってリターンキーを押しましょう。

```shell
...
...
...
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 と打ち込む！
```

そうするとズラーっとコマンドログが流れていき...

```shell
aws_key_pair.example: Creating...
aws_key_pair.example: Creation complete after 0s [id=example]
aws_instance.example: Creating...
aws_instance.example: Still creating... [10s elapsed]
aws_instance.example: Still creating... [20s elapsed]
aws_instance.example: Creation complete after 23s [id=i-hogehogehoge]

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

こんな感じで終われば成功です！

### 今の状況を確認する

↓のコマンドで、今の状況を確認できます。

```
terraform show
```

振られたIPアドレスなんかもこれで知ることができます。
（`terraform apply`の結果にも表示されますけれども。）

#### SSHで接続して見る

ついでにぜひ、実際にEC2インスタンスに接続も試してみてください。

```shell
ssh -l ec2-user -i ./aws_key_pair_example XXX.XXX.XXX.XXX
```

接続IPは、少し前の手順で確認したIPを指定してください。

### 作ったEC2インスタンスを破棄する

今度は↓のコマンドを打ってみましょう。

```
terraform destroy
```

「破棄を実行したら、こうなっちゃうよ」という情報がズラーっと表示されます。
最後に「本当に破棄する？」と聞いてくるので `yes` と打ってリターンキーを押しましょう。

```shell
...
...
...
      - tags        = {} -> null
    }

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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value:     ←ここに yes と打ち込む！
```

そうするとコマンドログがズラーっと流れていき...

```shell
...
...
...
aws_instance.example: Still destroying... [id=i-hogehogehoge, 30s elapsed]
aws_instance.example: Destruction complete after 35s
aws_key_pair.example: Destroying... [id=example]
aws_key_pair.example: Destruction complete after 0s

Destroy complete! Resources: 4 destroyed.
```

`Destroy complete!`と表示されたら成功です！

AWSコンソールのEC2画面で、実際にインスタンスが破棄されていることを確認してみてください。
またEC2インスタンス以外にも、Terraformにより作成されたリソース（セキュリティグループ、キーペアなど）も合わせて削除してくれます。

# あとがき

いかがでしたでしょうか？
Terraformではこの他にも、VPCそのものやロードバランサーの作成まで出来てしまいます。
ぜひ活用してみて下さい。