Terraform(0.12)でVPCからEC2とRDSを作る一例

## この記事で作るもの

- VPC
- RDS(Aurora)
- EC2(踏み台用途)
- EC2キーペア

RDSはVPCのプライベートサブネットに配置する想定です。そのためローカルから接続することができません。
RDSの設定で`パブリックアクセシビリティ`をONにすればアクセスできますが、
セキュリティ上、踏み台サーバー(EC2)を配置することが良いと思われます。
また構築するEC2は、通常のアプリケーションサーバーとしての用途に置き換えても問題ないと思います。

Terraformのバージョンは`0.12.19`となります。

## 前提条件

- awscliが使えること

```
$ brew install awscli
```

```
$ aws --version
aws-cli/1.17.0 Python/3.8.1 Darwin/19.2.0 botocore/1.14.0
```

- Terraform(0.12.19)が使えること

```
$ wget https://releases.hashicorp.com/terraform/0.12.19/terraform_0.12.19_darwin_amd64.zip
$ unzip terraform_0.12.19_darwin_amd64.zip -d /usr/local/bin/
```

```
$ terraform -v
Terraform v0.12.19
```

## ディレクトリ構成

```
.
├── README.md
├── components
│   ├── db
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── provider.tf
│   │   └── variables.tf
│   ├── ec2
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── provider.tf
│   │   └── variables.tf
│   ├── network
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── provider.tf
│   │   └── variables.tf
│   └── securitygroup
│       ├── backend.tf
│       ├── main.tf
│       ├── outputs.tf
│       ├── provider.tf
│       └── variables.tf
├── environments
│   ├── production
│   │   └── terraform.tfvars
│   └── staging
│       └── terraform.tfvars
└── modules
    ├── iam_role
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── key_pair
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── securitygroup
        ├── main.tf
        ├── outputs.tf
        └── variables.tf
```

今回、以下の4つのコンポーネントを用意しました。

- db
- ec2
- network
- securitygroup

AWSへのデプロイは、これらの単位でおこなわれます。
デプロイの順番は`network`→`securitygroup`→`db`→`ec2`とします。

またモジュールについては、以下の3つを用意しました。

- iam_role
- key_pair
- securitygroup

これらは、作成する度に定義を使い回せるようにモジュール化しました。

なお、環境は`ステージング`と`本番`とし、`terraform workspace`によって環境を切り分ける仕様とします。

## 事前準備

### 初期化
まず、自身のAWS認証情報をawscliより入力してください。

```
$ aws configure --profile { 自身で決めたProfile名 }
AWS Access Key ID [None]: { 自身のアクセスキー }
AWS Secret Access Key [None]: { 自身のシークレットアクセスキー }
Default region name [None]: ap-northeast-1
Default output format [None]: 
```

Profile名はTerraformの定義で使います。`default`は避けましょう。

### S3バケットの作成

事前にS3バケットを手動で作成しておきます。
このS3バケットは、Terraformの状態を格納しておく大事なバケットです。
このバケットに格納されている情報をもとに、TerraformがAWSリソースを構築していきます。

コンソール画面で作ってもいいですし、cliでやってもいいでしょう

```
aws s3 --profile { 自身で決めたProfile名 } mb s3://{ 自身で決めたバケット名 }
```

### workspaceの作成

各コンポーネントに移動し、`terraform init`と`terraform workspace new {環境名}`を実施しましょう。

```
$ cd ./components/db/
$ terraform init
$ terraform workspace new staging
$ terraform workspace new production
$ terraform workspace select staging
```

４つのコンポーネントすべてに適用します。

## provider.tfの記述

以下のコンポーネントそれぞれに`provider.tf`を用意します。

- db
- ec2
- network
- securitygroup

それぞれ、以下のように定義しましょう。

```provider.tf
variable "profile" {
  default = "{ 自身で決めたProfile名 }"
}

provider "aws" {
  version = "= 2.45.0"
  region  = "ap-northeast-1"
  profile = var.profile
}
```

## backend.tfの記述

以下のコンポーネントそれぞれに`backend.tf`を用意します。

- db
- ec2
- network
- securitygroup

component名には上記の文字列を入れましょう。`db`なら
`key    = "db/terraform.tfstate"`ということになります。

```backend.tf
terraform {
  required_version = "= 0.12.19"
  backend "s3" {
    region  = "ap-northeast-1"
    encrypt = true

    bucket = "{ 先ほど作成したS3バケット }"
    key    = "{ component名 }/terraform.tfstate"

    profile = "{ 自身で決めたProfile名 }"
  }
}
```

なお、今回はコンポーネントごとに状態を分けているため、このままでは各コンポーネントのリソース情報にアクセスすることができません。
例えば、EC2を作りたいのにどのVPCを指定すればいいのか分からない…といった具合です。
こういうケースでは、`backend.tf`で依存関係のあるコンポーネントを`data`として定義してあげます。

EC2ではVPCとセキュリティグループを作成する必要があるので`network`と`securitygroup`のコンポーネントを記載します。

```components/ec2/backend.tf
terraform {
  required_version = "= 0.12.19"
  backend "s3" {
    region  = "ap-northeast-1"
    encrypt = true

    bucket = "{ 先ほど作成したS3バケット }"
    key    = "bastion/terraform.tfstate"

    profile = "{ 自身で決めたProfile名 }"
  }
}

data "terraform_remote_state" "network" {
  backend = "s3"

  config = {
    bucket = "{ 先ほど作成したS3バケット }"
    key    = "env:/${terraform.workspace}/network/terraform.tfstate"
    region = "ap-northeast-1"

    profile = "{ 自身で決めたProfile名 }"
  }
}

data "terraform_remote_state" "securitygroup" {
  backend = "s3"

  config = {
    bucket = "{ 先ほど作成したS3バケット }"
    key    = "env:/${terraform.workspace}/securitygroup/terraform.tfstate"
    region = "ap-northeast-1"

    profile = "{ 自身で決めたProfile名 }"
  }
}
```

`dbコンポーネント`はVPCを指定する必要があるので`network`を記載します
`securitygroupコンポーネント`もVPCを指定する必要があるので`network`を記載します
　※厳密にはDBもセキュリティグループは必要ですが、後述しますが今後修正することはない定義になるので今回はdbコンポーネントに作っちゃいます

## VPCの作成

以下を作成します

- VPC
- パブリックルートテーブル
- プライベートルートテーブル (2つ)　※一応将来的にNATGatewayを分けられるようにしてる
- インターネットゲートウェイ
- パブリックサブネット (２つ)
- プライベートサブネット (2つ)

### VPC

```components/network/main.tf
resource "aws_vpc" "default" {
  cidr_block = "10.1.0.0/16"
  enable_dns_support = true
  enable_dns_hostnames = true

  tags = {
    Name = "sample-${terraform.workspace}"
  }
}
```

### パブリックルートテーブル

```components/network/main.tf
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.default.id

  tags = {
    Name = "sample-public-route-table-${terraform.workspace}"
  }
}
```

### プライベートルートテーブル

```components/network/main.tf
resource "aws_route_table" "private_0" {
  vpc_id = aws_vpc.default.id

  tags = {
    Name = "sample-private-route-table-0-${terraform.workspace}"
  }
}

resource "aws_route_table" "private_1" {
  vpc_id = aws_vpc.default.id

  tags = {
    Name = "sample-private-route-table-1-${terraform.workspace}"
  }
}
```

### インターネットゲートウェイ

```components/network/main.tf
resource "aws_internet_gateway" "default" {
  vpc_id = aws_vpc.default.id

  tags = {
    Name = "sample-internet-gateway-${terraform.workspace}"
  }
}

resource "aws_route" "public" {
  route_table_id = aws_route_table.public.id
  gateway_id = aws_internet_gateway.default.id
  destination_cidr_block = "0.0.0.0/0"
}
```

### パブリックサブネット

```components/network/main.tf
resource "aws_subnet" "public_0" {
  vpc_id = aws_vpc.default.id
  cidr_block = "10.1.0.0/24"
  map_public_ip_on_launch = true
  availability_zone = "ap-northeast-1c"

  tags = {
    Name = "sample-public-subnet-0-${terraform.workspace}"
  }
}

resource "aws_subnet" "public_1" {
  vpc_id = aws_vpc.default.id
  cidr_block = "10.1.1.0/24"
  map_public_ip_on_launch = true
  availability_zone = "ap-northeast-1d"

  tags = {
    Name = "sample-public-subnet-1-${terraform.workspace}"
  }
}

resource "aws_route_table_association" "public_0" {
  subnet_id = aws_subnet.public_0.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_1" {
  subnet_id = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}
```

### プライベートサブネット

```components/network/main.tf
resource "aws_subnet" "private_0" {
  vpc_id = aws_vpc.default.id
  cidr_block = "10.1.10.0/24"
  map_public_ip_on_launch = false
  availability_zone = "ap-northeast-1c"

  tags = {
    Name = "sample-private-subnet-0-${terraform.workspace}"
  }
}

resource "aws_subnet" "private_1" {
  vpc_id = aws_vpc.default.id
  cidr_block = "10.1.11.0/24"
  map_public_ip_on_launch = false
  availability_zone = "ap-northeast-1d"

  tags = {
    Name = "sample-private-subnet-1-${terraform.workspace}"
  }
}

resource "aws_route_table_association" "private_0" {
  subnet_id = aws_subnet.private_0.id
  route_table_id = aws_route_table.private_0.id
}

resource "aws_route_table_association" "private_1" {
  subnet_id = aws_subnet.private_1.id
  route_table_id = aws_route_table.private_0.id
}
```

あとで別のコンポーネントから参照するリソースを`outputs.tf`に記載しておきます

```components/network/outputs.tf
output "sample_vpc_id" {
  value = aws_vpc.default.id
}

output "sample_vpc_public_subnet_0_id" {
  value = aws_subnet.public_0.id
}

output "sample_vpc_public_subnet_1_id" {
  value = aws_subnet.public_1.id
}

output "sample_vpc_private_subnet_0_id" {
  value = aws_subnet.private_0.id
}

output "sample_vpc_private_subnet_1_id" {
  value = aws_subnet.private_1.id
}

output "sample_vpc_cider_block" {
  value = aws_vpc.default.cidr_block
}
```
## セキュリティグループの作成
### セキュリティグループ

セキュリティグループの作成はモジュール化してみます。

#### modules

```modules/securitygroup/variables.tf
variable "name" {}
variable "vpc_id" {}
variable "port" {}

variable "cider_blocks" {
  type = list(string)
}
```

```modules/securitygroup/main.tf
resource "aws_security_group" "default" {
  name = var.name
  vpc_id = var.vpc_id

  tags = {
    Name = var.name
  }
}

resource "aws_security_group_rule" "ingress" {
  type = "ingress"
  from_port = var.port
  to_port = var.port
  protocol = "tcp"
  cidr_blocks = var.cider_blocks
  security_group_id = aws_security_group.default.id
}

resource "aws_security_group_rule" "egress" {
  type = "egress"
  from_port = 0
  to_port = 0
  protocol = "-1"
  cidr_blocks = ["0.0.0.0/0"]
  security_group_id = aws_security_group.default.id
}
```

```modules/securitygroup/outputs.tf
output "security_group_id" {
  value = aws_security_group.default.id
}
```

コンポーネントからは以下のようにしてモジュールを呼び出します。
今回設定するインバウンドはSSH(Port=22)です。自身のIPからのみSSHできるように設定しましょう。

#### components

```components/securitygroup/main.tf
module "ec2" {
  source = "../../modules/securitygroup"
  name = "ec2-sg-${terraform.workspace}"
  vpc_id = data.terraform_remote_state.network.outputs.sample_vpc_id
  port = 22
  cider_blocks = var.ec2_access_ip
}
```

vpc_idは、`networkコンポーネント`からリソースの情報を引っ張るところに注意しましょう。
先ほど作成した`components/network/outputs.tf`に記載があること、
`components/securitygroup/backend.tf`に`terraform_remote_state`として記載していることが条件です。

## RDS(Aurora)の作成

RDSでは以下の項目を作成します。

- Auroraモニタリング用IAMロール
- クラスターパラメーターグループ
- DBパラメーターグループ
- DBサブネットグループ
- Auroraセキュリティグループ
- Auroraクラスター
- Auroraインスタンス

### Auroraモニタリング用IAMロール

IAMロールの作成もモジュール化してみます。

```modules/iam_role/variables.tf
variable "name" {}
variable "policy" {}
variable "identifier" {}
```

```modules/iam_role/main.tf
resource "aws_iam_role" "default" {
  name = var.name
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type = "Service"
      identifiers = [var.identifier]
    }
  }
}

resource "aws_iam_policy" "default" {
  name = var.name
  policy = var.policy
}

resource "aws_iam_role_policy_attachment" "default" {
  policy_arn = aws_iam_policy.default.arn
  role = aws_iam_role.default.name
}
```

```modules/iam_role/outputs.tf
output "iam_role_arn" {
  value = aws_iam_role.default.arn
}

output "iam_role_name" {
  value = aws_iam_role.default.name
}
```

モジュールができたらコンポーネントから呼んでみましょう
`AmazonRDSEnhancedMonitoringRole`というポリシーが既にAWS公式で存在します。
こちらを適用しましょう。
`identifier`は、それぞれのサービスごとに固定値が存在しますので注意しましょう。
今回は`monitoring.rds.amazonaws.com`となります。

```components/db/main.tf
data "aws_iam_policy" "aurora_monitoring_policy" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
}

data "aws_iam_policy_document" "aurora_monitoring" {
  source_json = data.aws_iam_policy.aurora_monitoring_policy.policy
}

module "aurora_monitoring_role" {
  source = "../../modules/iam_role"
  name = "aurora_monitoring_role"
  identifier = "monitoring.rds.amazonaws.com"
  policy = data.aws_iam_policy_document.aurora_monitoring.json
}
```

### クラスターパラメーターグループ

今回はcharset関連を`utf8mb4`に変更し、タイムゾーンを東京にしてみます。
その他、要件に従って適切なパラメータを設定しましょう。

```components/db/main.tf
resource "aws_rds_cluster_parameter_group" "sample" {
  name = "sample-cluster-paameter-group-${terraform.workspace}"
  family = "aurora-mysql5.7"

  parameter {
    name = "character_set_client"
    value = "utf8mb4"
  }

  parameter {
    name = "character_set_connection"
    value = "utf8mb4"
  }

  parameter {
    name = "character_set_database"
    value = "utf8mb4"
  }

  parameter {
    name = "character_set_results"
    value = "utf8mb4"
  }

  parameter {
    name = "character_set_server"
    value = "utf8mb4"
  }

  parameter {
    name = "time_zone"
    value = "Asia/Tokyo"
  }
}
```

### DBパラメーターグループ

```components/db/main.tf
resource "aws_db_parameter_group" "sample" {
  name = "sample-db-paameter-group-${terraform.workspace}"
  family = "aurora-mysql5.7"
}
```

### DBサブネットグループ

サブネットのIDは、`networkコンポーネント`からリソースの情報を引っ張るところに注意しましょう。
先ほど作成した`components/network/outputs.tf`に記載があること、
`components/db/backend.tf`に`terraform_remote_state`として記載していることが条件です。

```components/db/main.tf
resource "aws_db_subnet_group" "sample" {
  name = "sample-db-subnet-group-${terraform.workspace}"
  subnet_ids = [
    data.terraform_remote_state.network.outputs.sample_vpc_private_subnet_0_id,
    data.terraform_remote_state.network.outputs.sample_vpc_private_subnet_1_id,
  ]
}
```

### Auroraセキュリティグループ

本来は`securitygroupコンポーネント`で作るものかもしれませんが
生涯固定のインバウンド設定になるので、`dbコンポーネント`で作ってもいいかなと思います。
セキュリティグループ作成用のモジュールを作ってない場合は、当記事を少し戻って確認してください。

今回、Port=3306をVPC内(CiderBlock)の指定でアクセス可能とします。
プライベートサブネットのため外からアクセスができませんが、
同じVPC内にパブリックサブネットに配置したEC2インスタンスがあればアクセスができるという想定です。


```components/db/main.tf
module "aurora_sg" {
  source = "../../modules/securitygroup"
  name = "sample-db-${terraform.workspace}"
  vpc_id = data.terraform_remote_state.network.outputs.sample_vpc_id
  port = 3306
  cider_blocks = [data.terraform_remote_state.network.outputs.sample_vpc_cider_block]
}
```

### Auroraクラスター

クラスターについて気をつけるべき点はマスターパスワードです。
当記事の方法では、一旦適当なパスワードを設定しておき、後でコンソール画面から手動で変更します。
`lifecycle`欄に注目してください。次回の変更からはマスターパスワードの変更は無視するような設定になっています。

```components/db/main.tf
resource "aws_rds_cluster" "sample" {
  cluster_identifier = "sample-${terraform.workspace}"
  master_username = "sample"
  master_password = "initial_password" # 手動で変更すること
  database_name = "sample"
  backup_retention_period = 7
  preferred_backup_window = "09:30-10:00" # UTC
  preferred_maintenance_window = "wed:10:30-wed:11:00" # UTC
  engine = "aurora-mysql"
  engine_version = "5.7.mysql_aurora.2.07.1"
  port = 3306
  vpc_security_group_ids = [module.aurora_sg.security_group_id]
  db_subnet_group_name = aws_db_subnet_group.sample.name
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.sample.name
  storage_encrypted = true
  deletion_protection = var.deletion_protection
  enabled_cloudwatch_logs_exports = ["audit", "error", "general", "slowquery"]
  skip_final_snapshot = false
  final_snapshot_identifier = "sample-${terraform.workspace}-final-snapshot"

  lifecycle {
    ignore_changes = [master_password]
  }
}
```

### Auroraインスタンス

なお今回、インスタンス数とインスタンスタイプについては、ステージングと本番で差異が発生すると想定させていただきました。
ステージングはインスタンス数1つで小スペック
本番はインスタンス数2つで中〜大スペック
といった具合です。
これらの変数は以下に定義しておきます。

```envronments/staging/terraform.tfvars
# Auroraスペック
instance_class = "db.t3.small"

# Auroraインスタンス数
cluster_instance_count = 1
```

```envronments/production/terraform.tfvars
# Auroraスペック
instance_class = "db.r5.large"

# Auroraインスタンス数
cluster_instance_count = 2
```

```components/db/main.tf
resource "aws_rds_cluster_instance" "sample" {
  count = var.cluster_instance_count
  identifier = "sample-${terraform.workspace}-${count.index}"
  cluster_identifier = aws_rds_cluster.sample.id
  instance_class = var.instance_class
  db_subnet_group_name = aws_db_subnet_group.sample.name
  db_parameter_group_name = aws_db_parameter_group.sample.name
  monitoring_role_arn = module.aurora_monitoring_role.iam_role_arn
  monitoring_interval = 60
  engine = "aurora-mysql"
  engine_version = "5.7.mysql_aurora.2.07.1"
  ca_cert_identifier = "rds-ca-2019"

  # 変更をすぐに適用する場合
  # apply_immediately = true
}
```

## EC2の作成

当記事では踏み台サーバーの用途でEC2を作成しますが、
アプリケーション用のサーバーとして作っても問題はありません。その場合、適切なセキュリティグループを作成するように心がけましょう。

`ec2コンポーネント`で作成するリソースは以下です。

- キーペア
- インスタンス

### キーペア

キーペアはモジュール化します。
ファイルのパーミッションに注意しましょう。
SSH接続するときに怒られます。
例) `file_permission = "0400"`

変数の`public_key_file`および`private_key_file`は鍵の出力先を指定します。

```modules/key_pair/variables.tf
variable "public_key_file" {}
variable "private_key_file" {}
variable "key_name" {}
```

```modules/key_pair/main.tf
resource "tls_private_key" "keygen" {
  algorithm = "RSA"
  rsa_bits = 4096
}

resource "local_file" "private_key_pem" {
  filename = var.private_key_file
  content = tls_private_key.keygen.private_key_pem
  file_permission = "0400"
}

resource "local_file" "public_key_openssh" {
  filename = var.public_key_file
  content = tls_private_key.keygen.public_key_openssh
  file_permission = "0600"
}
```

```modules/key_pair/output.tf
output "private_key_file" {
  value = var.private_key_file
}

output "private_key_pem" {
  value = tls_private_key.keygen.private_key_pem
}

output "public_key_file" {
  value = var.public_key_file
}

output "public_key_openssh" {
  value = tls_private_key.keygen.public_key_openssh
}

resource "aws_key_pair" "key_pair" {
  key_name   = var.key_name
  public_key = tls_private_key.keygen.public_key_openssh
}

output "key_name" {
  value = aws_key_pair.key_pair.key_name
}
```

コンポーネント側の処理です。

```components/ec2/main.tf
module "ec2_key" {
  source = "../../modules/key_pair"
  public_key_file = "./ec2-${terraform.workspace}.id_rsa.pub"
  private_key_file = "./ec2-${terraform.workspace}.id_rsa"
  key_name = "ec2-${terraform.workspace}"
}
```

### インスタンス

先ほど作成したキーペアを指定しましょう。
なお、AMIは`Amazon Linux2`にしています。当記事ではAMIのIDを直書きしていますが
要件に従って自由に選んでください。

```components/ec2/main.tf
resource "aws_instance" "ec2" {
  ami = "ami-011facbea5ec0363b"
  instance_type = "t3.nano"
  availability_zone = "ap-northeast-1c"
  ebs_optimized = false
  vpc_security_group_ids = [data.terraform_remote_state.securitygroup.outputs.ec2_security_group_id]
  key_name = module.ec2_key.key_name
  subnet_id = data.terraform_remote_state.network.outputs.sample_vpc_public_subnet_0_id
  associate_public_ip_address = true

  tags = {
    Name = "sample-${terraform.workspace}"
  }
}
```

## デプロイ

いよいよデプロイです！
デプロイコマンド実行時に、環境変数のファイルを指定するのを忘れないでください。
また、`workspace`には気をつけましょう。
ステージングにデプロイするときは`terraform workspace select staging`しましょう。

```
$ cd ./components/network/
$ terraform workspace select staging

$ terraform plan -var-file="../../environments/$(terraform workspace show)/terraform.tfvars"
```

```
$ terraform apply -var-file="../../environments/$(terraform workspace show)/terraform.tfvars"
```

`network`→`securitygroup`→`db`→`ec2`の順であれば滞りなくデプロイできるかと思います。

以上