rails環境構築その３【terraform】

# はじめに
terraformをつかってAWS(EC2,RDS)を作成します。

# 全体の流れ
1. terraformインストール
2. AWSでIAMユーザー作成
3. AWS CLI導入
    4. AWS CLIにIAMユーザーの登録
5. terraformとは
6. terraform導入
    5. プロバイダーの設定
    6. VPC作成
    7. サブネット作成
    8. インターネットゲートウェイ作成
    9. ルートテーブル作成
    10. EC2作成
    11. Security Groupの作成
    11. RDS作成  

参考
* [10分で理解するTerraform](https://qiita.com/Chanmoro/items/55bf0da3aaf37dc26f73)

# terraformインストール

```
$ brew update
$ brew install terraform
```

# AWSでIAMユーザー作成
参考
* [Identity and Access Management へようこそ](https://console.aws.amazon.com/iam/home?#/home)
* [Terraformで構築するAWS](https://y-ohgi.com/introduction-terraform/first/preparation/)  

```

AdministratorAccessポリシー(管理者権限)を選択してください。

権限が足りないと
Error creating VPC: UnauthorizedOperation: You are not authorized to perform this operation.
とエラーが出ます。
```

# AWS CLI インストール
参考
* [AWS CLI のインストールと設定](https://docs.aws.amazon.com/ja_jp/streams/latest/dev/kinesis-tutorial-cli-installation.html)
順番にやればいいですが、私の場合はpythonのパスや環境でハマりました
エラー解決↓
* [MacOSとHomebrewとpyenvで快適python環境を。](https://qiita.com/crankcube@github/items/15f06b32ec56736fc43a#python%E3%81%AE%E8%A8%AD%E5%AE%9A-virtualenv-pipenv)
* [Python・Python3のインストール先、パス等確認](https://qiita.com/sebeckawamura/items/f5d85f4a7049d92b6c85)
* [Python2.7からPython3.6をデフォルトにする話](https://qiita.com/sebeckawamura/items/4bc5945245877f250d2e)
* [Python3インストール（Mac編）](https://qiita.com/ms-rock/items/72b8f1abc661c539bb09)  

上記のリンクを参考にすればイケルと思います。

```:ターミナル
curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py
sudo pip install awscli
```

エラーなくインストルされバージョンが出れば成功です

```:ターミナル
pip --version
aws --version
```

# AWS CLIにIAMユーザーの登録

```:ターミナル(ホームディレクトリ)
aws configure
または、
aws configure --profile ユーザー名
```

## 順番に記入します

* [AWS CLI の設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html)  
* [AWS-CLIの初期設定のメモ](https://qiita.com/reflet/items/e4225435fe692663b705)  
それぞれ先ほど作成した、IAMユーザーをコピペします。  
リージョンと出力形式はリンク先から選んでください。

```:ターミナル
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [None]: 
Default output format [None]: json
# Asia Pacific (Tokyo) ap-northeast-1
```
# terraformとは
インフラのコード化であります。GUIを通さずにインフラに変更を加えられコードの共有と再利用ができます。
参考
* [インフラの構成管理を自動化するTerraform入門](https://thinkit.co.jp/story/2015/07/14/6212)
* [Terraformを使ってAWSのVPCを作成してEC2を起動した](https://blog.dshimizu.jp/article/1345)
ファイル形式は主に`.tf`と`.tfvars`を使います。
ファイル分割はあとにして全て`main.tf`に書きます。
## 主なコマンド

```:terraformがあるディレクトリに移動して実行する
#　初期化　Terraformで新しく設定を記述した場合、初期化を行う必要があります。
terraform init
# 確認(所謂dry-run)
terraform plan
# 適用 コードの状態をAWS上へ適用
terraform apply
# すべて消去するコマンド
terraform destroy
# リソースの閲覧
terraform show

```
## 準備

```
mkdir terraform
cd terraform
touch main.tf
touch terraform.tfvars
```

# プロバイダーの設定
最初にproviderという指定をする必要がある。
複数の環境に対応しているため、「どのプロバイダーを使うのか？」を宣言する
## providerとは
* その名と通りプロバイダでAWSの他にherokuやGCPもできるらしい
* profileで`aws config`でprofileを指定した場合はその名前、していない場合は`"default"`
* `region  = "ap-northeast-1"`で指定したリージョン内にVPCなどを作っていく


```main.tf

provider "aws" {
  profile = ユーザー名
  region  = "ap-northeast-1"
}
```

## VPCを作成
使うリソース`aws_vpc`

```main.tf
# VPC
resource "aws_vpc" "aws-tf-vpc" {
  cidr_block           = "10.1.0.0/16"
  instance_tenancy     = "default"

  tags = {
    Name = "aws-tf-vpc"
  }
}
```
# 実行
```:terraformがあるディレクトリに移動して実行する
terraform init
terraform plan
terraform apply

# リソースの閲覧
terraform show
```

# 実際に確かめてみる
* [実際にAWSにアクセスして確認してみましょう](https://ap-northeast-1.console.aws.amazon.com/vpc/home?region=ap-northeast-1#vpcs:sort=VpcId)

`aws-tf-vpc`と書いてある`VPC`があれば成功です


## resourceとは

```
resourceはVPCやEC2のような起動したいリソースを定義
リソースの種類は、プロバイダーがAWSの場合はaws_*という名前でTerraformで予め定義されています。VPCであればaws_vpc、EC2であればaws_instanceです。
```
## resourceの構文

```
リソースはresourceブロックで設定します。resource "<リースの種類>" "<リソース名>" {}という構文です。
```

## resourceの定義と命名

```
resource "aws_vpc" "this" {

ここでは 「"aws_vpc"というリソースを"this"という名前」 で作成しています。
resource "aws_vpc" まではAWSのVPCを作成するという意味で、 "this" はTerraformで定義する他のリソースから参照する際に使用します。
```

## 他リソースの属性の参照

```
Terraformにはテンプレート内の他リソースの属性を参照する方法があります。
具体的には、<リソースの種類>.<リソース名>.<属性名>で他リソースの属性を参照することができます。
```

その他の知識は下のリンクで
* [AWSでTerraformに入門](https://dev.classmethod.jp/cloud/terraform-getting-started-with-aws/)

## vscodeをterraform v0.12対応させる
```

下記のようなメッセージが出た人向け
For Terraform 0.12 support try enabling the experimental language server with the 'Terraform: Enable/Disable Language Server' command

コマンドパレットを開く(ctl/cmd+shift+p)
terraform: install/update language server -> 現在最新の v0.0.9 を選択
terraform: Enable/Disable Language Server を実行
一度vscodeを閉じたらHCL2記法の.tfファイルでもエラーが出なくなりました。

```
* [VSCodeでTerraformを書くときの設定(2019/11/07追記: HCL2対応)](https://qiita.com/pypypyo14/items/5520f3defa55119f3a1a)
* [feat: Terraform 0.12 support (was hcl2 support) #157](https://github.com/mauve/vscode-terraform/issues/157)




# .gitignoreに上げて行けないファイルの追加
下記のファイルはgithubに上げてはいけないとので追加します。
* [Terraform.gitignore](https://github.com/github/gitignore/blob/master/Terraform.gitignore)

```:.gitignore
#  Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# .tfvars files
*.tfvars
```

# サブネット作成
使うリソース`aws_subnet`

```main.tf

# サブネット２つ作成(publicとprivate)
resource "aws_subnet" "aws-tf-public-subnet-1a" {
  vpc_id            = aws_vpc.aws-tf-vpc.id
  cidr_block        = "10.1.1.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    Name = "aws-tf-public-subnet-1a"
  }
}

resource "aws_subnet" "aws-tf-private-subnet-1a" {
  vpc_id            = aws_vpc.aws-tf-vpc.id
  cidr_block        = "10.1.20.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    Name = "aws-tf-private-subnet-1a"
  }
}

```

# インターネットゲートウェイの作成
使うリソース`aws_internet_gateway`

```main.tf
resource "aws_internet_gateway" "aws-tf-igw" {
  vpc_id = aws_vpc.aws-tf-vpc.id
  tags = {
    Name = "aws-tf-igw"
  }
}
```

# ルートテーブルの作成
使うリソース`aws_route_table`

```main.tf
resource "aws_route_table" "aws-tf-public-route" {
  vpc_id = aws_vpc.aws-tf-vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.aws-tf-igw.id
  }
  tags = {
    Name = "aws-tf-public-route"
  }
}
```

## サブネットの関連付けでルートテーブルをパブリックサブネットに紐付け
使うリソース`aws_route_table_association`

```main.tf
resource "aws_route_table_association" "aws-tf-public-subnet-association" {
  subnet_id      = aws_subnet.aws-tf-public-subnet-1a.id
  route_table_id = aws_route_table.aws-tf-public-route.id
}
```

# EC2作成(public側に作る)
使うリソース`aws_instance`、`aws_key_pair`

```main.tf
resource "aws_instance" "aws-tf-web" {
  ami                     = "ami-011facbea5ec0363b"
  instance_type           = "t2.micro"
  disable_api_termination = false
  key_name = aws_key_pair.auth.key_name
  vpc_security_group_ids  = [aws_security_group.aws-tf-web.id]
  subnet_id               = aws_subnet.aws-tf-public-subnet-1a.id

  tags = {
    Name = "aws-tf-web"
  }
}

# amiとはAmazon Linux 2 AMIです。今回は実際のamiの値を直接入れてますが、いつも最新版にできます。

variable "public_key_path" {}

resource "aws_key_pair" "auth" {
    key_name = "terraform-aws"
    public_key = file(var.public_key_path)
}
```

```terraform.tfvars
# ローカルに鍵がある場所を指定
public_key_path = "~/.ssh/terraform-aws.pub"
```
## amiを常に最新版にする
* [Terraformでもいつでも最新AMIからEC2を起動したい
](https://dev.classmethod.jp/cloud/aws/launch-ec2-from-latest-ami-by-terraform/)
* [TerraformでAWS AMIの最新を常に使うようにする](https://qiita.com/litencatt/items/998072fed75ba01835d4)

## インスタンスのkey_nameについて　
GUIで作るときはインスタンス作成時にsshキーを新規作成や既存のキーを使いAWSにアクセスしますが、
terraformで作成する場合は`.tfvars`ファイルから参照して公開鍵をインスタンスの`key_name`に貼り付けます。
私は`terraform-aws`という名前で鍵を作成しました。

```:ターミナル
$ cd .ssh
$ ssh-keygen -t rsa
terraform-aws　今回の場合の名前
ここでエンターキーを２連打
$ ls
ここでterraform-aws  terraform-aws.pubができていることを確認
```

参考
* [Resource: aws_key_pair](https://www.terraform.io/docs/providers/aws/r/key_pair.html)
* [Terraform でキーペア登録し起動した EC2 に SSH接続
](https://kenzo0107.hatenablog.com/entry/2017/03/27/215941)  

### Terraform 変数について
Terraform の変数は`variable`ブロックで定義
`var.<変数名>`という書式で参照
外部ファイルに値を定義した場合、`terraform.tfvars`なら自動的に読み込まれて変数に代入されます。
また`.tfvars`はgithubに挙げないとこが大事です。
参考
* [【Terraform 再入門】EC2 + RDS によるミニマム構成な AWS 環境をコマンドライン一発で構築してみよう](https://tech.recruit-mp.co.jp/infrastructure/post-10665/)


# Security Groupの作成
使うリソース`aws_security_group`、`aws_security_group_rule`

```main.tf
resource "aws_security_group" "aws-tf-web" {
  name        = "aws-tf-web"
  description = "aws-tf-web_sg"
  vpc_id      = aws_vpc.aws-tf-vpc.id
  tags = {
    Name = "aws-tf-web"
  }
}

# 80番ポート許可のインバウンドルール
resource "aws_security_group_rule" "inbound_http" {
  type      = "ingress"
  from_port = 80
  to_port   = 80
  protocol  = "tcp"
  cidr_blocks = [
    "0.0.0.0/0",
  ]

  # ここでweb_serverセキュリティグループに紐付け
  security_group_id = aws_security_group.aws-tf-web.id
}

# 22番ポート許可のインバウンドルール
resource "aws_security_group_rule" "inbound_ssh" {
  type      = "ingress"
  from_port = 22
  to_port   = 22
  protocol  = "tcp"
  cidr_blocks = [
    "0.0.0.0/0",
  ]

  # ここでweb_serverセキュリティグループに紐付け
  security_group_id = aws_security_group.aws-tf-web.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",
  ]

  # ここでweb_serverセキュリティグループに紐付け
  security_group_id = aws_security_group.aws-tf-web.id
}
```

## Security Group詳細
* [Terraform でセキュリティグループを管理](https://christina04.hatenablog.com/entry/2015/05/21/122353)
* [Terraformで1つのセキュリティグループに複数のルールを設定する](https://qiita.com/Canon11/items/c1ee988516a6492dfb74)
* [TerraformでSecurity Groupを作ったら上手くいかなかった](https://dev.classmethod.jp/cloud/aws/my-mistake-about-creating-sg-by-terraform/)



# ElasticIP作成
使うリソース`aws_eip`

```main.tf
resource "aws_eip" "aws-tf-eip" {
  instance = aws_instance.aws-tf-web.id
  vpc      = true
}
output "example-public-ip" {
    value = "${aws_eip.aws-tf-eip.public_ip}"
}

```
### outputについて
ElasticIPなどはGUIから確認できますが、`output`ブロックで記述すると`terraform apply`実行時に下記のように出力されます。

```
Apply complete! 

Outputs:

example-public-ip = ElasticIP
```
#RDSの作成
RDSは２つのサブネットが必要なのであと一つ違うアベイラビリティゾーンから作成します。
使うリソース`aws_subnet`、`aws_db_subnet_group`

```main.tf
# RDS用のサブネットを作成
resource "aws_subnet" "aws-tf-private-subnet-1c" {
  vpc_id = aws_vpc.aws-tf-vpc.id
  cidr_block = "10.1.3.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    Name = "aws-tf-private-subnet-1c"
  }
}
# 使用する２つを指定します。
resource "aws_db_subnet_group" "rdb-tf-db" {
    name        = "rdb-tf-db-subnet"
    description = "It is a DB subnet group on tf_vpc."
    subnet_ids  = [aws_subnet.aws-tf-private-subnet-1a.id,aws_subnet.aws-tf-private-subnet-1c.id]
    tags = {
        Name = "rdb-tf-db"
    }
}
```

# DB用のセキュリティーを作成
WebサーバーからのみDBサーバーにアクセスできるようにするためにセキュリティを作ります。
使うリソース`aws_security_group`、`aws_security_group_rule`

```main.tf
# Security Group
resource "aws_security_group" "aws-tf-db" {
    name        = "aws-tf-db"
    description = "aws-tf-db-sg"
    vpc_id      = aws_vpc.aws-tf-vpc.id
    tags = {
      Name = "aws-tf-db"
    }
}

resource "aws_security_group_rule" "db" {
    type                     = "ingress"
    from_port                = 5432
    to_port                  = 5432
    protocol                 = "tcp"
    source_security_group_id = aws_security_group.aws-tf-web.id
    security_group_id        = aws_security_group.aws-tf-db.id
}

# source_security_group_idとはアクセスを許可するセキュリティグループIDつまりWeb側のセキュリティグループを指します。
```

# RDSインスタンスの作成
使うリソース`aws_db_instance`

```main.tf


variable "aws-td-db-username" {}
variable "aws-td-db-password" {}

resource "aws_db_instance" "aws-td-db" {
  identifier              = "aws-td-db"
  allocated_storage       = 20
  name = "db11"
  engine                  = "postgres"
  engine_version          = "11.5"
  instance_class          = "db.t2.micro"
  storage_type            = "gp2"
  username                = var.aws-td-db-username
  password                = var.aws-td-db-password
  vpc_security_group_ids  = [aws_security_group.aws-tf-db.id]
  db_subnet_group_name    = aws_db_subnet_group.rdb-tf-db.name
}

```
```terraform.tfvars

public_key_path = "~/.ssh/terraform-aws.pub"
aws-td-db-username = ※※※※※※※※※※
aws-td-db-password = ※※※※※※※※※
それぞれ指定してください。
```
# 実行
```
terraform plan
terraform apply
# 確認
terrafom show
```

## RDSインスタンス作成時にハマったエラーについて

```:その１
DBName must begin with a letter and contain only alphanumeric characters

これはRDSインスタンスのnameについてのエラーです。私の場合は`name = "db11"`と書きましたが、文字で初めて英数字両方を書かないといけないらしいです。
```
参考
* [AWS aws_db_instance DBName issue](https://github.com/terraform-providers/terraform-provider-aws/issues/9655)

```:その２
Error creating DB Instance: InvalidParameterValue: Invalid DB engine

これはRDSインスタンスの`engine`の名前の書き方でエラーが出ました。今回は`engine = "postgres"`と書きました。
```
参考
* ["Invalid DB engine" when creating AWS/RDS Postgresql instance](https://github.com/terraform-providers/terraform-provider-aws/issues/4492)
* [Engineのセクションを見てください](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBInstance.html)

# 内容一式

```main.tf
provider "aws" {
  profile = プロフィール名
  region  = "ap-northeast-1"
}

# VPC作成
resource "aws_vpc" "aws-tf-vpc" {
  cidr_block           = "10.1.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = "true"
  enable_dns_hostnames = "true"
  tags = {
    Name = "aws-tf-vpc"
  }
}

# サブネット２つ作成(publicとprivate)
resource "aws_subnet" "aws-tf-public-subnet-1a" {
  vpc_id            = aws_vpc.aws-tf-vpc.id
  cidr_block        = "10.1.1.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    Name = "aws-tf-public-subnet-1a"
  }
}

resource "aws_subnet" "aws-tf-private-subnet-1a" {
  vpc_id            = aws_vpc.aws-tf-vpc.id
  cidr_block        = "10.1.20.0/24"
  availability_zone = "ap-northeast-1a"
  tags = {
    Name = "aws-tf-private-subnet-1a"
  }
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "aws-tf-igw" {
  vpc_id = aws_vpc.aws-tf-vpc.id
  tags = {
    Name = "aws-tf-igw"
  }
}

# ルートテーブルの作成
resource "aws_route_table" "aws-tf-public-route" {
  vpc_id = aws_vpc.aws-tf-vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.aws-tf-igw.id
  }
  tags = {
    Name = "aws-tf-public-route"
  }
}

# サブネットの関連付けでルートテーブルをパブリックサブネットに紐付け
resource "aws_route_table_association" "aws-tf-public-subnet-association" {
  subnet_id      = aws_subnet.aws-tf-public-subnet-1a.id
  route_table_id = aws_route_table.aws-tf-public-route.id
}

# EC2作成(public側)
resource "aws_instance" "aws-tf-web" {
  ami                     = "ami-011facbea5ec0363b"
  instance_type           = "t2.micro"
  disable_api_termination = false
  key_name = aws_key_pair.auth.key_name
  vpc_security_group_ids  = [aws_security_group.aws-tf-web.id]
  subnet_id               = aws_subnet.aws-tf-public-subnet-1a.id

  tags = {
    Name = "aws-tf-web"
  }
}
variable "public_key_path" {}

resource "aws_key_pair" "auth" {
    key_name = "terraform-aws"
    public_key = file(var.public_key_path)
}
# Security Group
resource "aws_security_group" "aws-tf-web" {
  name        = "aws-tf-web"
  description = "aws-tf-web_sg"
  vpc_id      = aws_vpc.aws-tf-vpc.id
  tags = {
    Name = "aws-tf-web"
  }
}

# 80番ポート許可のインバウンドルール
resource "aws_security_group_rule" "inbound_http" {
  type      = "ingress"
  from_port = 80
  to_port   = 80
  protocol  = "tcp"
  cidr_blocks = [
    "0.0.0.0/0",
  ]

  # ここでweb_serverセキュリティグループに紐付け
  security_group_id = aws_security_group.aws-tf-web.id
}

# 22番ポート許可のインバウンドルール
resource "aws_security_group_rule" "inbound_ssh" {
  type      = "ingress"
  from_port = 22
  to_port   = 22
  protocol  = "tcp"
  cidr_blocks = [
    "0.0.0.0/0",
  ]

  # ここでweb_serverセキュリティグループに紐付け
  security_group_id = aws_security_group.aws-tf-web.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",
  ]

  # ここでweb_serverセキュリティグループに紐付け
  security_group_id = aws_security_group.aws-tf-web.id
}

# ElasticIP
resource "aws_eip" "aws-tf-eip" {
  instance = aws_instance.aws-tf-web.id
  vpc      = true
}
output "example-public-ip" {
    value = aws_eip.aws-tf-eip.public_ip
}

####RDSの作成
# RDS用のサブネットを作成
resource "aws_subnet" "aws-tf-private-subnet-1c" {
  vpc_id = aws_vpc.aws-tf-vpc.id
  cidr_block = "10.1.3.0/24"
  availability_zone = "ap-northeast-1c"
  tags = {
    Name = "aws-tf-private-subnet-1c"
  }
}
# DB用のセキュリティーを作成
# Security Group
resource "aws_security_group" "aws-tf-db" {
    name        = "aws-tf-db"
    description = "aws-tf-db-sg"
    vpc_id      = aws_vpc.aws-tf-vpc.id
    tags = {
      Name = "aws-tf-db"
    }
}

resource "aws_security_group_rule" "db" {
    type                     = "ingress"
    from_port                = 5432
    to_port                  = 5432
    protocol                 = "tcp"
    source_security_group_id = aws_security_group.aws-tf-web.id
    security_group_id        = aws_security_group.aws-tf-db.id
}

resource "aws_db_subnet_group" "rdb-tf-db" {
    name        = "rdb-tf-db-subnet"
    description = "It is a DB subnet group on tf_vpc."
    subnet_ids  = [aws_subnet.aws-tf-private-subnet-1a.id,aws_subnet.aws-tf-private-subnet-1c.id]
    tags = {
        Name = "rdb-tf-db"
    }
}

variable "aws-td-db-username" {}
variable "aws-td-db-password" {}

resource "aws_db_instance" "aws-td-db" {
  identifier              = "aws-td-db"
  allocated_storage       = 20
  name = "db11"
  engine                  = "postgres"
  engine_version          = "11.5"
  instance_class          = "db.t2.micro"
  storage_type            = "gp2"
  username                = var.aws-td-db-username
  password                = var.aws-td-db-password

  vpc_security_group_ids  = [aws_security_group.aws-tf-db.id]
  db_subnet_group_name    = aws_db_subnet_group.rdb-tf-db.name
}

```

```terraform.tfvars
public_key_path = "~/.ssh/terraform-aws.pub"
aws-td-db-username = ※※※※※※※※※※
aws-td-db-password = ※※※※※※※※※
```
おしまい。
何かありましたらコメント欄で