第1回「EC2」@Baby Step Terraform for AWS

# お題
英語的におかしいであろう表題のことは置いといて、ある（小さな）アプリをAWS上で動かそうとした時にTerraformを使って少しずつ目的のものに近づけていくことを試みる。
Terraformのテンプレートファイルの記述も、最初は愚直に、問題に直面しつつ徐々に改修していく想定。
世の中の記事や書籍では最初からベストプラクティスを踏まえた設計をすることが多い。
でも、理解のためには、まず目的とする最低限の書き方で書いて、そこから「今、こうなってるからこうした方がいい」、「次にこれらを追加する想定だから、こうしておいた方がいい」といった改善をしてベストプラクティスに近づいた方がいいと思う。

以下、構築予定。ただし、今回は１の`EC2`上でWebアプリを動かすとこのみ。

1. アプリ（※）を`EC2`上で動かしてみる。
2. マネージドなDBに接続しにいく。
3. デフォルトのネットワークを使うのではなく`vpc`、`subnet`を定義する。
4. マルチAZ(アベイラビリティゾーン)化とロードバランサーを導入する。
(5. 画像ファイルを`S3`に置いて`CloudFront`経由でアクセスさせる。) ※
6. アプリ（※）をDocker化して`ECS`で動かすようにする。
7. 最後はCI/CD。GitHubにアプリのソースをプッシュしたら自動でビルド・デプロイが走るようにする。
8. `Route 53`使って独自ドメインでアクセスできるようにする。

※Goで適当に作ったアプリ（`http://【デプロイ先ホスト】/hello`にアクセスすると「`hello`」と吐くだけ）
https://github.com/sky0621/go-experiment/tree/af129be808476b56e06073775a92fb5d4cec3980

Terraformのテンプレートファイル(`*.tf`)の書き方やModule化など、最初は愚直なやり方で行い、ちょっとずつベストプラクティス要素を取り入れていく。

極力、無料枠内でやりくりしたいところだけど、立てるリソース如何ではなんとも。。。
https://aws.amazon.com/jp/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=categories%2312monthsfree%7Ccategories%23alwaysfree

## 今回の最終的なイメージ

![step01n.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/b66891ba-1042-c675-fd85-71e287d0452f.png)

[PlantUML](http://plantuml.com/ja/)＋[AWS-PlantUML](https://github.com/milo-minderbinder/AWS-PlantUML)で作成
【参考】https://qiita.com/sky0621/items/f27ca0638635119e6721

# お断り

- AWS自体の説明は薄いので「AWS触ったこともない」人に理解してもらえる内容にはなっていないです。
- 上記に関連して、AWSのアカウントは保持済み、AWSのCredentialはローカルにある前提で`terraform`コマンド叩いています。
- この記事の内容で実際のサービスを本番運用しているわけではないので、このまま真似してプロダクションレディになる保証はないです。

# 開発環境
### # OS - Linux(Ubuntu)
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.2 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
```

### # IDE - Visual Studio Code
```
Version: 1.35.1
Commit: c7d83e57cd18f18026a8162d042843bda1bcf21f
Date: 2019-06-12T14:27:31.086Z
Electron: 3.1.8
Chrome: 66.0.3359.181
Node.js: 10.2.0
V8: 6.6.346.32
OS: Linux x64 4.15.0-47-generic
```

##### vscode-terraform Plugin
```
Name: Terraform
Id: mauve.terraform
Description: Syntax highlighting, linting, formatting, and validation for Hashicorp's Terraform
Version: 1.3.12
Publisher: Mikael Olenfalk
VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=mauve.terraform
```

### # Terraform
```
$ terraform version
Terraform v0.12.2
```

### # tfenv
```
$ tfenv
tfenv 0.6.0
```

# 実践

## 1. ミニマムアプリのEC2デプロイ

アプリはGolang製でLinux(Ubuntu)環境でビルドしたバイナリ（`go-experiment`）がGitHub（※）に上がっている前提。
※ https://github.com/sky0621/go-experiment/tree/af129be808476b56e06073775a92fb5d4cec3980

### Projectのディレクトリ構成

EC2インスタンス１つ立てるだけということもあり、Projectルート直下に１ファイルのみ。

```
$ tree 
.
└── main.tf

0 directories, 1 file
```

### テンプレートファイルの中身チェック

```main.tf
resource "aws_instance" "go-app-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}
```

#### aws_instance

EC2のインスタンス生成を行う。
https://www.terraform.io/docs/providers/aws/r/instance.html

#### ami

[Amazonマシンイメージ](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/AMIs.html)は、[Amazon Linux2](https://aws.amazon.com/jp/amazon-linux-2/)を採用。

#### instance_type

インスタンスタイプの種類はさまざま。
https://aws.amazon.com/jp/ec2/instance-types/
今回は無料枠の対象である`t2.micro`を採用。
https://aws.amazon.com/jp/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=categories%23featured

#### user_data

ここでGitHub経由で`go-experiment`アプリをEC2インスタンス内に格納して起動。

ただ、ここでエラーが起きた場合、どうやって検知できるんだろう・・・。

### AWS環境へ反映

フォーマッターにかけて、

```
$ terraform fmt
main.tf
```

バリデーションチェックして、

```
$ terraform validate
Success! The configuration is valid.
```

プランチェックして、

```
$ terraform plan
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.


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

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.name will be created
  + resource "aws_instance" "name" {
      + ami                          = "ami-0f9ae750e8274075b"
      + arn                          = (known after apply)
　　　　　　　　　〜〜　省略　〜〜
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
　　　　　　　　　〜〜　省略　〜〜
      + tenancy                      = (known after apply)
      + user_data                    = "3a280d56e8e7e3aec19bad9a8f5b696867b8dd39"
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

　〜〜　省略　〜〜
```

いざ、実行。

```
$ terraform apply

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

　〜〜　省略　〜〜

  Enter a value: yes

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

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

EC2インスタンスは？

![screenshot-ap-northeast-1.console.aws.amazon.com-2019-06-23-11-24-22-157.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/e2b034c0-ee68-0938-129e-e96d8f0ac8d6.png)

出来てる。
じゃあ、もうアプリにつながるかというと、

![Screenshot from 2019-06-23 19-08-09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/33782a95-1afb-324b-dce0-851fc1c3f66d.png)

つながらない。

### セキュリティグループの定義

EC2インスタンス立てただけでは`80`番ポートが開いてないので、穴あけ作業が必要。

#### テンプレートファイルに追記

```main.tf
resource "aws_instance" "go-app-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = [aws_security_group.go-app-security-group.id]

　〜〜　省略　〜〜
}

resource "aws_security_group" "go-app-security-group" {
  name = "go-app-security-group"

  ingress {
      from_port = 80
      to_port = 80
      protocol = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
      from_port = 0
      to_port = 0
      protocol = "-1"
      cidr_blocks = ["0.0.0.0/0"]
  }
}

output "go-app-public-dns" {
  value = aws_instance.go-app-server.public_dns
}
```

#### vpc_security_group_ids

作成したセキュリティグループとこのEC2インスタンスを紐付ける。
https://www.terraform.io/docs/providers/aws/r/instance.html#vpc_security_group_ids

#### aws_security_group

紐付けたインスタンスへの”入り”と”出”を制御する。
https://www.terraform.io/docs/providers/aws/r/security_group.html

#### ingress

紐付けるサーバへの”IN"を制御。今回は`tcp:80`番ポートを開ける。特にアクセス元は絞らない。

#### egress

紐付けるサーバの”OUT"を制御。`protocol`を`-1`ないし`all`にすると全ポート開放になるらしい。
https://www.terraform.io/docs/providers/aws/r/security_group_rule.html

### 再度、AWS環境へ反映

Terraformコマンド使うくだりは同じなので省略。
applyが終わり、再度、ブラウザで見てみると、

![Screenshot from 2019-06-24 07-58-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/094bdd71-8d27-db57-f9e8-5f1aae7f7d71.png)

セキュリティグループの設定でポート「`80`」番を開けたので、今度は表示された。

### この段階でのTerraform関連のソース

https://github.com/sky0621/stepup-terraform/blob/821511843b67305945f52491a81c26a7cc1e5e6a/v0.12/aws/step01/main.tf

## 1の後のStepupフェーズ

### おさらい

現在のTerraformテンプレートファイルは下記。

```main.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = [aws_security_group.web-server-security-group.id]

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}

resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

output "web-server-public-dns" {
  value = aws_instance.web-server.public_dns
}
```

### プラクティス

この分量のテンプレートファイルなら一読して理解不可能な複雑さはない。
ただ、少なくともこの後、ネットワークを構成したり、マネージドなDB、S3、ECSというようにAWSのリソースをふんだんに使い出すことになる。
そうなると、`main.tf`だけでは読み解きづらくなるので、現時点でファイルを分けておく。

#### outputs

本家HashiCorpのGitHub上の事例でも、`output`や`variable`(当記事ではまだ使ってない)は専用のファイルを設けている。
※[Subnetの事例](https://github.com/terraform-providers/terraform-provider-aws/tree/master/examples/networking/subnet)

上記にならって以下のようにファイル分けしておく。
以下の通り。

```main.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}

resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

```outputs.tf
output "web-server-public-dns" {
  value = "${aws_instance.web-server.public_dns}"
}
```

※ちなみに、これまで単に「`aws_instance.web-server.public_dns`」と書いていた部分を今回「`"${aws_instance.web-server.public_dns}"`」というように修正した。
修正前の時点でも動作はしていたものの、Visual Studio CodeのTerraformプラグイン上では警告が出ていた。
今回の修正により警告がなくなった。

#### AWSリソース別のファイル化

これも本家HashiCorpの事例や他の記事でもよく見るように、AWSリソース別にファイル化することが多い様子。
あわせて、何度も使われる（であろう）リソースは[Module](https://www.terraform.io/docs/configuration/modules.html)化も行うらしいけど、今回はまだそこまではしない。

今回やるのは、EC2インスタンスリソース分とセキュリティグループ分の別ファイル化。
以下の通り。

```ec2.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}
```

```security_group.tf
resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

#### user_dataの別ファイル化

EC2インスタンス起動時に実行するコマンドを`user_data`に指定しているが、これはいわゆるセットアップスクリプトなので別ファイルとして管理したい。
Terraformでは、ファイル操作や文字列操作ができる（テンプレートファイル内に書ける）組み込み関数が用意されているので、それを利用。

```web-server-setup.sh
#!/bin/bash
sudo yum -y install git
git clone https://github.com/sky0621/go-experiment.git
cd go-experiment
./go-experiment
```

```ec2.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = file("./web-server-setup.sh")
}
```

### ファイル分割後の構成

```
$ tree
.
├── ec2.tf
├── outputs.tf
├── security_group.tf
├── terraform.tfstate
└── web-server-setup.sh
```

※`terraform.tfstate`はAWS適用後の状態を保持するファイル（`terraform apply`実行により自動作成される）

### Providerの設定

#### リージョン指定の謎

これまでのテンプレートファイルで作られるのはAWSの各種リソース。
ただ、Terraformが扱うのはAWSに限ったことではない。GCPだってAzureだって扱える。
じゃあ、何をもってAWSのリソースを使うと判断できるのか。
テンプレートファイルで「`aws_instance`」といった、AWSを示すリソースを指定しているのだから、当然と言えば当然なんだけど。
ここで１つ疑問。
今のところ、テンプレートファイルでリージョンの指定はしていない。
なのに、EC2インスタンスは下記のように東京リージョンに作られている。
![screenshot-ap-northeast-1.console.aws.amazon.com-2019-06-25-16-32-25-813.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/8da9beaf-7610-5d0d-dcf2-15108dbfe295.png)
これは、実はAWSのクライアントツールをセットアップした時に各種クレデンシャル情報に加え、デフォルトのリージョンを下記のように環境変数にセットしたためと思われる。

```
$ env | grep AWS
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx
```

試しに、以下のようにリージョンを「`us-east-2`(=オハイオ)」に変えて`terraform apply`を実行すると、

```
$ env | grep AWS
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=us-east-2
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx
```

![Screenshot from 2019-06-26 06-38-49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/0f1e8602-e3dc-29c2-e10c-bffa48efa9a1.png)

となる。

#### リージョンの明示的な定義

上記のようにTerraformコマンドの実行環境に依存するリージョンを明示的に指定するには「`provider`」を定義する。
https://www.terraform.io/docs/configuration/providers.html
これも、専用のテンプレートファイルとする。
ちなみに、前述の「`outputs.tf`」もそうだったけど、「`variables.tf`」や今回の`provider`が「`providers.tf`」になるようにファイル名には複数形を用いられる（ことが多い？）。
ただし、「`vpc.tf`」や「`security_group.tf`」のように各サービスの場合は単数形を用いている様子。

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

上記のようにしておくと、環境変数として下記のように「`us-east-2`」がセットされていても、

```
$ env | grep AWS
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=us-east-2
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx
```

下のように「`ap-northeast-1`」リージョンでインスタンスが作られる。

![Screenshot from 2019-06-26 08-18-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/b3b84ab3-6c23-28aa-8eb8-ce2fa65bab82.png)

#### その他プロバイダー設定

##### プロバイダーバージョン

プロバイダーとしてはAWSを用いる。そのプロバイダーにはバージョンがある。
今どんなバージョンが存在するかは下記参照。
https://github.com/terraform-providers/terraform-provider-aws/blob/master/CHANGELOG.md

[セマンティックバージョニング](https://semver.org/lang/ja/)を採用しているので、現時点の安定版バージョンの中でバグフィックス起因のアップデートのみ許容する指定にする。
https://www.terraform.io/docs/configuration/providers.html#version-provider-versions

```
provider "aws" {
  version = "~> 2.16.0"
  region = "ap-northeast-1"
}
```

これ以外に指定可能な設定については下記参照。
https://www.terraform.io/docs/providers/aws/index.html#argument-reference

### Terraform自体の設定

プロバイダーのバージョンも大事だけど、そもそもTerraform自体のバージョンも明示的に固定すべき。
これは以下のように、また別ファイル化しておく。

```terraform.tf
terraform {
  required_version = "~> 0.12.0"
}
```

### ファイル分割後の構成

```
$ tree
.
├── ec2.tf
├── outputs.tf
├── providers.tf
├── security_group.tf
├── terraform.tf
└── web-server-setup.sh
```

※あえて `*.tfstate` ファイルは表示上、外した。

上記以外の設定については下記参照。
https://www.terraform.io/docs/configuration/terraform.html

### この段階でのTerraform関連のソース

https://github.com/sky0621/stepup-terraform/tree/595afc446d7b4c9f72fb81331187a52358175f2e/v0.12/aws/step01n

今回は、まだ６ファイルだけなので、中身を全て再掲しておく。

以下は`EC2`インスタンスを立てて外部からアクセスするのに必要な定義。

```ec2.tf
# https://www.terraform.io/docs/providers/aws/r/instance.html
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = file("./web-server-setup.sh")
}
```

```web-server-setup.sh
#!/bin/bash
sudo yum -y install git
git clone https://github.com/sky0621/go-experiment.git
cd go-experiment
./go-experiment
```

```security_group.tf
# https://www.terraform.io/docs/providers/aws/r/security_group.html
resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

↓は、単に`terraform apply`した時にコンソールに出力してほしい内容。

```outputs.tf

output "web-server-public-dns" {
  value = "${aws_instance.web-server.public_dns}"
}
```

以降は、Terraform自体やAWSプロバイダーの定義。

```terraform.tf
terraform {
  required_version = "~> 0.12.0"
}
```

```providers.tf
provider "aws" {
  version = "~> 2.16.0"
  region = "ap-northeast-1"
}
```