TerraformでAWS構築（Mac編　事前準備〜VPC作成）

# はじめに
Terraformって便利ですよね。
サーバ構成や設定をコードで管理するためのツール（IaC：Infrastructure as Code）で、AWS、GCP、Azureを始め、様々なサービスに対応してます。

必要な環境をまとめて構築、変更、削除でき、設定値を変えれば開発環境、ステージング環境、本番環境を同じ構成で作れるので、とても便利です。

使えるようになるとメンテナンス性が飛躍的にアップするので、まずは基本だけでもマスターしましょう:slight_smile:

　※Linuxのコマンドラインを操作できる方が対象となります。
　※各公式サイトのリンクを貼っているので、Mac以外の環境については、そちらをご確認ください:bow:

Windowsの場合は、WSL（Windows上でLinuxを動作させる仕組み）を使うといいかもしれません。
https://docs.microsoft.com/ja-jp/windows/wsl/

# 今回やること
* 事前準備
* Terraformの初期設定
* VPCの作成

# 事前準備
## AWSのアクセスキー、シークレットキーを発行
  * AWSのアカウント＆作業用ユーザーを用意（手順は割愛します）
  * 作業用ユーザーに、構築に必要な権限を付与する（してもらう）
  * 作業用ユーザーの「認証情報＞アクセスキー」からアクセスキーを発行する
  * アクセスキー、シークレットキーをメモまたはダウンロードしておく（※シークレットキーは作成したときしか知ることはできません。必ず控えておきましょう。）

※個人のAWSアカウントでやる場合も、ルートユーザーは使わずに、作業用のユーザーを作成した方がいいです。（必要最小限の権限に絞れるので）
※作業用ユーザーの権限は、よく分からなかったらAdministratorAccess管理ポリシーを付与しておけば大丈夫です。（ルートユーザーではないので）

## AWS CLIのインストール

```ruby:AWSCLIのインストール
$ curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
$ sudo installer -pkg AWSCLIV2.pkg -target /
```
他の環境へのインストールや詳しい情報はこちら
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-install.html

#### tfenv（Terraformの管理ツール）のインストール

Terraformはバージョンが変わると、一部、互換性が無い場合があります。自分が作ったものであれば問題ありませんが、他人が作ったものはバージョンが違ったりすることも。
tfenvはバージョンをすぐに切り替えられて便利なので、これを使って進めます。

```ruby:tfenv（Terraformの管理ツール）のインストール
$ brew install tfenv
```
他の環境へのインストールや詳しい情報はこちら
https://github.com/tfutils/tfenv

## terraformのインストール

```ruby:最新版をインストールしたいとき
$ tfenv install latest
Installing Terraform v0.15.4
Downloading release tarball from https://releases.hashicorp.com/terraform/0.15.4/terraform_0.15.4_darwin_amd64.zip
######################################################################## 100.0%
Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.15.4/terraform_0.15.4_SHA256SUMS
==> Downloading https://ghcr.io/v2/homebrew/core/pcre/manifests/8.44
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/pcre/blobs/sha256:ed9b483538da7bc6559d2e63dd36659736fab9510681661d970d707a18731de4
〜中略〜
All commands have been installed with the prefix "g".
If you need to use these commands with their normal names, you
can add a "gnubin" directory to your PATH from your bashrc like:
  PATH="/usr/local/opt/grep/libexec/gnubin:$PATH"
No keybase install found, skipping OpenPGP signature verification
Archive:  tfenv_download.xfh6sb/terraform_0.15.4_darwin_amd64.zip
  inflating: /usr/local/Cellar/tfenv/2.2.2/versions/0.15.4/terraform  
Installation of terraform v0.15.4 successful. To make this your default version, run 'tfenv use 0.15.4'
```
バージョンを指定してインストールしたいときは、以下のようにinstallの後にバージョンを書いてください。

```ruby:インストールするバージョンを指定したいとき
$ tfenv install 0.12.6
Installing Terraform v0.12.6
Downloading release tarball from https://releases.hashicorp.com/terraform/0.12.6/terraform_0.12.6_darwin_amd64.zip
######################################################################## 100.0%
Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.12.6/terraform_0.12.6_SHA256SUMS
No keybase install found, skipping OpenPGP signature verification
Archive:  tfenv_download.Y85qVG/terraform_0.12.6_darwin_amd64.zip
  inflating: /usr/local/Cellar/tfenv/2.2.2/versions/0.12.6/terraform  
Installation of terraform v0.12.6 successful. To make this your default version, run 'tfenv use 0.12.6'
```
使うバージョンを指定するため、インストール済みのバージョンを確認します。

```ruby:インストール済みバージョンの一覧表示
$ tfenv list
  0.15.4
  0.12.6
No default set. Set with 'tfenv use <version>'
```
バージョン0.15.4 を使うように、useで指定します。

```ruby:使いたいバージョンの指定
$ tfenv use 0.15.4
Switching default version to v0.15.4
Switching completed

$ tfenv list
* 0.15.4 (set by /usr/local/Cellar/tfenv/2.2.2/version)
  0.12.6
```

## awsのcredentialsを設定

※「region name」は、利用するAWSアカウントのリージョンと合わせてください。
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions
ここでは「アジアパシフィック (東京)」の想定で、「ap-northeast-1」を使います。

```ruby:awsのcredentialsを設定
$ aws configure
AWS Access Key ID [None]: AAAAAAAAAAAAAAAAAAA
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: json
```
```ruby:credentialsの確認
$ cat ~/.aws/credentials
[default]
aws_access_key_id = AAAAAAAAAAAAAAAAAAA
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

実際の開発では開発環境、本番環境などでAWSアカウントを分ける場合が多いので、切り替えられるようにprofileも設定しておきます。（Terraformで環境を切り替えるのに必要です）

```ruby:プロファイルの登録
$ aws configure --profile dev-user
AWS Access Key ID [None]: BBBBBBBBBBBBBBBBBBB
AWS Secret Access Key [None]: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

```ruby:プロファイルの確認
$ cat ~/.aws/credentials
[default]
aws_access_key_id = AAAAAAAAAAAAAAAAAAA
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

[dev-user]
aws_access_key_id = BBBBBBBBBBBBBBBBBBB
aws_secret_access_key = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
```

## 作業場所の用意

ローカルの環境もすっきりさせておきたいので、作業場所を作っておきます。（ディレクトリ名は何でもOKです）

```ruby:作業場所の用意
$ mkdir -p ~/work/dev-test-terraform
$ cd ~/work/dev-test-terraform
```
※ mkdirに-pオプションを付けると、ディレクトリをまとめて作成してくれます。

## tfstateファイルの置き場所をS3に用意
tfstateというのは、terraformで構築した情報が入っているファイルです。
S3に置かずローカルだけでもいいのですが、最初からS3に置いた方が、利便性が飛躍的に高まります。複数人での共同作業や、作業PCが壊れることも考えて、最初からS3に置くようにしましょう。

```ruby:S3にtfstate用のバケットを作成
$ aws s3 mb --profile dev-user s3://xxxxxxxx-dev-terraform-tfstate
make_bucket: xxxxxxxx-dev-terraform-tfstate
```
※S3のバケット名はグローバルで一意な必要があります。すでに使われているものは設定できないので、xxxxxxxx部分を適切に変更してください。

今回はAWS CLIを使ってS3にバケットを作成しました。
AWS CLIは非常に便利なので、使えるようになると、開発作業が楽になります。

[参考] AWS CLIのS3コマンド
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-services-s3-commands.html

ここまでで、事前準備が終わりです:slight_smile:

それでは、Terraformを使ってVPCの構築を行なってみましょう:smiley:

# Terraformを使った構築
## ディレクトリ構成
まず、完成イメージから。
ディレクトリのイメージは、以下のようになります。

```ruby:ディレクトリ構成
$ pwd;find . | sort | sed '1d;s/^\.//;s/\/\([^/]*\)$/|--\1/;s/\/[^/|]*/|  /g'
/Users/xxxxxxx/work/dev-test-terraform
|--backend.tf
|--dev.tfvars
|--main.tf
|--modules
|  |--vpc
|  |  |--main.tf
|  |  |--output.tf
|  |  |--variables.tf
|--provider.tf

```

（余談ですが）以下のコマンドを使うと、treeコマンドが無くても擬似的にツリー表示ができて便利です。

```ruby:findとsort、sedの組み合わせでツリー表示
$ pwd;find . | sort | sed '1d;s/^\.//;s/\/\([^/]*\)$/|--\1/;s/\/[^/|]*/|  /g'
```

## .tfファイルの用意
完成イメージに沿って、各.tfファイルを用意します。

### main.tf
sourceで.tfファイルの場所を指定します。
ディレクトリ構成に合わせ、今回は「./module/vpc」を指定します。
その他、必要な変数をここで設定してください。
（var.envなど、変数の設定は後で行います）

```ruby:main.tf
module "vpc" {
  source      = "./modules/vpc"
  env         = var.env
  vpc_cidr    = var.vpc_cidr
  app_name    = "test-application"
}
```

他のリソース（EC2, RDB, Lambda, API Gatewayなど）を増やす場合は、同様にmoduleを増やしていきます。

### provider.tf
Terraformで利用するプロバイダー（AWS,GCP,Azure,Datadogなど）を指定します。
今回はAWSの構築を行うので、「aws」を指定します。
「shared_credentials_file」はデフォルトでこの値なので、もし、AWSの認証情報を書いたファイルが別にある場合は、そのパスを設定してください。
また、「profile」で指定した値をcredentialsから読み込みます。（今回は、準備で設定した「dev-user」を使います）

```ruby:provider.tf
provider "aws" {
  region = var.region
  profile = var.profile
  shared_credentials_file = "~/.aws/credentials"
}
```


### backend.tf
このファイルでは、使用するTerraformのバージョンや.tfstateファイルの置き場所を設定します。
前述のように、Terraformはバージョンによって文法が異なるケースがあります。環境による文法エラーを防ぐため、バージョンは固定しておきましょう。（今回は0.15.4を指定してます）

```ruby:backend.tf
terraform {
  required_version    = "0.15.4"

  backend "s3" {
    region  = "ap-northeast-1"
    bucket  = "xxxxxxxx-dev-terraform-tfstate"
    key     = "terraform/aws.tfstate"
  }
}
```
### dev.tfvars
環境ごとの設定を指定します。
他にもstg.tfvars、prd.tfvarsなども作成することで、ステージング環境や本番環境の値も設定することができます。


※.tfファイルや.tfvarsはGitにプッシュしたり誰かと共有したりする前提で作成してください。
AWSの認証情報やDBのパスワードなど、秘匿情報は環境変数に設定し、構築時にコマンドで環境変数を渡すと、より安全に作業を行えます。

```ruby:dev.tfvars
env      = "dev"
region   = "ap-northeast-1"
profile  = "dev-user"
vpc_cidr = "172.31.0.0/16"
```

VPCのCIDRは、以下の範囲が推奨されているので、いずれかの値を指定してください。
・10.0.0.0/16
・172.31.0.0/16
・192.168.0.0/20
詳しくはこちら
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/VPC_Subnets.html

### modules/vpc/main.tf
VPCを構築するためのresourceを設定します。
lower()を使うと全て小文字にしてくれるので便利です。

```ruby:modules/vpc/main.tf
#####################################
# VPC Settings
#####################################
resource "aws_vpc" "vpc" {
    cidr_block           = var.vpc_cidr
    enable_dns_hostnames = true

    tags = {
        Name    = lower("${var.env}-${var.app_name}-vpc")
        project = var.app_name
    }
}

```
### modules/vpc/variables.tf
ここでは、モジュール内（vpc/main.tf）で使う変数の設定を行います。
直接値を書くこともできますが、ここでは値を空にして、dev.tfvarsで設定した値をmain.tfから受け取ってます。

```ruby:modules/vpc/variables.tf
variable "env" {}
variable "app_name" {}
variable "vpc_cidr" {}

```
### modules/vpc/output.tf
設定されている値を出力し、他のモジュールで使うことができます。
このファイルは空でも大丈夫ですが、今回はvpcのidを出力してみます。

```ruby:modules/vpc/output.tf
output "vpc_id" {
  value = aws_vpc.vpc.id
}
```

## VPCの構築
それでは、TerraformでVPCを構築してみましょう。
まずは初期化コマンドを実行して、AWSを構築する環境を整えます。

### terraformの初期化コマンド
「terraform init」を実行します。

```ruby:terraform初期化コマンドの実行例
$ terraform init
Initializing modules...
- dev-test in modules/vpc

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v3.47.0...
- Installed hashicorp/aws v3.47.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

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.
```
これにより、必要なライブラリがインストールされたり、状態を管理する.tfstateファイルが作成されます。
各ファイルの中身は、実際に確認してみるといいと思います。

```ruby:initオプションで作成されたディレクトリとファイル
/Users/xxxxxxx/work/dev-test-terraform
|--.terraform.lock.hcl
|--.terraform
|  |--modules
|  |  |--modules.json
|  |--providers
|  |  |--registry.terraform.io
|  |  |  |--hashicorp
|  |  |  |  |--aws
|  |  |  |  |  |--3.47.0
|  |  |  |  |  |  |--darwin_amd64
|  |  |  |  |  |  |  |--terraform-provider-aws_v3.47.0_x5
|  |--terraform.tfstate
```

### Workspaceの作成
上記で設定ファイル「dev.tfvars」を作成しました。
TerraformではWorkspaceという機能を使って構成ファイル（.tfstate）を別々に管理できるので、「dev」ワークスペースを作成します。

```ruby:Workspaceの作成
$ terraform workspace new dev
Created and switched to workspace "dev"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
```

このコマンドを実行することで、S3に以下のファイルが作られます。

```ruby:
xxxxxxxx-dev-terraform-tfstate.s3.ap-northeast-1.amazonaws.com / env: / dev / terraform / dev-aws.tfstate
```

続いて、Workspaceの確認を行います。

```ruby:Workspaceの確認
$ terraform workspace list
  default
* dev
```

すでにdevが選択されてますね。
もし違うWorkspaceが選択されていたら、以下のコマンドを実行してください。

```ruby:使用するWorkspaceの指定
$ terraform workspace select dev
```
特に何も出力されないので、再度、「terraform workspace list」コマンドで確認すればOKです。

### terraform planコマンドの実行
Terraformの実行環境は整ったので、次は「terraform plan」コマンドを実行してみます。
これは、実際に反映する前に、実行計画を確認できるコマンドです。

文法的に間違っている場合などは、ここでエラーになります。
さて、うまくいくでしょうか。

```ruby:planの実行
$ terraform plan -var-file=dev.tfvars

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.dev-test.aws_vpc.vpc will be created
  + resource "aws_vpc" "vpc" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "172.31.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
      + dhcp_options_id                  = (known after apply)
      + enable_classiclink               = (known after apply)
      + enable_classiclink_dns_support   = (known after apply)
      + enable_dns_hostnames             = true
      + enable_dns_support               = true
      + id                               = (known after apply)
      + instance_tenancy                 = "default"
      + ipv6_association_id              = (known after apply)
      + ipv6_cidr_block                  = (known after apply)
      + main_route_table_id              = (known after apply)
      + owner_id                         = (known after apply)
      + tags                             = {
          + "Name"    = "dev-test-application-vpc"
          + "project" = "test-application"
        }
      + tags_all                         = {
          + "Name"    = "dev-test-application-vpc"
          + "project" = "test-application"
        }
    }

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

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
```

無事に、作成されるVPCの内容が表示されましたね。
それでは、これから実際にAWSへ反映し、構築してみます。

### terraform applyコマンドの実行

先ほどは実行計画を確認するだけでしたが、今度は確認後、「yes」で承認します。

```ruby:applyの実行
$ terraform apply -var-file=dev.tfvars

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.dev-test.aws_vpc.vpc will be created
  + resource "aws_vpc" "vpc" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "172.31.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
      + dhcp_options_id                  = (known after apply)
      + enable_classiclink               = (known after apply)
      + enable_classiclink_dns_support   = (known after apply)
      + enable_dns_hostnames             = true
      + enable_dns_support               = true
      + id                               = (known after apply)
      + instance_tenancy                 = "default"
      + ipv6_association_id              = (known after apply)
      + ipv6_cidr_block                  = (known after apply)
      + main_route_table_id              = (known after apply)
      + owner_id                         = (known after apply)
      + tags                             = {
          + "Name"    = "dev-test-application-vpc"
          + "project" = "test-application"
        }
      + tags_all                         = {
          + "Name"    = "dev-test-application-vpc"
          + "project" = "test-application"
        }
    }

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

Do you want to perform these actions in workspace "dev"?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.dev-test.aws_vpc.vpc: Creating...
module.dev-test.aws_vpc.vpc: Still creating... [10s elapsed]
module.dev-test.aws_vpc.vpc: Creation complete after 12s [id=vpc-0308ce2f14a10adfd]

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

```
terraform applyは成功したようです。
実際にVPCが作成されているか、コンソール画面で確認してみましょう。

![スクリーンショット 2021-07-04 10.11.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/b61225c0-f79b-c536-1a8d-bf37391c6720.png)

VPCが作成されました！

今回はこれで終わりですが、皆さんもVPCの作成ができたでしょうか？

ここまでを理解しておけば他のリソースにも応用できるので、順次追加していこうと思います。

## 最後に
今回は、TerraformによるAWS構築について、準備からVPCの作成までを行いました。
視覚的に分かりやすくするため、コマンドをそのまま書きましたが、慣れてきたらシェルなどを使って、環境ごとのスクリプトを実行するだけで構築できるようにすることも可能です。

次回はAPI GatewayとLambdaを使った、サーバレスの環境を構築します:slight_smile: